Ab die Post

Das JavaMail Framework ist ein sehr altes Schlachtross aus den Reihen der Java Standard APIs. Trotz des hohen Alters von über 20 Jahren kann man ohne große Umstände Emails damit versenden und empfangen.

Das folgende Beispiel verschickt eine Benachrichtigung über neue Stammbäume an interessierte Abonnenten.

public void sendTreeMail(File file, String... to) {
  Properties mailProperties = new Properties();
  mailProperties.put("mail.smtp.host", "localhost");
  mailProperties.put("mail.smtp.port", "3025");
  Session session = Session.getDefaultInstance(mailProperties);
  
  BodyPart text = new MimeBodyPart();
  text.setText("Es sind neue Stammbäume importiert worden");

  BodyPart attachment = new MimeBodyPart();
  DataSource source = new FileDataSource(file);
  attachment.setDataHandler(new DataHandler(source));
  attachment.setFileName(file.getName());

  Multipart multipart = new MimeMultipart();
  multipart.addBodyPart(text);
  multipart.addBodyPart(attachment);

  MimeMessage message = new MimeMessage(session);
  message.setFrom(new InternetAddress("admin@schegge.de"));
  for (String to : tos) {
    message.addRecipient(TO, new InternetAddress(to));
  }
  message.setSubject("Neue Stammbäume", "UTF-8");
  message.setText("Es sind neue Stammbäume importiert worden", "UTF-8");
  message.setContent(multipart);
  Transport.send(message);
}

Der E-Mail Versand erfolgt über einen SMTP Server. Um mit ihm in Kontakt treten zu können, werden daher zuerst die Verbindungsparameter in einer Properties Instanz gesetzt. Danach wird die E-Mail in Form einer MimeMessage mit allen benötigten Informationen versehen. Dies ist zuerst der Inhalt der Email, der aus einem Text und einem Attachment besteht. Beide werden als BodyPart erstellt und in einer Multipart Instanz zusammengefasst und dann als Inhalt in die MimeMessage eingefügt. Die Adressaten und der Absender in Form von InternetAddress Instanzen und die Betreffzeile vervollständigen die E-Mail. Am Ende wird die E-Mail mit dem Aufruf von Transport.send versendet.

Arbeiten die Software Entwickler mit dem Spring Framework, dann vereinfacht sich der Umgang mit der JavaMail API. Ein Großteil der Aufgaben werden durch den JavaMailSender und den MimeMessageHelper übernommen.

@Autowired
private JavaMailSender sender;

public void sendTreeMail(File file, String... to) {
  MimeMessage mimeMessage = sender.createMimeMessage();
  MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
  helper.setFrom("admin@schegge.de");
  helper.setTo(to);
  helper.setSubject("Neue Stammbäume");
  helper.setText("Es sind neue Stammbäume importiert worden");
  helper.addAttachment(file.getName(), file);
  sender.send(mimeMessage);
}

Der MimeMessageHelper verschlankt das Arbeiten mit der MimeMessage. Die Struktur einer Multipart-Message muss nicht mehr explizit zusammengefügt werden, sondern wird im Verborgenen verwaltet. Die Verwendung von InternetAddress Instanzen ist zwar noch möglich, aber nicht mehr nötig.

Der JavaMailSender kümmert sich um das Konfigurieren der Verbindungsparameter, dem Erstellen und dem Versand der MimeMessage. Die Verbindungsparameter für SMTP werden, wie in Spring Boot üblich, in der applications.properties angegeben.

spring.mail.host=localhost
spring.mail.port=3025

Mehr ist nicht notwendig um E-Mails, auf die eine oder andere Weise, aus eigenen Anwendungen heraus zu verschicken.

Aber kaum ist das Feature eingebaut, da soll es häufig auch schon wieder ausgeschaltet werden. Gerade in der Testphase ist es nicht wünschenswert, wenn die versendeten E-Mails ihr Ziel erreichen. Nichts ist ärgerlicher als einen Kunden zu erklären, warum er eine Mahnung erhalten hat, obwohl er seine Rechnungen alle beglichen hat.

Spring Boot Applikationen bieten über Dependency Injection und Profile die Möglichkeit, Mail Services im Testbetrieb durch alternative Implementierungen zu ersetzen. Dann werden die E-Mails nicht mehr versendet, sondern im Log protokolliert oder im Dateisystem gespeichert.

Gibt es im eigenen Framework solch eine Möglichkeit nicht, dann kann man das eigene System mit einer Mail Server Attrappe verknüpfen. Solche Attrappen nehmen die Emails der Anwendung entgegen, leiten diese aber nicht weiter. Bekannte Implementierungen sind MailHog oder GreenMail.

Mit GreenMail können nicht nur SMTP Server, sondern auch IMAP und POP3 Server imitiert werden. Entweder in Form einer Stand-Alone Anwendung oder wie im folgenden Beispiel in einem JUnit 5 Integrations-Test.

GreenMail stellt eine JUnit 5 Extension für Unit Tests bereit, das folgende Beispiel basiert aber auf einer eigenen JUnit 5 Extension.

@GreenMailSettings(setup = SMTP, debug = true)
@ExtendWith(GreenMailExtension.class)
class MailTest {
  @Test
  void testSMTP(GreenMailBox mailBox, Session mailSession)
      throws MessagingException, ExecutionException, InterruptedException {
    TreeMailService service = new TreeMailService(mailSession);
    service.sendTreeMail("info@ahnen.de", "admin@vorfahren.org");

    Future<Mail> receivedMessage = mailBox.findBySubject("Neue Stammbäume");
    assertEquals(List.of("info@ahnen.de", "admin@vorfahren.org"), receivedMessage.get().getTo());
  }
}

Damit ein GreenMail SMTP Server für die Unit Tests bereitsteht, muss die Extension @ExtendWith(GreenMailExtension.class) verwendet werden. Damit startet ein SMTP Server für jeden Test und wird nach dem Test wieder beendet.

Über die Annotation @GreenMailSettings kann zusätzlich definiert werden, was für ein Server verwendet werden soll und ob der JavaMail Debug Modus aktiviert werden soll.

Der hier dargestellte Test hat zwei Methoden-Parameter, die von der Extension mit passenden Werten befüllt werden. Insgesamt unterstützt die Extension die Typen Properties, Session und GreenMailBox. Ein Parameter vom Typ Properties wird mit den JavaMail Properties für den GreenMail SMTP Server befüllt. Ein Parameter vom Typ Session wird mit eben diesen Properties erzeugt. Beide Parameter können dafür genutzt werden, die zu testende Implementierung mit korrekten Verbindungsparameter zu versorgen.

Der Parameter vom Typ GreenMailBox dient innerhalb des Tests zum Validieren des E-Mail Versands. Mit der Methode findBySubject wird im GreenMail SMTP Server geschaut, ob eine entsprechende E-Mail dort angeliefert wurde. Da der Server asynchron arbeitet, liefert diese Methode ein Future<Mail> zurück. Dabei handelt es sich bei der Klasse Mail um eine Wrapper-Klasse für MimeMessages, um die Prüfungen im Test zu vereinfachen. Neben der findBySubject existiert noch eine findAll Methode für alle angelieferten E-Mails und findByRecipient Methode, die E-Mails für einen speziellen Adressaten zurückliefert.

Der nächsten Beitrag wird sich um die Implementierung der JUnit 5 GreenMail Extension drehen und weitere Details der JavaMail API beleuchtet.