Message-Id invalide selon RFC 2822 + encoding

Bonjour,

Je remonte deux problèmes relatifs aux emails envoyés.

Message-Id Invalide

Suite à une analyse avec https://www.mail-tester.com/,
Il semblerait que le ‹ Message-Id â€º ne soit pas valide.

L’erreur mail-tester est :
INVALID_MSGID Message-Id is not valid, according to RFC 2822

Voici l’en-tête « Message-Id Â» selon mail-tester :

Message-Id: =?utf-8?q?=3C165306032961=2E3393=2E9803830538757387937=2EKyW?=
 =?utf-8?q?aOn82WZ=40www=2Eatelierdusoleiletduvent=2Eorg=3E?=

Peut-être un problème d’encodage ou de définition d’encodage ?

En effet, dans le « Journal des Emails Â» sous Dokos, le Message-Id est :
Message-Id: <165306032961.3393.9803830538757387937.KyWaOn82WZ@www.atelierdusoleiletduvent.org>

Avez-vous le même problème ?
Faut-il définir le type d’encodage des en-têtes du mail ?

Encodage du champ ‹ sujet â€º

Aussi, j’ai une erreur sur l’encodage du champ sujet qui était « Bon de réservation Â»
Détails mail-tester :

-0.1 SUBJECT_NEEDS_ENCODING Subject is encoded but does not specify the encoding
-1.105 SUBJ_ILLEGAL_CHARS Subject: has too many raw illegal characters

1 « J'aime »

Bonjour,

Merci d’avoir fait remonter ce problème.

Il semblerait que la politique d’encodage des e-mails utilisée par Dokos, SMTPUTF8, ne convertit pas les charactères non-ASCII à l’intérieur des headers. Voilà l’explication pour l’erreur de Subject. Néanmoins, je n’ai pas réussi à reproduire le problème du Message-Id, qui n’est pas censé être encodé en Quoted-Printable peu importe son contenu, ni même découpé en plusieurs lignes (bien que cela soit autorisé).1

Apparemment, d’après le test que vous avez effectué, c’est SpamAssassin qui donne un mauvais score au mail pour deux raisons : le Message-Id ne devrait pas être encodé (même en Quoted-Printable), et le header Subject ne devrait pas contenir de caractères non-ASCII.

Une correction qui vise à changer la politique d’encodage vers SMTP, donc qui permettra d’améliorer le score SpamAssassin des mails envoyés par Dokos, est disponible ici : dokos/dodock #14.


1. Il me semble que l’usage des caractères non-ASCII dans les headers est parfois autorisé, sinon il peut se faire par des encodages comme Base64 ou Quoted-Printable.[RFC 6531, RFC 6532, SpamAssassin, rspamd]

2 « J'aime »

Bonjour Corentin,

Au top, merci pour ces détails précis !
Le lien vers la demande de fusion ne fonctionne pas / plus mais j’ai retrouvé le commit via le graph du repo.

Je vois deux solutions :

  • (a) j’essaie d’appliquer les changements avec un patch sur mon instance dokos
  • (b) j’attend qu’une demande de fusion soit intégrée à la branche master de dodock

@chdecultot un avis ou un timing pour intégrer le commit de @corentin ?

Belle fin de journée,

Bonjour @guillaume.augais,

Je n’avais pas vu que les MR n’étaient pas accessibles à tous sur Gitlab, c’est corrigé.

J’ai fait plusieurs tests avec des passerelles SMTP différentes et je n’ai pas réussi à reproduire le problème d’encodage pour le Message-Id.
Du coup, serait-il possible de connaître la passerelle SMTP que vous utilisez ?

Je voudrais m’assurer que le problème vient bien de l’encodage de Dokos et pas de la passerelle sous-jacente.

Part contre j’ai bien la même erreur pour le sujet, donc il faut qu’on corrige.

J’essaye de m’en occuper cette semaine.

Bien reçu pour la visibilité des MR, merci.

La passerelle sous-jacente est Gandi et le DKIM est activé sur la boite mail d’envoi qui est bonjour@atelierdusoleiletduvent.org.

Voici la configuration du domaine sous Dokos :

  • IMAP en SSL sur le port 993 (pas pertinent à priori)
  • SMTP en SSL sans TLS sur le port 465

Si cela peut aider, voici le lien vers l’analyse mail-tester : Spam Test Result
L’email a été envoyé depuis le bouton « Nouvel Email Â» du pied de page d’une commande client Dokos.

Suite à vos remarques je constate que dans le « Journal des emails Â», le Message-Id semble formatté correctement… C’est à dire avec les caractères ‹ < â€º et ‹ > â€º au lieu de ‹ =?utf-8?q?=3C â€º et ‹ =3E?= â€º

=> Voir cet export du contenu source, extrait du champ « Message Â» après sélection de l’email en question dans le « Journal des emails Â» : Re: Guillaume AUGAIS (#CMD-VT-2022-00087)

Je viens de faire plusieurs tests :

  • client Thunderbird / compte Gandi sans DKIM : pas de pb de Message-Id (mail-tester donne 9/10 car pas de DKIM)
  • client Thunderbird / compte Gandi avec DKIM : pas de pb de Message-Id (mail-tester donne 10/10)

Ce qui metterait hors de cause Gandi, ou l’activation du DKIM…
J’ai aussi vérifié en modifiant la config SMTP Dokos : passer de 465 (avec SSL) à 587 (avec TLS) n’a rien changé à l’allure du ‹ Message-Id â€º reçu par mail-tester

Comparaison des en-têtes Dokos / Thunderbird

Avec un envoi de mail Dokos,
le formattage des en-têtes est le suivant :

Content-Type: multipart/mixed; boundary=« ===============8978091418656974850== Â»
MIME-Version: 1.0
Message-Id: 165402808017.3389.2808957330285164207.v2NW1ExiIU@www.atelierdusoleiletduvent.org
X-Original-From: Guillaume AUGAIS bonjour@atelierdusoleiletduvent.org
Subject: Re: Guillaume AUGAIS (#CMD-VT-2022-00087)
From: Atelier du Soleil et du Vent bonjour@atelierdusoleiletduvent.org
To: test-6gci2kqob@srv1.mail-tester.com
Date: Tue, 31 May 2022 20:14:43 -0000

Alors que via Thunderbird, j’ai :

Content-Type: multipart/alternative;
boundary=« ------------WolpAFcc0rwpqED1z0ClrKkw Â»
Message-ID: 16fe9764-36c5-9a01-0b3d-c14c427cafba@mondomainetest.fr
Date: Tue, 31 May 2022 22:37:27 +0200
MIME-Version: 1.0

Plusieurs remarques :

  • (1) le champs boundary est sur une ligne nouvelle dans Thunderbird
  • (2) l’en-tête ‹ MIME-Version â€º est intercalée entre ‹ Content-Type â€º et ‹ Message-Id â€º après (mais cela ne devrait pas avoir d’incidence j’imagine)

Est-ce que (1) peut avoir une influence ?

En espérant que ces nouveaux éléments aident à mieux comprendre le problème.
Si vous voyez d’autres tests à réaliser, dites-moi.

Bonjour @guillaume.augais,

Après analyse approfondie du problème, nous allons publier ce soir un correctif pour changer la politique d’encodage des emails (merci @corentin). J’ai fait un certain nombre de tests sans constater de problème à la réception.
Ca devrait résoudre le problème d’encodage du sujet.

On ajoutera le header List-Unsubscribe dans la v3, parce qu’il y a quelques problèmes avec la longueur de l’URL qu’il faut régler d’abord.

Par contre je n’ai pas réussit à reproduire le problème du message-id, malgré tous mes tests. Mail-tester donne bien une note de 9/10 à tous les emails de test (le 1/10 étant lié à l’absence de DKIM).

Merci beaucoup pour votre aide !

Bonjour,

J’ai finalement pris le temps de mettre à jour l’instance (v2.18.2 de dokos).
Je confirme que je n’observe plus le souci d’encodage du sujet.

En revanche, j’observe la même chose avec l’erreur sur le Message-Id.

C’est déjà un bon pas, un grand merci à nouveau.
Je vais voir si cela suffit pour éviter la catégorisation en spam chez certains hébergeurs mails.

Belle semaine

1 « J'aime »

Bonsoir,

Ce message pourrait faire l’objet d’une nouvelle issue mais je soupçonne fortement une régression…

En effet, il semblerait que je ne peux plus utiliser frappe.sendmail() avec un sujet qui comporte un accent.

Depuis la console, ceci fonctionne

frappe.sendmail(
    recipients=['blabla@gmail.com'],
    subject="Message sans accent",
    message="Lien",
    as_markdown=False,
    delayed=False,
)

alors que ceci génère une erreur :

frappe.sendmail(
    recipients=['blabla@gmail.com'],
    subject="Message accentué",
    message="Lien",
    as_markdown=False,
    delayed=False,
)

Voici la trace :

In [7]: frappe.sendmail(
   ...:     recipients=['blabla@gmail.com'],
   ...:     subject="Message accentué",
   ...:     message="Lien",
   ...:     as_markdown=False,
   ...:     delayed=False,
   ...: )
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~/dokos/apps/frappe/frappe/commands/utils.py in <module>
      4     message="Lien",
      5     as_markdown=False,
----> 6     delayed=False,
      7 )

~/dokos/apps/frappe/frappe/__init__.py in sendmail(recipients, sender, subject, message, as_markdown, delayed, reference_doctype, reference_name, unsubscribe_method, unsubscribe_params, unsubscribe_message, add_unsubscribe_link, attachments, content, doctype, name, reply_to, queue_separately, cc, bcc, message_id, in_reply_to, send_after, expose_recipients, send_priority, communication, retry, now, read_receipt, is_notification, inline_images, template, args, header, print_letterhead, with_container)
    531                 send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
    532                 communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
--> 533                 inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
    534 
    535 whitelisted = []

~/dokos/apps/frappe/frappe/email/queue.py in send(recipients, sender, subject, message, text_content, reference_doctype, reference_name, unsubscribe_method, unsubscribe_params, unsubscribe_message, attachments, reply_to, cc, bcc, message_id, in_reply_to, send_after, expose_recipients, send_priority, communication, now, read_receipt, queue_separately, is_notification, add_unsubscribe_link, inline_images, header, print_letterhead, with_container)
    163                 header=header,
    164                 now=now,
--> 165                 print_letterhead=print_letterhead)
    166 
    167 

~/dokos/apps/frappe/frappe/email/queue.py in add(recipients, sender, subject, **kwargs)
    184                         frappe.db.commit()
    185         else:
--> 186                 email_queue = get_email_queue(recipients, sender, subject, **kwargs)
    187                 if kwargs.get('now'):
    188                         send_one(email_queue.name, now=True)

~/dokos/apps/frappe/frappe/email/queue.py in get_email_queue(recipients, sender, subject, **kwargs)
    228 
    229                 e.message_id = mail.msg_root["Message-Id"].strip(" <>")
--> 230                 e.message = cstr(mail.as_string())
    231                 e.sender = mail.sender
    232 

~/dokos/apps/frappe/frappe/email/email_body.py in as_string(self)
    246                 self.validate()
    247                 self.make()
--> 248                 return self.msg_root.as_string(policy=policy.SMTP)
    249 
    250 def get_formatted_html(subject, message, footer=None, print_html=None,

/usr/lib/python3.7/email/message.py in as_string(self, unixfrom, maxheaderlen, policy)
    156                       maxheaderlen=maxheaderlen,
    157                       policy=policy)
--> 158         g.flatten(self, unixfrom=unixfrom)
    159         return fp.getvalue()
    160 

/usr/lib/python3.7/email/generator.py in flatten(self, msg, unixfrom, linesep)
    114                     ufrom = 'From nobody ' + time.ctime(time.time())
    115                 self.write(ufrom + self._NL)
--> 116             self._write(msg)
    117         finally:
    118             self.policy = old_gen_policy

/usr/lib/python3.7/email/generator.py in _write(self, msg)
    193         meth = getattr(msg, '_write_headers', None)
    194         if meth is None:
--> 195             self._write_headers(msg)
    196         else:
    197             meth(self)

/usr/lib/python3.7/email/generator.py in _write_headers(self, msg)
    220     def _write_headers(self, msg):
    221         for h, v in msg.raw_items():
--> 222             self.write(self.policy.fold(h, v))
    223         # A blank line always separates headers from body
    224         self.write(self._NL)

/usr/lib/python3.7/email/policy.py in fold(self, name, value)
    181 
    182         """
--> 183         return self._fold(name, value, refold_binary=True)
    184 
    185     def fold_binary(self, name, value):

/usr/lib/python3.7/email/policy.py in _fold(self, name, value, refold_binary)
    203     def _fold(self, name, value, refold_binary=False):
    204         if hasattr(value, 'name'):
--> 205             return value.fold(policy=self)
    206         maxlen = self.max_line_length if self.max_line_length else float('inf')
    207         lines = value.splitlines()

/usr/lib/python3.7/email/headerregistry.py in fold(self, policy)
    256                 parser.CFWSList([parser.WhiteSpaceTerminal(' ', 'fws')]))
    257         header.append(self._parse_tree)
--> 258         return header.fold(policy=policy)
    259 
    260 

/usr/lib/python3.7/email/_header_value_parser.py in fold(self, policy)
    142 
    143     def fold(self, *, policy):
--> 144         return _refold_parse_tree(self, policy=policy)
    145 
    146     def pprint(self, indent=''):

/usr/lib/python3.7/email/_header_value_parser.py in _refold_parse_tree(parse_tree, policy)
   2650                 # combining it with previously encoded words if allowed.
   2651                 last_ew = _fold_as_ew(tstr, lines, maxlen, last_ew,
-> 2652                                       part.ew_combine_allowed, charset)
   2653             want_encoding = False
   2654             continue

/usr/lib/python3.7/email/_header_value_parser.py in _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset)
   2727             # XXX We'll get an infinite loop here if maxlen is <= 7
   2728             continue
-> 2729         first_part = to_encode[:text_space]
   2730         ew = _ew.encode(first_part, charset=encode_as)
   2731         excess = len(ew) - remaining_space

TypeError: slice indices must be integers or None or have an __index__ method

Dans le fichier apps/frappe/frappe/email/email_body.py, la ligne 248 est appelée.
Ligne modifiée dans ce commit : fix: Change encoding policy to correctly encode subject in header (67842c03) · Commits · atelierdusoleiletudvent / Dodock · GitLab

Et commit qui a été effectué suite aux échanges ci-dessus…

Pouvez-vous reproduire le bug ?

Bonjour @guillaume.augais,

Je viens de faire un test depuis la console sur un serveur de production et je n’ai pas eu d’erreur.
L’email est bien parti avec le sujet encodé:
image

A la réception:
image

Par contre je remarque que nos serveurs de production sont sur Python 3.8 alors que vous êtes sur 3.7, ce qui pourrait expliquer que je ne peux pas le reproduire.
D’après la trace d’erreur, la librairie Email de python n’est pas à jour, il y a eu une correction de bug justement sur ces lignes qui est disponible sur Python3.7 (bpo-33529: Fix infinite loop in email header encoding by wojcikk2903 · Pull Request #12020 · python/cpython · GitHub)
Pouvez-vous essayer de mettre à jour Python3.7 et voir si ça résoud le problème ?

Merci et bonne journée!

Bonjour,

J’ai mis à jour vers python 3.8.2 après réinstallation de bench et dokos sur mon serveur qui tourne sous Debian 10.

Et je confirme également : plus aucun souci avec le message ayant un sujet comportant un accent.
Et autre bonne nouvelle : le problème de formattage du message-id semble avoir disparu.

J’obtiens donc 10/10 avec mail-tester. Hourra

En revanche j’ai du contourner quelques problèmes ou installer manuellement certaines dépendences python : le script ansible a eu du mal sous Debian 10 + python 3.8.2.

Pour info, j’avais gardé le python 3.7 du système et j’ai rajouté une ‘alternative’ 3.8.2, que j’ai configurée comme par défaut avec update-alternatives.

Si j’ai un peu de temps et que l’occasion se présente j’essaierai de repartir à zéro sur un nouveau serveur avec debian 10 + python 3.8 et voir si j’ai les mêmes soucis ou non.

2 « J'aime »

Bonjour @guillaume.augais,

Merci pour ce retour !
Je pense qu’ajouter une alternative est une bonne option. La version 3 nécessitera python 3.10 a minima pour fonctionner, donc il faudra ajouter une alternative supplémentaire.
Il y a une commande bench migrate-env qui permet de migrer un bench vers une nouvelle version de python disponible sur le système. On va la documenter pour le passage à la version 3.

Pour info, les minima requis pour la version 3 seront:

  • Python 3.10
  • Nodejs 14 (nodejs 16 est possible aussi, mais pas encore compatible avec nodejs 18)
1 « J'aime »