GreenMail

GreenMail est un serveur de messagerie pour les tests. Il supporte SMTP, mais aussi POP3 et IMAP, avec ou sans TLS. On peut lui envoyer des e-mails mais il ne les transmet jamais aux destinataires. Il les garde en mémoire et les restitue en Java ou via une API Web.

On l’utilise surtout en test d’intégration, démarré directement depuis JUnit.

GreenMail est un remplaçant de FakeSMTP, qui n’est plus maintenu.

Le projet est ancien, ses premières versions datent de la fin des années 2000. Il est fait en Java et il est toujours maintenu.

Le choix de la version dépend de l’API Mail utilisée par l’application :

  • GreenMail 2.1 pour Jakarta Mail 2.1 (package jakarta.mail), avec un JDK 11 minimum,

  • GreenMail 1.6 pour l’ancienne API Java Mail (package javax.mail), par exemple avec Spring Boot 2.

JUnit 5

GreenMail fournit une extension JUnit 5.

<dependency>
    <groupId>com.icegreen</groupId>
    <artifactId>greenmail-junit5</artifactId>
    <version>2.1.14</version>
    <scope>test</scope>
</dependency>

L’extension démarre le serveur avant chaque test et l’arrête après. Chaque test commence donc avec une boîte aux lettres vide.

class MailTest {

  @RegisterExtension
  static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP);

  @Test
  void should_receive_mail() throws Exception {
    GreenMailUtil.sendTextEmailTest("alice@example.com", "noreply@example.com",
                                    "Bienvenue", "Bonjour Alice");

    MimeMessage[] messages = greenMail.getReceivedMessages();
    assertThat(messages).hasSize(1);
    assertThat(messages[0].getSubject()).isEqualTo("Bienvenue");
    assertThat(GreenMailUtil.getBody(messages[0])).isEqualTo("Bonjour Alice");
  }

}

ServerSetupTest définit les ports de test, décalés de 3000 par rapport aux ports standards, pour ne pas avoir besoin des droits root. Le port 3025 est utilisé pour SMTP. Les ports 3110 et 3143 sont utilisés pour POP3 et IMAP, avec ServerSetupTest.SMTP_POP3_IMAP.

Si on préfère démarrer le serveur une seule fois pour toute la classe, il faut le préciser avec withPerMethodLifecycle(false). Dans ce cas, c’est à nous de vider les boîtes aux lettres entre deux tests, avec greenMail.purgeEmailFromAllMailboxes().

Lecture des messages

Contrairement à FakeSMTP, il n’y a pas de fichier .eml à relire : GreenMail renvoie directement des MimeMessage de l’API Jakarta Mail. On peut donc les manipuler comme expliqué dans la page FakeSMTP.

La classe GreenMailUtil apporte quelques méthodes pratiques, comme getBody() pour le contenu ou getAddressList() pour les destinataires.

Envoi asynchrone

Si l’application envoie ses e-mails de façon asynchrone, le message n’est pas forcément arrivé au moment de la vérification. Il faut alors l’attendre, avec un délai maximal en millisecondes et le nombre de messages attendus.

    assertThat(greenMail.waitForIncomingEmail(5_000, 1)).isTrue();

Authentification SMTP

Par défaut, GreenMail accepte tous les messages, et crée à la volée une boîte aux lettres pour chaque destinataire. En revanche, si l’application s’authentifie, l’utilisateur doit exister.

On peut soit le déclarer dans la configuration :

  @RegisterExtension
  static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
          .withConfiguration(GreenMailConfiguration.aConfig()
                                                   .withUser("noreply@example.com", "noreply", "secret"));

Soit désactiver complètement le contrôle de l’authentification, avec GreenMailConfiguration.aConfig().withDisabledAuthentication().

Port dynamique

Le port fixe peut être gênant, par exemple si plusieurs builds tournent en parallèle sur la même machine. Avec dynamicPort(), GreenMail choisit un port libre, qu’on récupère ensuite.

  @RegisterExtension
  static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP.dynamicPort());

  @Test
  void should_receive_mail() {
    int smtpPort = greenMail.getSmtp().getPort();
    // ...
  }

Spring Boot

Dans un test Spring Boot, il suffit de faire pointer les propriétés spring.mail.* sur GreenMail. Le plus simple est de garder le port fixe.

@SpringBootTest(properties = {
        "spring.mail.host=localhost",
        "spring.mail.port=3025",
        "spring.mail.username=noreply",
        "spring.mail.password=secret"
})
class WelcomeServiceTest {

  @RegisterExtension
  static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
          .withConfiguration(GreenMailConfiguration.aConfig()
                                                   .withUser("noreply@example.com", "noreply", "secret"));

  @Autowired
  WelcomeService welcomeService;

  @Test
  void should_send_welcome_mail() throws Exception {
    welcomeService.welcome("alice@example.com");

    assertThat(greenMail.waitForIncomingEmail(5_000, 1)).isTrue();
    MimeMessage message = greenMail.getReceivedMessages()[0];
    assertThat(message.getSubject()).isEqualTo("Bienvenue");
  }

}

Docker

GreenMail peut aussi fonctionner en serveur autonome, par exemple en environnement de développement. L’image Docker officielle est greenmail/standalone.

docker run --publish 127.0.0.1:3025:3025 --publish 127.0.0.1:8080:8080 --detach greenmail/standalone:2.1.14

Le port 3025 est utilisé pour l’envoi de messages en SMTP. Le port 8080 est utilisé pour l’interface Web et l’API. L’image expose aussi les ports POP3 (3110), IMAP (3143) et leurs équivalents TLS (SMTPS: 3465, POP3S: 3995, IMAPS: 3993).

Par défaut, l’image démarre tous les protocoles, sans contrôle de l’authentification. La configuration se change avec la variable d’environnement GREENMAIL_OPTS.

  smtp:
    image: greenmail/standalone:2.1.14
    ports:
      - "127.0.0.1:3025:3025"
      - "127.0.0.1:8080:8080"
    environment:
      GREENMAIL_OPTS: >-
        -Dgreenmail.setup.test.smtp
        -Dgreenmail.hostname=0.0.0.0
        -Dgreenmail.auth.disabled

La propriété greenmail.hostname=0.0.0.0 est indispensable pour que le serveur soit accessible depuis l’extérieur du conteneur.

API

Le serveur autonome expose une API Web, documentée en OpenAPI sur http://localhost:8080. Les principales opérations sont les suivantes :

  • Lire la configuration : GET /api/configuration

  • Lister les utilisateurs : GET /api/user

  • Créer un utilisateur : POST /api/user

  • Lire les messages d’un utilisateur : GET /api/user/{email}/messages/INBOX

  • Supprimer tous les messages : POST /api/mail/purge

  • Redémarrer le serveur : POST /api/service/reset

  • Vérifier que le serveur est prêt : GET /api/service/readiness

curl -s http://localhost:8080/api/user/alice@example.com/messages/INBOX | jq .
curl -X POST http://localhost:8080/api/mail/purge

Pour des tests automatisés, l’extension JUnit reste beaucoup plus simple qu’un conteneur. Elle évite Docker, démarre en quelques millisecondes et donne un accès direct aux MimeMessage.