angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Utilisation de Python pour obtenir le statut d'envoi Postfix pour les messages avec un message-id

Postfix peut générer des rapports. Je vous présente ici une autre façon de connaître l'état d'envoi des courriels.

20 août 2020
Dans Email
post main image
https://unsplash.com/@walling

Le problème : j'ai un site web qui envoie des e-mails. Les e-mails (meta) sont stockés dans une base de données avant d'être envoyés au Postfix MTA. Chaque courriel envoyé par le site web a un message-id unique, quelque chose comme 159766775024.77.9154479190021576437@peterspython.com. Je veux vérifier si les courriels sont effectivement envoyés et ajouter ces informations aux enregistrements de la base de données (méta) des courriels.

Postfix a des statuts comme :

  • status=bounced
  • status=deferred
  • status=sent

Je commence par envoyer, puis je peux ajouter d'autres statuts.

L'utilisation de Python nous rend la vie plus facile

Je n'ai pas trouvé de programme qui me donne cette information, alors j'ai décidé de l'écrire moi-même. Mon serveur est un serveur Linux (Ubuntu) avec ISPConfig. Nous utilisons le serveur /var/log/mail.log pour obtenir l'information.

Attention : les différentes versions de Postfix peuvent avoir des formats de ligne de compte différents. De plus, sur mon serveur, le courrier électronique sortant est vérifié par le filtre de contenu (spam) Amavis.

Bien sûr, nous utilisons Python pour cette tâche. J'aime aussi la programmation Bash, mais quand les choses deviennent un peu plus complexes, il vaut mieux passer à un langage de haut niveau. J'utilise un mélange ici. Les commandes Linux pour réduire le temps de traitement et la taille du programme, et le code Python pour effectuer un certain filtrage et générer les lignes de résultats.

Grep, Sed, Cut

Linux possède un certain nombre de commandes qui réduisent considérablement nos efforts de programmation, dans ce cas, je les utilise :

  • Grep
  • Sed
  • Coupez

Grep ' a l'option '-f' où les lignes du fichier A peuvent être filtrées par un motif du fichier B. Sed est très facile à utiliser si vous connaissez les expressions régulières. Je l'utilise pour comprimer plusieurs espaces en un seul, ajouter un deux-points à une ligne, supprimer des parties d'une ligne. Cut est utilisé pour sélectionner un ou plusieurs champs de la ligne (filtrée), ou des colonnes d'un fichier.

Postfix, files d'attente et mail.log

Postfix utilise des files d'attente et les messages se voient attribuer des queue_ids. Lorsque nous soumettons un message à Postfix, ce message est vérifié et s'il est accepté, il est mis dans ce que j'appellerai le accept_queue. Nous voyons une ligne du type :

Aug 17 16:07:52 server8 postfix/cleanup[8062]: C9DC982EEB:  message-id=<159767327280.144.3674122625658819909@peterspython.com>

Ici, le accept_queue_id est "C9DC982EEB". Si tout va bien, après un certain temps, votre message est mis dans ce que j'appelle le send_queue. C'est un peu déroutant car la ligne contient 'status=sent' alors qu'elle n'a pas été envoyée ! Dans ce cas, la ligne l'est :

Aug 17 16:07:53 server8 postfix/smtp[8063]: C9DC982EEB: to=<peterpm@xs4all.nl>, relay=127.0.0.1[127.0.0.1]:10026, delay=0.33, delays=0.01/0.01/0.01/0.3, dsn=2.0.0,  status=sent  (250 2.0.0 from  MTA(smtp:[127.0.0.1]:10027): 250 2.0.0 Ok:  queued as 2083482EEC)

Notez la fin de cette ligne : "queued as 2083482EEC)". Pour vérifier si et quand ce message a été accepté par le serveur de courrier de réception, nous devons maintenant rechercher send_queue_id '2083482EEC'. Si vous recherchez mail.log , vous trouverez une ligne qui contient non seulement "status=sent" mais aussi "<> accepted message", où <> est le serveur de courrier électronique du destinataire, par exemple :

Aug 17 16:07:55 server8 postfix/smtp[8067]: 2083482EEC: to=<peterpm@xs4all.nl>, relay=mx3.xs4all.nl[194.109.24.134]:25, delay=2, delays=0.01/0/1.6/0.44, dsn=2.0.0,  status=sent  (250 2.0.0 mxdrop307.xs4all.net accepted message  07HE7rrG015924)

Le code

Pour exécuter les commandes Linux dans Python , j'utilise :

os.system(command)

Le flux est dans la méthode update_message_id_and_status(). Le travail lourd est effectué par les commandes Linux . Elles génèrent un certain nombre de fichiers temporaires. Lorsque nous disposons de suffisamment de données, nous utilisons le code Python pour faciliter le traitement.

Lorsque vous laissez le débogage en marche, le programme affiche les deux premières lignes de chaque fichier qui est créé. Certaines lignes du fichier message_id_status_file résultant :

159767280175.144.12615794910844491932@peterspython.com  sent 2020-08-17 16:00:05
159767326022.144.11661923945211507822@peterspython.com  sent 2020-08-17 16:07:42
159767327280.144.3674122625658819909@peterspython.com  sent 2020-08-17 16:07:55

Voici le script Python :

import  datetime
import inspect
import os
import csv
import subprocess 

MAIL_LOG_FILE='/var/log/mail.log'
MAIL_LOG_FILE='./mail.log' 
TMP_FILE_DIR='/tmp'
POSTFIX_MESSAGE_ID_STATUS_FILE_DIR='.'
POSTFIX_MESSAGE_ID_STATUS_FILENAME='message_id_status.txt'

class  PostfixMessageIdStatus:

    def __init__(self):
        self.mail_log_file = MAIL_LOG_FILE
        self.message_id_status_file = os.path.join(POSTFIX_MESSAGE_ID_STATUS_FILE_DIR,  POSTFIX_MESSAGE_ID_STATUS_FILENAME)
        self.tmp_message_ids_file = os.path.join(TMP_FILE_DIR, 'tmp_message_ids.txt')
        self.tmp_accept_queue_queue_id_message_id_all_file = os.path.join(TMP_FILE_DIR, 'tmp_accept_queue_queue_id_message_id_all.txt')
        self.tmp_accept_queue_queue_id_message_id_file = os.path.join(TMP_FILE_DIR, 'tmp_accept_queue_queue_id_message_id.txt')
        self.tmp_accept_queue_ids_file = os.path.join(TMP_FILE_DIR, 'tmp_accept_queue_ids.txt')
        self.tmp_accept_queue_id_send_queue_id_file = os.path.join(TMP_FILE_DIR, 'tmp_accept_queue_id_send_queue_id.txt')
        self.tmp_send_queue_ids_file = os.path.join(TMP_FILE_DIR, 'tmp_send_queue_ids.txt')
        self.tmp_sent_lines_file = os.path.join(TMP_FILE_DIR, 'self.tmp_send_lines.txt')

        self.my_message_id_domains = [
            '@peterspython.com'
        ]
        self.class_name = 'PostfixMessageIdStatus'
        self.dbg = False

    def get_fname(self):
        frame = inspect.stack()[1]
        function_name = inspect.currentframe().f_back.f_code.co_name
        fname = self.class_name  +  '::'  +  function_name
        return fname

    def print_line_count_and_few_lines_of_file(self, f): 

        if not os.path.isfile(f):
            raise FileNotFoundError(fname, ': file {} does not exist'.format(f))
        result = subprocess.check_output(['wc', '-l', f]).decode("utf-8") 
        line_count = int(result.split(' ')[0]) 
        print('file: {}'.format(f))
        print('line_count: {}'.format(line_count))
        command = "head --lines=2 "  +  f
        os.system(command)

    def create_empty_message_id_status_file_if_not_exists(self, f):
        fname = self.get_fname()
        dbg = self.dbg

        if not os.path.exists(f):
            print(fname  +  ': file {} does not exist so create it'.format(f))
            open(f, 'a').close()

            if not os.path.exists(f):
                raise Exception(fname  +  ': file {} could not be created'.format(f))

    def copy_message_ids_from_message_id_status_file_into_tmp_message_ids_file(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        command = 'cut -d " " -f 1 '  +  self.message_id_status_file  +  ' > '  +  self.tmp_message_ids_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def filter_mail_log_to_get_all_accept_queue_id_and_message_ids(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        # only include our own message_ids using the domain part
        message_id_domains_filter = '-e '  +  ' -e '.join(self.my_message_id_domains)

        # do not remove ':' from the queue_id, we need this later
        command = "grep 'message-id=' "  +  self.mail_log_file  +  " | grep "  +  message_id_domains_filter  +  " | sed 's/\s\s*/ /g' | sed 's/message-id\=<//g' | sed 's/>//g' | cut -d ' ' -f 6,7 | sort -u > "  +  self.tmp_accept_queue_queue_id_message_id_all_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def filter_tmp_accept_queue_queue_id_message_id_all_file_with_tmp_message_ids_file(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        command = "grep -v -f "  +  self.tmp_message_ids_file  +  " "  +  self.tmp_accept_queue_queue_id_message_id_all_file  +  " > "  +  self.tmp_accept_queue_queue_id_message_id_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def create_tmp_accept_queue_ids_file(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        command = "cut -d ' ' -f 1 "  +  self.tmp_accept_queue_queue_id_message_id_all_file  +  " > "  +  self.tmp_accept_queue_ids_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def filter_mail_log_by_accept_queue_ids_and_queued_as_to_get_send_queue_ids(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        command = "grep -f "  +  self.tmp_accept_queue_ids_file  +  ' '   +  self.mail_log_file  +  " | grep 'status=sent' | grep 'queued as' | sed 's/to.*queued as/ /g' | sed 's/)//g' | sed 's/\s\s*/ /g' |  sed 's/$/:/g' | cut -d ' ' -f 6,7 > "  +  self.tmp_accept_queue_id_send_queue_id_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def create_tmp_send_queue_ids_file(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        command = "cut -d ' ' -f 2 "  +  self.tmp_accept_queue_id_send_queue_id_file  +  " > "  +  self.tmp_send_queue_ids_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def filter_mail_log_by_send_queue_ids_and_accepted_message_to_get_sent_lines(self):
        fname = self.get_fname()
        dbg = self.dbg
        if dbg:
            print(fname  +  '()')

        command = "grep -f "  +  self.tmp_send_queue_ids_file  +  ' '   +  self.mail_log_file  +  " | grep 'status=sent' | grep 'accepted message'| sed 's/\s\s*/ /g' > "  +  self.tmp_sent_lines_file
        if dbg:
            print(fname  +  ': command = {}'.format(command))
        os.system(command)

    def  update_message_id_and_status(self):
        fname = self.get_fname()
        dbg = self.dbg
        dbg = True

        if dbg:
            print(fname  +  '()')

        # we use grep, sed, cut to do heavy work

        self.create_empty_message_id_status_file_if_not_exists(self.message_id_status_file)

        self.copy_message_ids_from_message_id_status_file_into_tmp_message_ids_file()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_message_ids_file)

        # after accepting a message, postfix passes the message to cleanup
        # example postfix line:
        # Aug 15 14:51:47 server8 postfix/cleanup[12794]: 23EFC803C7:  message-id=<135717442.1972837.1597495901382@mail.yahoo.com>

        # get all  accept_queue  queue_id and message_id 
        self.filter_mail_log_to_get_all_accept_queue_id_and_message_ids()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_accept_queue_queue_id_message_id_all_file)

        # remove lines from tmp_accept_queue_queue_id_message_id_all_file that contain message_ids we already have
        self.filter_tmp_accept_queue_queue_id_message_id_all_file_with_tmp_message_ids_file()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_accept_queue_queue_id_message_id_file)

        # we now have a file with lines:  accept_queue_id  message_id
        # example:
        # E0EB882BF4: 159777360230.7.2488114074420651363@peterspython.com

        # create from the above a file with only  accept_queue_ids
        self.create_tmp_accept_queue_ids_file()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_accept_queue_ids_file)

        # we now have a file with  accept_queue_ids
        # example:
        # E0EB882BF4:

        # use this file to filter the  mail.log  file to get lines ending with 'queued as D80D582C38)'
        # Aug 15 15:00:01 server8 postfix/smtp[13089]: 82E3D82C26: to=<peterpm@xs4all.nl>, relay=127.0.0.1[127.0.0.1]:10026, delay=0.39, delays=0.03/0.01/0.01/0.33, dsn=2.0.0,  status=sent  (250 2.0.0 from  MTA(smtp:[127.0.0.1]:10027): 250 2.0.0 Ok:  queued as D80D582C38)
        self.filter_mail_log_by_accept_queue_ids_and_queued_as_to_get_send_queue_ids()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_accept_queue_id_send_queue_id_file)

        # we now have a file with  accept_queue_ids and  send_queue_ids
        # example:
        # 9168182C54: 8A6FF82CB8:

        # create from the above a file with only  send_queue_ids
        self.create_tmp_send_queue_ids_file()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_send_queue_ids_file)

        # we now have a file with  send_queue_ids
        # example:
        # 5346D82C08:

        # use this file to filter the  mail.log  file to get lines ending with 'accepted message'
        # Aug 15 15:00:03 server8 postfix/smtp[13029]: D80D582C38: to=<peterpm@xs4all.nl>, relay=mx1.xs4all.nl[194.109.24.132]:25, delay=2, delays=0.01/0.01/1.6/0.42, dsn=2.0.0,  status=sent  (250 2.0.0 mxdrop301.xs4all.net accepted message  07FD01qu014687)
        self.filter_mail_log_by_send_queue_ids_and_accepted_message_to_get_sent_lines()
        if dbg:
            self.print_line_count_and_few_lines_of_file(self.tmp_sent_lines_file)

        # we now have a file with sent lines
        # example:
        # Aug 16 07:00:04 server8 postfix/smtp[12993]: EB13A82DD3: to=<peterpm@xs4all.nl>, relay=mx1.xs4all.nl[194.109.24.132]:25, delay=2, delays=0.01/0.01/1.6/0.46, dsn=2.0.0,  status=sent  (250 2.0.0 mxdrop306.xs4all.net accepted message  07G50207012529)

        # from here we use python to process the sent lines and update our message_id and status file

        # slurp and process self.tmp_accept_queue_id_send_queue_id_file
         accept_queue_id2message_ids = {}
        with open(self.tmp_accept_queue_queue_id_message_id_file) as fh_csv:
            rows = csv.reader(fh_csv, delimiter=' ')
            for row in rows:
                 accept_queue_id2message_ids[row[0]] = row[1]
        if dbg:
            print(fname  +  ':  accept_queue_id2message_ids = {}'.format(accept_queue_id2message_ids))

        # slurp and process self.tmp_accept_queue_id_send_queue_id_file
         send_queue_id2accept_queue_ids = {}
        with open(self.tmp_accept_queue_id_send_queue_id_file) as fh_csv:
            rows = csv.reader(fh_csv, delimiter=' ')
            for row in rows:
                 send_queue_id2accept_queue_ids[row[1]] = row[0]
        if dbg:
            print(fname  +  ':  send_queue_id2accept_queue_ids = {}'.format(send_queue_id2accept_queue_ids))

        # iterate sent lines
        message_id_status_lines = []
        with open(self.tmp_sent_lines_file) as fh:
            for line in fh:
                line_parts = line.split(' ')

                log_dt = '2020 '  +  ' '.join(line_parts[:3])
                log_dt_obj =  datetime.datetime.strptime(log_dt, '%Y %b %d %H:%M:%S')
                log_dt_iso = log_dt_obj.strftime("%Y-%m-%d %H:%M:%S")

                 send_queue_id  = line_parts[5]
                if  send_queue_id  not in  send_queue_id2accept_queue_ids:
                    continue

                 accept_queue_id  =  send_queue_id2accept_queue_ids[send_queue_id]
                if  accept_queue_id  not in  accept_queue_id2message_ids:
                    continue

                message_id =  accept_queue_id2message_ids[accept_queue_id]

                message_id_status_line = '{} sent {}'.format(message_id, log_dt_iso)
                if dbg:
                    print(fname  +  ': {}'.format(message_id_status_line))

                message_id_status_lines.append(message_id_status_line)

        # update message_id_status_file
        if len(message_id_status_lines) > 0:
            with open(self.message_id_status_file, 'a') as fh:
                fh.write('\n'.join(message_id_status_lines)  +  '\n')

# start
postfix_message_id_status =  PostfixMessageIdStatus()
postfix_message_id_status.update_message_id_and_status()

Résumé

En exécutant les commandes Linux à partir de Python , votre programme devient petit et rapide. Une fois que nous avons nos données, nous utilisons le code Python pour faire des choses plus difficiles à faire en Bash. Le traitement du fichier mail.log est un défi, mais une fois que vous savez ce qui se passe, ce n'est plus si difficile. Et enfin, j'ai un fichier contenant les message-id qui ont effectivement été envoyés, c'est-à-dire acceptés par le serveur de messagerie destinataire. Je peux utiliser ce fichier pour mettre à jour les enregistrements (méta) de courrier électronique dans la base de données.

Liens / crédits

Postfix
http://www.postfix.org/

En savoir plus...

Email Postfix

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.