QVINTVS · SCRIBET

E-Mails in Ruby empfangen mit ruby-lmtp

Ich habe eine serverseitige Implementierung des LMTP-Protokolls für die Programmiersprache Ruby veröffentlicht, die ich hier samt Anwendungsbeispiel vorstelle.

Vor einiger Zeit schon hatte ich auf Twitter angekündigt, dass ich an einer neuen Library für Ruby arbeite, daher hier jetzt noch einmal gewissermaßen „offiziell“. Vor kurzem habe ich die erste Version von ruby-lmtp veröffentlicht, die wie immer über RubyGems zur Verfügung steht:

$ gem install lmtp

Das Repository befindet sich zurzeit auf GitHub.

Die Library bietet einen Server für das LMTP-Protokoll. Das heißt, mit ihrer Hilfe ist es möglich, E-Mails von jedem gängigen MTA wie etwa Postfix direkt an eine Ruby-Anwendung zustellen zu lassen. Ich nutze diese Möglichkeit etwa in meiner (noch unfertigen) Forensoftware Chessboard zur Integration von Mailinglisten in das Forum.

LMTP (RFC 2033) kommt üblicherweise in vertrauenswürdigen Umgebungen zum Einsatz, in welcher der MTA die empfangenen E-Mails zur endgültigen Speicherung an den MDA übergibt, z.B. empfängt Postfix (MTA) eine E-Mail für hans@example.com über SMTP, welche er dann anschließend zur Speicherung in der Mailbox von Hans per LMTP an Dovecot (MDA) weitergibt. Dovecot speichert die Mail dann in ein maildir-Verzeichnis, auf welches Hans dann per IMAP zugreifen kann.

LMTP ist also ein Protokoll für den vorletzten Abschnitt des Wegs einer E-Mail (der letzte ist der Abruf durch den Nutzer per IMAP/POP3). Für viel mehr ist das Protokoll auch nicht zu gebrauchen, da es insbesondere keine Sicherheitsmechanismen vorsieht. Keine Nutzernamen, keine Passwörter. Das ist auch keine Schwäche des Protokolls, sondern eine bewusste Designentscheidung, die das Protokoll gegenüber dem recht komplexen SMTP erheblich vereinfacht. Auf der anderen Seite sollte man natürlich davon absehen, das Protkoll für den Einsatz im offenen Internet zu benutzen.

Die von mir geschriebene LMTP-Bibliothek erlaubt es nun, eine Ruby-Anwendung als LMTP-Server zu betreiben. Das heißt effektiv, dass eine Ruby-Anwendung von der Außenseite gesehen wie ein Postfach behandelt werden kann. Sie erhält eine E-Mail-Adresse, und der MTA wird angewiesen, für diese E-Mail-Adresse eine Zustellung über LMTP an die Ruby-Anwendung, die als LTMP-Server fungiert, durchzuführen.

Zurzeit ist eine Zustellung ausschließlich über UNIX-Domain-Sockets möglich, eine Unterstützung für TCP-Sockets folgt evtl. später, wobei allerdings zu bedenken ist, dass aufgrund des Designs des Protokolls ein Betrieb außerhalb vertrauenswürdiger Umgebungen wirklich nicht empfehlenswert ist. In der Regel werden UNIX-Domain-Sockets die gewünschten Möglichkeiten bieten.

Postfix konfigurieren

Am Beispiel von Postfix sieht das wie folgt aus. Zunächst muss eine neue virtuelle Adresse angelegt werden. Das funktioniert wie immer bei virtuellen Mailboxen in Postfix über den Parameter virtual_mailbox_maps in der /etc/postfix/main.cf:

virtual_mailbox_maps = hash:/etc/postfix/virtual_mailboxes

Gefolgt von dem Eintrag der neuen E-Mail-Adresse in der angegebenen Datei /etc/postfix/virtual_mailboxes:

meinerubyapp@example.com /var/spool/meinerubyapp

Das angegebene Verzeichnis wird vom LTMP-Zustellungsdaemon (dazu sogleich) ignoriert. Man kann dort also angeben, was man will, etwa /dev/null oder den Pfad zur Anwendung. Es ist nur erforderlich, um die Syntax der Datei zu wahren.

Den Zustellungsmechanismus für die von Postfix verwalteten Adressen legt der Parameter transport_maps in der main.cf fest:

transport_maps = hash:/etc/postfix/transport

Erneut ist dies ein Verweis auf eine Datei im Hashformat (Tabelle), bei der eine Adresse einem Zustellmechanismus zugeordnet wird. Da eine Zustellung per LMTP beabsichtigt ist, muss sie folgende Zeile enthalten:

meinerubyapp@example.com lmtp:unix:private/meinerubyapp/lmtpsocket

Der angegebene Pfad ist relativ zum Chroot-Verzeichnis von Postfix, löst absolut also nach /var/spool/postfix/private/meinerubyapp/lmtpsocket auf. Dabei handelt es sich um einen UNIX-Domain-Socket (unix), auf dem ein LMTP-Server (lmtp) lauscht. Wenig überraschend soll es sich bei diesem LTMP-Server um eine Ruby-Anwendung handeln, die ruby-lmtp einsetzt.

Damit ist die Konfiguration von Postfix abgeschlossen. Die beiden Dateien noch mit postmap bearbeiten und den Daemon neu starten, um die Änderungen an der main.cf wirksam werden zu lassen.

# postmap /etc/postfix/virtual_mailboxes
# postmap /etc/postfix/transport
# systemctl restart postfix

Die Ruby-Anwendung

Der Einfachheit halber beschränke ich mich hier auf eine minimale Ruby-Anwendung, die zudem noch mit Root-Rechten läuft. Das sollte man aus Sicherheitsgründen selbstverständlich so nicht machen, aber es vereinfacht das Beispiel ganz erheblich, da es zum Erstellen des Sockets im angegebenen Verzeichnis Root-Rechte braucht (oder Spielereien mit File-Permissions).

Der Code ist denkbar einfach. Das folgende Ruby-Programm macht nichts anderes, als alle empfangenen E-Mails in ein Verzeichnis /tmp/mails zu schreiben. Natürlich wäre es unter Zuhilfenahme einer komplexen E-Mail-Bibliothek wie mail möglich, die E-Mail zu parsen und die einzelnen Elemente zu behandeln. So handhabt es etwa Chessboard, welches die einzelnen E-Mail-Header durchsucht. Das führt aber hier zu weit und geht am Thema vorbei; ruby-lmtp stellt die empfangenen E-Mails als langen String zur Verfügung (der auch noch die Zeilenumbrüche mit \r\n gemäß LMTP- und Mailformat-RFC enthält), welcher anschließend an mail o.ä. übergeben werden kann.

Hier ist ein Beispielprogramm, das in /usr/local/bin/lmtptest gespeichert werden könnte:

#!/usr/bin/ruby
# Testprogramm für ruby-lmtp.

# Konstanten
SOCKET = "/var/spool/postfix/private/meinerubyapp/lmtpsocket"
TARGET = "/tmp/mails"

# Wir brauchen root-Rechte
fail "Need root rights!" unless Process.uid == 0

# Verzeichnisse anlegen
unless File.directory?(File.dirname(SOCKET))
  Dir.mkdir(File.dirname(SOCKET))
end

unless File.directory?(TARGET)
  Dir.mkdir(TARGET)
end

# Für sequentielle IDs
idgenerator = 0.upto(Float::INFINITY)

# Eigentlicher Server (0666 sind die File-Permissions,
# hier rw-rw-rw, sodass Postfix auf den Socket
# schreiben kann; für production code so natürlich
# nicht empfehlenswert).
server = LmtpServer.new(SOCKET, 0666) do |email|
  filename = sprintf("%04d", idgenerator.next)

  File.open(File.join(TARGET, filename), "wb") do |f|
    f.write(email)
  end
end

# Logging-Callback
server.logging do |level, msg|
  $stderr.puts "[#{level}] #{msg}"
end

# Server starten
server.start

server.start blockt, d.h. vereinnahmt den gesamten aktuellen Thread für sich. Wenn man (wie durchaus nicht unwahrscheinlich) noch etwas anderes tun möchte, sollte man den LMTP-Server daher in einem separaten Thread laufen lassen.

Jedes Mal, wenn Postfix nun eine E-Mail auf den LMTP-Socket schreibt, wird der an new übergebene Callback ausgeführt, der die E-Mail als String erhält. Dieser schreibt sie wie erwähnt in eine Datei unter /tmp/mail.

Nicht vergessen, die Anwendung auch zu starten:

# chmod a+x /usr/local/bin/lmtptest
# lmtptest

Danach kann man testweise E-Mails an die oben angelegte Adresse meinerubyapp@example.com schreiben und dabei zusehen, wie sie ihren Weg über Postfix und die Ruby-Anwendung bis in das Zielverzeichnis /tmp/mail findet.

Fazit

Mit ruby-lmtp steht eine einfache Möglichkeit zum Empfang von E-Mails in einer Ruby-Anwendung zur Verfügung. Die Implementation des LMTP-Protokolls verhindert Race Conditions, die bei der Verwendung bloßer Pipes auftreten können genauso wie sie die einzelnen Kommunikationsvorgänge deutlich voneinander scheidet. Die Bibliothek ist pures Ruby, und hat keine Abhängigkeiten außerhalb von Rubys stdlib.

Da ich außerdem großen Wert auf gute und vollständige Dokumentation lege, kann dieselbe online eingesehen werden.

Valete.