Delivery Status Notification (DSN) – parser en C#

Nous connaissons tous cet e-mail qui commence par…

Mail Delivery Error
 Note: This message was generated automagically. An error was detected, while processing the enclosed message.
[...] 

Si l’on envoie des e-mails de façon automatique (automate, mailing list…), mon avis est qu’il est toujours important, parfois même indispensable, de traiter ces messages d’erreur: informer le destinateur, maintenir une liste de diffusion à jour en supprimant les adresses inexistantes, etc..

Les DSN sont essentiellement couverts par les RFC 3464 (rapport) et 3463 (codes de statut). L’analyse de tels messages est donc relativement simple.

Les serveurs SMTP (MTA) implémentent cependant de façon inégale les DSN. Les rapports contiennent bien souvent plusieurs codes de statut différents. Parfois, le code de statut est très générique et seule la description litérale permet d’interpréter la cause de non délivrance (par exemple, la destination n’existe pas ou est pleine). Tout le jeu consiste donc à identifier le code de statut le plus précis et, parfois, à reconnaître certains motifs à partir d’expressions régulières.

Certains MTA ne supportent pas les DSN tels que spécifiés par les RFC. Curieusement, Gmail ne permet pas, à ce jour du moins, de recevoir un DSN valide à partir d’un message envoyé depuis ses serveurs. La recherche d’un entête X-Failed-Recipients devrait permettre d’identifier les messages d’erreur de Gmail. En revanche, Gmail est bien capable de produire des DSN valides en réponse à d’autres serveurs SMTP. Par conséquent, si l’analyse des DSN vous importe, il convient de ne pas utiliser Gmail pour envoyer vos messages.

À noter: depuis .NET 4.0, la classe MailMessage contient une propriété DeliveryNotificationOptions pour modifier les entêtes relatives aux notifications.

Exemple de parseur

Le projet C# est sur GitHub: http://github.com/eric-b/DSN-Parser (il s’agit d’un seul fichier).
Cette implémentation est utilisée dans tous mes projets susceptibles d’envoyer des e-mails. Je précise malgré tout que je ne l’utilise que pour remonter d’éventuelles erreurs et non pour traiter des notifications de suivi (de bonne délivrance).

Son fonctionnement est simple:

  • Le message doit commencer par l’entête Return-path: <> (spécifié par la RFC 5321). C’est un premier filtre simple et rapide.
  • L’entête Content-Type doit contenir report-type=delivery-status.
  • Le rapport est interprété (RFC 3464).

Exemple

Pour plus de lisibilité, le message de test est à la fin de l’article.


void test()
{
            if (MailDeliveryInfo.IsDsn(PARTIAL_MESSAGE)) // PARTIAL_MESSAGE: headers only
            {
                var report = MailDeliveryInfo.TryCreate(RAW_MESSAGE); // RAW_MESSAGE: full message
                if (report != null)
                {
                    Console.WriteLine("{0}\r\n{2}\r\nRaw report:\r\n{1}",
                        report.Date,
                        report.RawReport,
                        string.Join(Environment.NewLine,
                            report.Status
                            .Select(t =>
                                string.Format("{0}: {1} ({2})",
                                t.Key,
                                t.Value.GetMostSignificantClassificationString(),
                                t.Value.MostSignificantStatusCode))
                            .ToArray()));
                }
                else
                {
                    Console.WriteLine("Failed to parse this message.");
                }
            }
            else
            {
                Console.WriteLine("Not a DSN.");
            }
}

/*
04/05/2012 15:25:09
test-dsn-failure@gmail.com: PermanentFailure/AddressingStatus/BadDestinationMailboxAddress (5.1.1)
Raw report:
Content-Description: Delivery report
Content-Type: message/delivery-status

Reporting-MTA: dns; xxx
Arrival-Date: Fri, 04 May 2012 15:25:09 +0200

Final-Recipient: rfc822; test-dsn-failure@gmail.com
Status: 5.1.1
Action: failed
Last-Attempt-Date: Fri, 04 May 2012 15:25:09 +0200
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 http://support.google.com/mail/bin/answer.py?answer=6596 t12si10077186weq.36
*/

Message original


Return-path: <>
Received: from xxx ([xxx])
    by xxx with ESMTP; Fri, 04 May 2012 16:18:13 +0200
From: <mailer-daemon@xxx> (Mail Delivery System)
To: xxx
Subject: Undelivered Mail Returned to Sender
Date: Fri, 04 May 2012 15:25:09 +0200
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
 boundary="HTB3nt3RR7vw/QMPR4kDPbKg+XWjXIKdC/rfHQ=="

This is a MIME-encapsulated message.

--HTB3nt3RR7vw/QMPR4kDPbKg+XWjXIKdC/rfHQ==
Content-Description: Notification
Content-Type: text/plain

I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.

For further assistance, please send mail to <postmaster@xxx>

If you do so, please include this problem report. You can
delete your own text from the attached returned message.

<test-dsn-failure@gmail.com>: 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 http://support.google.com/mail/bin/answer.py?answer=6596 t12si10077186weq.36


--HTB3nt3RR7vw/QMPR4kDPbKg+XWjXIKdC/rfHQ==
Content-Description: Delivery report
Content-Type: message/delivery-status

Reporting-MTA: dns; xxx
Arrival-Date: Fri, 04 May 2012 15:25:09 +0200

Final-Recipient: rfc822; test-dsn-failure@gmail.com
Status: 5.1.1
Action: failed
Last-Attempt-Date: Fri, 04 May 2012 15:25:09 +0200
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 http://support.google.com/mail/bin/answer.py?answer=6596 t12si10077186weq.36

--HTB3nt3RR7vw/QMPR4kDPbKg+XWjXIKdC/rfHQ==
Content-Description: Undelivered Message
Content-Type: message/rfc822

[original message...]