Un bug amusant de l'outil GNU mailutils

— last updated

Outil mailutils et TP de réseau

J'ai un TP d'introduction aux réseaux qui porte sur les protocoles de la messagerie électronique SMTP et POP. Ce sont des protocoles assez simples et au format texte, ce qui permet d'observer facilement une conversation complète avec Follow TCP Stream dans wireshark, et aussi de "jouer" au client ou au serveur avec l'outil netcat.

Dans ce TP j'utilise aussi (pour tester que les étudiants arrivent bien à "jouer" un serveur SMTP avec netcat) l'outil mailutils send du paquet logiciel mailutils et ai remarqué un comportement bizarre. En cherchant à comprendre ce qui se passait, j'ai été amené à éclaircir ma compréhension de certains aspects de la spécification et explorer l'implémentation de l'outil mailutils pour y déceler une bogue amusante (et en pratique totalement inoffensive, sauf peut être pour un TP d'introduction aux réseaux…)

Mais commençons par le commencement !

Le protocole SMTP

Le format typique d'une conversation SMTP est le suivant (S: indique un message émis par le serveur C: par le client) :

S: 220 smtp.example.com Welcome
C: HELO client.example.com
S: 250 smtp.example.com
C: MAIL FROM:<lulu@example.com>
S: 250 OK
C: RCPT TO:<jp@example.com>
S: 250 OK
C: DATA
S: 354 go on please!
C: From: lulu@example.com
C: To: jp@example.com
C: Subject: yay!
C:
C: What's up doc?
C: .
S: 250 OK
C: QUIT
S: 221 Bye

On voit sur cet exemple qu'une fois le client connecté (ce qui n'est pas montré ici car cela se passe à la couche transport), le serveur commence par le saluer avec un message 220 suivi de son nom de domaine et d'un texte optionnel, puis le client envoie des requêtes auxquelles le serveur répond. HELO est la salutation du client (le client y indique son nom de domaine), MAIL indique l'expéditeur d'un message, RCPT son (un) destinataire, DATA la demande de transmettre le mail proprement dit. Après la réponse 354 le client envoie le contenu du mail, terminé par une ligne ne contenant qu'un point.1 Enfin QUIT indique que le client a terminé.

Comme on le voit, les requêtes sont des mots de quatre lettres suivis d'un espace et d'un texte, les réponses des nombres sur trois chiffres (le premier indiquant la catégorie de la réponse, 2 = tout va bien, 3 = j'attends la suite…) suivi d'un espace et de texte.

On n'en voit pas ici, mais il est aussi possible d'avoir des réponses multi-lignes. Dans ce cas, le nombre est suivi d'un tiret au lieu d'un espace, indiquant que la réponse se poursuit sur la ligne suivante. Exemple extrait du RFC821 :

                               123-First line
                               123-Second line
                               123-234 text beginning with numbers
                               123 The last line

Le mécanisme d'extension SMTP

L'exemple ci-dessus correspond à la version originale du protocole, telle que définie dans le rfc821. Le rfc1869 a défini un "modèle d'extension", permettant au serveur d'informer le client des extensions au protocole qu'il sait gérer.2

Pour cela le client envoie une requête EHLO (extended helo) au lieu de HELO et le serveur y répond avec une réponse multi-ligne, la première indiquant (comme pour HELO) son nom de domaine, les suivantes chacune un mot-clé identifiant une extension supportée.

rfc5321-3.2+4

In addition to opening the session, use of EHLO indicates that the client is able to process service extensions and requests that server provide a list of the extensions it supports.

rfc5321-4.1.1.1+50

Normally, the response to EHLO will be a multiline reply. Each line of the response contains a keyword and, optionally, one or more parameters. Following the normal syntax for multiline replies, these keywords follow the code (250) and a hyphen for all but the last line, and the code and a space for the last line.

Voici un exemple tiré du rfc2821, où le serveur répond au EHLO en indiquant qu'il gère les extensions 8BITMIME, SIZE, DSN et HELP :

S: 220 foo.com Simple Mail Transfer Service Ready
C: EHLO bar.com
S: 250-foo.com greets bar.com
S: 250-8BITMIME
S: 250-SIZE
S: 250-DSN
S: 250 HELP

Si le serveur est une ancienne version ne connaissant pas le mécanisme d'extension, il ne comprendra pas le EHLO, le client peut alors reprendre par un HELO.

rfc5321-2.2.1+10

Contemporary SMTP implementations MUST support the basic extension mechanisms. For instance, servers MUST support the EHLO command even if they do not implement any specific extensions and clients SHOULD preferentially utilize EHLO rather than HELO. (However, for compatibility with older conforming implementations, SMTP clients and servers MUST support the original HELO mechanisms as a fallback.)

Ce qui donnerait

S: 220 smtp.example.com running antique software
C: EHLO client.example.com
S: 502 Don't know that
C: HELO client.example.com
S: 250 smtp.example.com

L'extrait précédent indique qu'un serveur moderne, même s'il ne supporte aucune extension, doit comprendre la requête EHLO. Il me semble clair que dans ce cas de figure, il doit fournir une réponse sur une seule ligne (c'est à dire qu'il fournit une liste de 0 extension), ce qui donnerait :

S: 220 smtp.example.com I'm modern but don't support any extension
C: EHLO client.example.com
S: 250 smtp.example.com

À noter la formulation suivante :

rfc5321-4.1.1.1+32

If the SMTP server, in violation of this specification, does not support any SMTP service extensions, it will generate an error response.

Cela me semble être une erreur, car contradictoire avec ce qui précède. Je pense que ce que l'auteur voulait dire, c'est que si le serveur SMTP ne connait pas le mécanisme d'extension (ce qui est rendu obligatoire par la spécification actuelle), il répondra une erreur, puisqu'il ne comprendra pas le message EHLO.

L'outil mailutils du paquet GNU mailutils

GNU mailutils est un ensemble de bibliothèques et d'utilitaires pour traiter le courrier électronique. Il contient entre autre un outil multi usage nommé… mailutils.

Cet outil permet de manipuler facilement les protocoles de la messagerie dans un shell. Par exemple mailutils send prend en arguments l'adresse IP d'un serveur SMTP et un fichier (censé contenir un mail), contacte le serveur SMTP et met en oeuvre l'envoi du mail à ce serveur.

mailutils send

Simulons un serveur SMTP : on lance netcat dans un terminal pour attendre une connexion sur le port TCP 25 :

# netcat -l -p 25 -v
listening on [any] 25 ...

Et dans un autre terminal on utilise mailutils send pour envoyer un mail à notre "serveur" :

$ mailutils send 127.0.0.1 mail.msg

Voici l'échange vu sur le premier terminal (la première ligne est affichée par netcat pour indiquer qu'un client vient de se connecter, les lignes précédées par * sont tapées dans le terminal et donc envoyées au client, les autres sont ce que nous recevons du client) :

   connect to [127.0.0.1] from localhost [127.0.0.1] 56584
 * 220 hello
   EHLO LC1758
 * 250 ok
   EHLO LC1758
 * 250 ?
   EHLO LC1758
 * 250 excellence ?
   EHLO LC1758
 * 250 innovation !
   MAIL FROM:<lulu@example.com>
 * 250 ok
   RCPT TO:<jp@example.com>
 * 250 ok
   DATA
 * 354 vas y toto
   From: lulu@example.com
   To: jp@example.com
   Subject: yay!
   
   What's up doc?
   .
 * 250
   QUIT
 * 221

On répond un 250 ok à la requête EHLO, ce qui semble ne pas satisfaire le client, puisqu'il répète la requête. On choisit de persister avec la même réponse, et cela finit par fonctionner puisque la réponse est acceptée la quatrième fois, permettant à la suite de se dérouler normalement. Ceci est quand même très bizarre et, surtout, embêtant pour un TP où les étudiants doivent simuler avec netcat un serveur SMTP crédible pour mailutils send.

mailutils smtp

Pour essayer de comprendre ce qui se passe, nous allons utiliser la commande mailutils smtp, qui fournit un shell client smtp permettant de déclencher manuellement les différentes opérations. On l'utilise ici pour se connecter à notre "serveur smtp" afin d'examiner plus précisément les choses. Ci dessous le client est à gauche, le serveur à droite, les lignes précédées d'un * sont tapées le reste n'est qu'affiché. On voit que la réponse 250 smtp.example.com est bien traitée comme une erreur.

* $ mailutils smtp                    * $ netcat -l -v -p 2220
* smtp> connect 127.0.0.1 2222          Connection received ...
  220 smtp.example.com                * 220 smtp.example.com
* smtp> ehlo client.example.com         EHLO client.example.com
  Error: Invalid argument             * 250 smtp.example.com
  smtp> 

On imagine donc que c'est le cas aussi pour mailutils send, et qu'il décide de réessayer, mais on ne voit pas du tout pourquoi cette erreur, à la quatrième occurence, est finalement considérée comme non problématique…

Le code source

Je vous préviens, le code source de mailutils est un peu touffu, respirons un grand coup et soyons attentif…

Le shell

Le main et les commandes du shell smtp sont dans le fichier mu/libexec/smtp.c. Le main appelle mutool_shell dans mu/libexec/shell.c dont voici un extrait :

while (!done)
  {
    char *s, *line = input_line ();
    [...]  
    /* Remove leading and trailing whitespace from the line.
       Then, if there is anything left, add it to the history list
       and execute it. */
    s = mu_str_stripws (line);

    if (*s)
      {
        [...]
        status = execute_line (s);
        if (status != 0)
          mu_error ("Error: %s", mu_strerror (status));
      }
    free (line);
  }

On a donc une boucle où on lit une ligne, l'exécute, le status retourné pouvant indiquer la présence d'une erreur. execute_line parse la ligne puis appelle la fonction C correspondant à la commande.

État courant

Ces fonctions sont définies dans smtp.c et utilisent une variable globale smtp stockant l'état courant de la conversation :

static mu_smtp_t smtp;

avec (include/mailutils/smtp.h) :

typedef struct _mu_smtp *mu_smtp_t;

[...]

struct _mu_smtp
{
  int flags;
  [...]
  mu_list_t capa;
  [...]
  /* I/O buffers */
  char replcode[4];
  char *replptr;
  
  char *rdbuf;
  [...]
  mu_list_t mlrepl;
  [...]
};

On y trouve tout un tas d'informations dont la dernière réponse reçue (rdbuf), son code de réponse (replcode, code à 3 chiffres plus l'octet zéro de fin de chaîne) et deux listes chaînées mlrepl (la dernière réponse multi-ligne reçue) et capa (pour capabilities, les extensions annoncées par le serveur).

Listes chaînées

Le type mu_list_t est une liste doublement chaînée :

include/mailutils/types.h

typedef struct _mu_list *mu_list_t;

include/mailutils/sys/list.h

struct list_data
{
  void *item;
  struct list_data *next;
  struct list_data *prev;
};

struct _mu_list
{
  struct list_data head;
  [...]
};

Je cache presque tout pour ne pas nous noyer dans les détails (!) mais l'important pour la suite est qu'une liste est un pointeur vers une structure struct _mu_list.

connect

La première commande sera connect, exécutée par la fonction com_connect qui établit la connexion TCP et initialise la variable globale smtp

 status = mu_smtp_create (&smtp);

libproto/mailer/smtp_create.c

 smtp = calloc (1, sizeof (*smtp));

On sait que calloc initialise le contenu du bloc de mémoire alloué avec des zéros, ce qui veut dire que mlrepl et capa ne sont alors pas des listes vides mais des pointeurs nuls.

ehlo

La commande ehlo sera exécutée par com_ehlo (dans mu/libexec/smtp.c) :

static int
com_ehlo (int argc, char **argv)
{
  if (argc == 1)
    {
      if (mu_smtp_test_param (smtp, MU_SMTP_PARAM_DOMAIN))
        {
          mu_error (_("no domain set"));
          return 0;
        }
    }
  else
    mu_smtp_set_param (smtp, MU_SMTP_PARAM_DOMAIN, argv[1]);
  return com_capa (1, argv);
}

On voit qu'elle ne fait pas grand chose, essentiellement elle appelle com_capa qui a pour but d'afficher la liste des capabilities du serveur (c'est à dire le contenu de capa).

capa

Si capa n'est pas encore initialisée elle commencera par récupérer cette liste, en envoyant un message EHLO.3

static int
com_capa (int argc, char **argv)
{
  mu_iterator_t iterator = NULL;
  int status = 0;

  [...]

      status = mu_smtp_capa_iterator (smtp, &iterator);

      if (status == 0)
        {
          [...]
          mu_iterator_destroy (&iterator);
        }
    }
  return status;
}

Donc, mu_smtp_capa_iterator est appelée et échoue (retourne un status non nul). Elle est définie dans libproto/mailer/smtp_capa_itr.c :

int
mu_smtp_capa_iterator (mu_smtp_t smtp, mu_iterator_t *itr)
{
  if (!smtp || !itr)
    return EINVAL;
  if (MU_SMTP_FISSET (smtp, _MU_SMTP_ERR))
    return MU_ERR_FAILURE;
  if (!smtp->capa)
    {
      int rc = mu_smtp_ehlo (smtp);
      if (rc)
        return rc;
    }
  if (!MU_SMTP_FISSET (smtp, _MU_SMTP_ESMTP))
    return MU_ERR_FAILURE;
  return mu_list_get_iterator (smtp->capa, itr);
}

ehlo pour récupérer les capabilities

Comme on l'a vu, smtp->capa est une liste chaînée (mu_list_t) contenant la liste des capabilities annoncées par le serveur. Si c'est un pointeur nul, la liste n'a pas encore été collectée, on essaie de le faire en appelant mu_smtp_ehlo (dans libproto/mailer/smtp_ehlo.c), dont voici un extrait :

int
mu_smtp_ehlo (mu_smtp_t smtp)
{
  int status;

  [...]
  
  status = mu_smtp_write (smtp, "EHLO %s\r\n",
                          smtp->param[MU_SMTP_PARAM_DOMAIN]);
  MU_SMTP_CHECK_ERROR (smtp, status);
  status = mu_smtp_response (smtp);
  MU_SMTP_CHECK_ERROR (smtp, status);
  if (smtp->replcode[0] == '2')
    {
      smtp->flags |= _MU_SMTP_ESMTP;
      smtp->capa = smtp->mlrepl;
      smtp->mlrepl = NULL;
      mu_list_set_comparator (smtp->capa, capa_comp);
    }
  else if (smtp->replcode[0] == '4')
    return MU_ERR_REPLY;
  else
    {
      status = mu_smtp_write (smtp, "HELO %s\r\n",
                              smtp->param[MU_SMTP_PARAM_DOMAIN]);
      MU_SMTP_CHECK_ERROR (smtp, status);
      status = mu_smtp_response (smtp);
      MU_SMTP_CHECK_ERROR (smtp, status);
      smtp->flags &= ~_MU_SMTP_ESMTP;
      if (smtp->replcode[0] != '2')
        return MU_ERR_REPLY;
    }
  if (smtp->state == MU_SMTP_EHLO)
    smtp->state = MU_SMTP_MAIL;
  return 0;

On voit qu'on émet la requête EHLO et récupère la réponse (fonction mu_smtp_response). Si le code de réponse est 2xx, on fixe smtp->capa à smtp_mlrepl, s'il est 4xx on retourne une erreur, sinon en essaie un HELO, conformément à la spécification (rfc5321-2.2.1+10).

Dans notre exemple, nous répondions 250, on devrait donc initialiser smtp->capa (c'est parce qu'il était nul qu'on a appelé mu_smtp_ehlo).

Analyse d'une réponse

Voyons la fonction mu_smtp_response (fichier libproto/mailer/smtp_io.c). La fonction commence par lire une ligne et vérifie qu'elle est bien formée :

int
mu_smtp_response (mu_smtp_t smtp)
{
  int rc;
  size_t n;
  
  rc = mu_stream_getline (smtp->carrier, &smtp->rdbuf, &smtp->rdsize, &n);
  MU_SMTP_CHECK_ERROR (smtp, rc);
  if (n == 0)
    MU_SMTP_CHECK_ERROR (smtp, EIO);
  n = mu_rtrim_class (smtp->rdbuf, MU_CTYPE_ENDLN);
  if (n < 3 || !mu_isdigit (smtp->rdbuf[0]))
    {
      mu_diag_output (MU_DIAG_NOTICE,
                      "received invalid reply from SMTP server");
      MU_SMTP_CHECK_ERROR (smtp, MU_ERR_BADREPLY);
    }

Elle copie le code numérique dans smtp->replcode et teste si la réponse est multi-ligne (smtp->rdbuf[3] == '-'). Si c'est le cas, le drapeau _MU_SMTP_MLREPL est levé :

  memcpy (smtp->replcode, smtp->rdbuf, 3);
  smtp->replcode[3] = 0;
  if (smtp->rdbuf[3] == '-')
    {
      smtp->flags |= _MU_SMTP_MLREPL;
      [...]

puis une boucle permet d'accumuler le texte de toutes les lignes suivantes dans la liste chaînée smtp->mlrepl :

      rc = _mu_smtp_init_mlist (smtp);                           (ref:init)
      MU_SMTP_CHECK_ERROR (smtp, rc);
      do
        {
          char *p;

          rc = mu_stream_getline (smtp->carrier, &smtp->rdbuf, &smtp->rdsize,
                                  &n);
          MU_SMTP_CHECK_ERROR (smtp, rc);
          if (n == 0)
            MU_SMTP_CHECK_ERROR (smtp, EIO);
          n = mu_rtrim_class (smtp->rdbuf, MU_CTYPE_ENDLN);
          if (n < 3 || memcmp (smtp->rdbuf, smtp->replcode, 3))
            {
              mu_diag_output (MU_DIAG_NOTICE,
                              "received invalid reply from SMTP server");
              MU_SMTP_CHECK_ERROR (smtp, MU_ERR_BADREPLY);
            }
          p = strdup (smtp->rdbuf + 4);
          if (!p)
            MU_SMTP_CHECK_ERROR (smtp, ENOMEM);
          mu_list_append (smtp->mlrepl, p);                      (ref:append)
        }
      while (smtp->rdbuf[3] == '-');
    }

Ici _mu_smtp_init_mlist(smtp) (libproto/mailer/smtp.io.c) fixe smtp->mlrepl à la liste vide, et mu_list_append (libmailutils/list/append.c) y ajoute un élément.

Au final on récupère donc bien la liste des capabilities dans smtp->mlrepl, qui sera ensuite affectée à smtp->capa par mu_smtp_ehlo. Mais le cas qui nous occupe est celui où la réponse n'est pas multi-ligne, et dans ce cas le drapeau _MU_SMTP_MLREPL est baissé et smtp->mlrepl n'est pas modifiée :

  else
    {
      smtp->flags &= ~_MU_SMTP_MLREPL;
      smtp->replptr = smtp->rdbuf + 4;
    }
  return 0;
}

Et donc

C'est donc un pointeur nul que mu_smtp_ehlo copie dans smtp->capa, et c'est pour cela que mu_smtp_capa_iterator retourne une valeur non nulle, le résultat de mu_list_get_iterator appliqué à un pointeur nul !

Résumons. Quand on tape la commande ehlo (je note -> !0 pour "retourne une valeur différente de zéro")

Correction

Le problème est donc dans la fonction mu_smtp_response : si la réponse est multi-ligne, le contenu est placé sous forme d'une liste dans smtp->mlrepl, mais si ce n'est pas le cas, smtp->mlrepl reste indéterminé, alors qu'il devrait devenir la liste vide !

On peut corriger facilement ceci en plaçant l'initialisation à la liste vide avant le if pour qu'elle soit effectuée que la réponse soit multi-ligne ou pas :

   smtp->replcode[3] = 0;
 
   rc = _mu_smtp_init_mlist (smtp);     (====)
   MU_SMTP_CHECK_ERROR (smtp, rc);
 
   if (smtp->rdbuf[3] == '-')

Après ce déplacement de deux lignes et compilation, une réponse simple-ligne est acceptée pour le EHLO :

* $ mailutils smtp                    * $ netcat -l -v -p 2220
* smtp> connect 127.0.0.1 2222          Connection received ...
  220 smtp.example.com                * 220 smtp.example.com
* smtp> ehlo client.example.com         EHLO client.example.com
  smtp>                               * 250 smtp.example.com

Et l'envoi d'un message par mailutils send se fait donc maintenant sans EHLO répétés :

$ mailutils send 127.0.0.1 mail.msg    * # netcat -v -l -p 25
                                         listening on [any] 25 ...
                                         connect to [127.0.0.1] from localhost [127.0.0.1] 52090
                                       * 220 salut
                                         EHLO LC1758
                                       * 250 ok
                                         MAIL FROM:<jean.pierre@sender>
                                       * 250 ok
                                         RCPT TO:<lulu@receiver>
                                       * 250 ok
                                         DATA
                                       * 354 go
                                         From: jean.pierre@sender
                                         To: lulu@receiver
                                         Subject: that one

                                         yolo
                                         .
                                       * 250 ok                                         
                                         QUIT
                                       * 221 ok

Ok, mais pourquoi quatre fois ?

On a corrigé le problème, mais tout n'est pas encore vraiment éclairci : dans le cas de mailutils send, pourquoi la requête EHLO est-elle répétée et la réponse acceptée la quatrième fois qu'elle est fournie !?

Cet outil n'utilise pas les fonctions com_ehlo etc, qui sont les commandes du shell smtp. Il est défini dans mu/libexec/send.c. Au cours de la conversation, le client a besoin de tester le support d'une extension par le serveur. Pour cela il peut consulter sa liste smtp->capa. Ceci est réalisé par la fonction mu_smtp_capa_test :

libproto/mailer/smtp_capa.c

int
mu_smtp_capa_test (mu_smtp_t smtp, const char *name, const char **pret)
{
  if (!smtp || !name)
    return EINVAL;
  if (MU_SMTP_FISSET (smtp, _MU_SMTP_ERR))
    return MU_ERR_FAILURE;
  if (!smtp->capa)
    {
      int rc = mu_smtp_ehlo (smtp);
      if (rc)
        return rc;
    }
  if (!MU_SMTP_FISSET (smtp, _MU_SMTP_ESMTP))
    return MU_ERR_FAILURE;
  return mu_list_locate (smtp->capa, (void*) name, (void**)pret);
}

Comme on le voit, si smtp->capa est nul, la fonction mu_smtp_ehlo est appelée pour l'initialiser. La fonction mu_list_locate teste si l'élément name est présent dans la liste smtp->capa, si c'est le cas elle retourne 0 et place un pointeur sur l'élément dans *pret.4 Mais on a vu qu'avec une réponse simple-ligne, smtp->capa n'était pas modifié, un argument nul sera passé à la fonction mu_list_locate qui retournera une valeur d'erreur (non nulle). Des appels répétés à mu_smtp_capa_test pour tester différentes extensions vont engendrer des appels répétés à mu_smtp_ehlo et donc l'envoi à chaque fois d'une requête EHLO.

Ci dessous la séquence des appels de fonctions au cours de l'exécution complète :

  • main (mu/libexec/send.c)

    • mu_mailer_create_from_url
    • mu_mailer_open = libproto/mailer/smtp.c : smtp_open

      • mailer_smtp_init_late
      • mu_smtp_open (libproto/mailer/smtp_open.c)

        • check smtp->state == MU_SMTP_INIT
        • mu_smtp_response – check 2xx
        • smtp->state = MU_SMTP_EHLO
      • mu_smtp_ehlo # le premier EHLO
      • mu_smtp_capa_test STARTTLS

        • mu_smtp_ehlo # déclenché par le test sur STARTTLS
      • mu_smtp_capa_test AUTH

        • mu_smtp_ehlo # déclenché par le test sur AUTH
    • mu_mailer_send_message

      • smtp_send_message (libproto/mailer/smtp.c)
      • mu_smtp_capa_test SIZE

        • mu_smtp_ehlo # le test sur SIZE
    • mu_mailer_close
    • mu_mailer_destroy

Après l'ouverture de la session, le EHLO initial est envoyé. Le client souhaite utiliser l'extension STARTTLS5, il teste donc sa présence dans sa liste capa, ce qui déclenche un appel à mu_smtp_ehlo. Il teste ensuite l'extension AUTH pour l'authentification (rfc4954), et enfin l'extension SIZE permettant d'annoncer la taille d'un message avant son envoi (rfc1870). Évidemment aucune de ces extensions n'est utilisée.

Et le HELO ?

Supposons maintenant que notre serveur est ancien et ne comprend pas les requêtes EHLO, il répondra donc par une erreur (par exemple 502 Command not implemented), et le client est censé retenter avec HELO (comme mentionné plus haut, code de mu_smtp_ehlo).

Voici ce qui se passe avec un serveur qui répond 502 à EHLO et 250 OK (simple ligne évidemment) à HELO :

root@LC1758:~# netcat -l -v -p 25
listening on [any] 25 ...
connect to [127.0.0.1] from localhost [127.0.0.1] 58162
220
EHLO LC1758
502
HELO LC1758
250
EHLO LC1758
502
HELO LC1758
250
EHLO LC1758
502
HELO LC1758
250
EHLO LC1758
502
HELO LC1758
250
MAIL FROM:<lulu@example.com>

Le problème est toujours le même. Après une réponse 5xx au EHLO le client essaie avec HELO mais smtp->capa reste égal à NULL ce qui entraîne une tentative de récupérer la liste des capabilities (et donc un appel à mu_smtp_ehlo pour chaque capa que le client aimerait bien utiliser, voir le code en #get-capa).

Puisque notre modification précédente met smtp->mlrepl à la liste vide dans tous les cas, on peut l'utiliser pour fixer la valeur de smtp->capa :

libproto/mailer/smtp_ehlo.c/mu_smtp_ehlo

      status = mu_smtp_write (smtp, "HELO %s\r\n",
                              smtp->param[MU_SMTP_PARAM_DOMAIN]);
      MU_SMTP_CHECK_ERROR (smtp, status);
      status = mu_smtp_response (smtp);
      MU_SMTP_CHECK_ERROR (smtp, status);
      smtp->flags &= ~_MU_SMTP_ESMTP;
      smtp->capa = smtp->mlrepl;  /* this must be the empty list */ (ligne ajoutée) (====)
      if (smtp->replcode[0] != '2')
	return MU_ERR_REPLY;
    }

Et bien voilà, tout fonctionne maintenant normalement !

Note : le bug a depuis été corrigé, mais pas avec le patch montré ici, voir https://savannah.gnu.org/patch/?10439.


1

Une chose qui peut étonner est que l'on indique les émetteur et destinataire à la fois par des messages SMTP (dans MAIL et RCPT) et dans le mail lui même. Les adresses transmises à SMTP sont celles "de l'enveloppe" (c'est à dire utilisées par SMTP en tant que service d'acheminement du mail) alors que celles présentes dans le mail font partie du mail. Vu de SMTP le mail est juste un fichier et il ne se préoccupe pas de son contenu. Les From et To dans le mail sont un peu comme les lignes en tête d'une lettre, et on ne s'attend pas à ce que le facteur les utilise !

2

Les rfc821 et rfc1869 sont obsolètes, ils ont été remplacés par le rfc2821, lui même remplacé par le rfc5321 qui est la spécification actuelle.

3

Comme on l'a vu, dans SMTP la requête EHLO établit la conversation avec le serveur et récupère la liste des capabilities. Le shell mailutils distingue les deux aspects qui sont intrinsèquement liés dans le protocole, mais d'autres protocoles séparent les deux aspects : POP3 a une requête CAPA (rfc2449), IMAP4 une requête CAPABILITY (rfc9051).

4

Le test de présence est fait avec une fonction de comparaison qui n'est pas nécessairement l'égalité, d'où l'intérêt de fournir un pointeur sur l'élément trouvé (conformément au nom "locate"…)

5

Définie dans le rfc3207, cette extension offre un mécanisme (supporté par beaucoup de protocoles) permettant de passer la connexion en TLS avant de poursuivre les échanges. Ce n'est pas le mécanisme recommandé de nos jours, on préfère le "TLS implicite" où on utilise un port spécifique et la session TLS est établie directement avant tout échange à la couche application (rfc8314).