Email Parsing mit den MIME-Tools für Perl

Aus HackerWiki

Contents

Vorwort

Um mit Perl effektiv Emails zu parsen, stehen die MIME-tools zur Verfügung. Dieser Artikel bezieht sich auf die MIME-tools in der Version 5.420. Diese Sammlung von Perl-Modulen stellt alle Funktionalitäten zur Verfügung um MIME-Nachrichten zu zerlegen und zusammenzusetzen.

Die wichtigsten Klassen

Um effektiv mit den MIME-tools zu arbeiten, sollten man die Hauptklassen dieser Toolsammlung kennen und ihre Zusammenhänge verstehen. Daher hier eine kurze Übersicht über die Klassen, mit denen man als Programmierer zu tun bekommt, wie sie auch in der Dokumentation der MIME-tools zu finden ist:

   (START HERE)             results() .-----------------.
          \                 .-------->| MIME::          |
           .-----------.   /          | Parser::Results |
           | MIME::    |--'           `-----------------'
           | Parser    |--.           .-----------------.
           `-----------'   \ filer()  | MIME::          |
              | parse()     `-------->| Parser::Filer   |
              | gives you             `-----------------'
              | a...                                  | output_path() 
              |                                       | determines
              |                                       | path() of...
              |    head()       .--------.            |
              |    returns...   | MIME:: | get()      |
              V       .-------->| Head   | etc...     |
           .--------./          `--------'            |
     .---> | MIME:: |                                 |
     `-----| Entity |           .--------.            |
   parts() `--------'\          | MIME:: |           /
   returns            `-------->| Body   |<---------'
   sub-entities    bodyhandle() `--------'
   (if any)        returns...       | open()
                                    | returns...
                                    |
                                    V
                                .--------. read()
                                | IO::   | getline()
                                | Handle | print()
                                `--------' etc...

Wie funktioniert der Parser?

Wie an der Abbildung zu erkennen ist, beginnt man immer mit einer Instanz des Parser-Objekts (MIME::Parser). Diesem übergibt man einen Input-Stream wie z.B. einen Datei-Handler aus dem die Nachricht geparset werden soll. Ist das Parsing erfolgreich, bekommt man eine "Entity", die ich im folgenden als Nachrichten-Instanz bezeichne, welche eine Instanz von MIME::Entity, einer Subklasse von Mail::Internet, ist. Besteht die Email aus mehreren Teilen (multipart), so werden die einzelnen Teile auch als Nachrichten-Instanzen abgebildet, die in der Top-Entity (der obersten Nachrichten-Instanz) enthalten sind.

Jede Nachrichten-Instanz hat einen Head und einen Body. Der Head (eine Instanz von MIME::Head) enthält Informationen über die Nachricht (Sender, Empfänger, Content-Type, Betreff usw.). Der Body "weiß" wo sich die Daten aus der Nachricht befinden und man kann ihn "auffordern" diese zu öffnen um so einen I/O-Handler für die Nachricht zu erhalten.

Wenn die original Nachricht eine Multipart-Nachricht ist, enthält das MIME::Entitiy-Objekt eine nicht leere Liste an Teilen (parts), bei der jeder Teil ein weiteres Entity-Objekt ist, das wiederum eine Multipart-Nachricht sein kann usw.

Der Parser (MIME::Parser) fragt nach Instanzen von MIME::Decoder, wenn er eine Datei dekodieren oder kodieren muß. MIME::Decoder enthält eine Map von unterstützten Kodierungen (z.B. base64) um zu klassifizieren, welche Instanz eine Nachricht dekodieren kann. MIME::Decoder ist auch separat nutzbar und um weitere Kodierungen erweiterbar.

Erstellen einer Nachricht

Das Erstellen einer Nachricht erfolgt komplett durch die MIME::Entity-Klasse. Für Singlepart-Nachrichten kann man den Konstruktor nutzen um Nachrichten-Instanzen zu erstellen. Um eine Multipart-Nachricht zu erstellen, muß man mit der Erstellung einer Oberinstanz über MIME::Entity::build() beginnen. Danach kann man mit MIME::Entity::attach() weitere Teile zur Nachricht hinzufügen.

Wenn man eine MIME-Nachricht erstellt, müssen 2 wichtige Informationen bereitgestellt werden: der Content-Type und das Content-Transfer-Encoding. Der Content-Type ist im Normalfall einfach zu bestimmen, da er direkt vom Dateiformat abgeleitet werden kann (eine HTML-Datei ist z.B. text/html). Schwieriger wird es beim Content-Transfer-Encoding. So sind z.B. einige HTML-Dateien 7-Bit-konform, während andere sehr lange Zeilen haben können, die als quoted-printable übertragen werden müssen.

Versenden einer Nachricht

Da MIME::Entity direkt von Mail::Internet abstammt, kann man die gewohnten Mechanismen zum Versenden einer Email nutzen:

$entity->smtpsend;

Kodierungs- und Dekodierungsunterstützung

Folgende Kodierungen werden von MIME::Decoder ünterstützt.

KodierungNormalerweise benutzt, wenn folgende Bedingungen auf die Nachricht zutreffen
7bit7-bit-Daten mit weniger als 1000 Zeichen pro Zeile oder Multipart-Nachricht
8bit8-bit-Daten mit weniger als 1000 Zeichen pro Zeile
binär/binary8-bit-Daten mit sehr langen Zeilen oder ohne Zeilenumbruch
quoted-printableText-Dateien mit einigen 8-Bit-Zeichen (z.B. Latin-1-Text)
base64Binäre Daten

Welche Kodierung man auswählt hängt vor allem davon ab, was man über den Inhalt eines Dokuments weiß (Text vs. Binär) und ob man das Ergebnis für den Email-Transport nutzen will. Generell garantieren nur base64 und quoted-printable einen korrekten Transport sämtlicher Daten.

Logging von Meldungen der Mime-Tools

Da die Mime-Tools sehr komplex sind und mit verschiedensten Datenformaten arbeiten, ist es manchmal notwendig, daß man auch mal einen Blick hinter die Kulissen werfen kann. Dafür gibt es einige Meldungstypen, die vom Toolkit selbst geloggt werden:

Debug-Meldungen

Diese Meldungen werden direkt auf STDERR ausgegeben und haben immer das Präfix "MIME-tools: debug". Will man die Debug-Nachrichten zu sehen bekommen, muß man das Debugging für die Mime-Tools aktivieren:

MIME::Tools->debugging(1);

Warnungen

Warnungen werden über den Standard-Mechanismus von Perl warn() geloggt um eine ungewöhnliche Situation zu kennzeichnen. Sie haben immer das Präfix "MIME-tools: warning". Um sie nutzen zu können, muß $^W auf 'true' gesetzt sein und die Mime-Tools dürfen nicht auf 'quiet' gestellt werden:

MIME::Tools->quiet(0);

Fehler-Meldungen

Auch die Fehler-Meldungen werden über den warn()-Mechanismus geloggt. Sie haben immer das Präfix "MIME-tools: error". Auch hier muß $^W auf 'true' gesetzt sein und die Mime-Tools dürfen nicht im quiet-Modus laufen.

Benutzungsmeldungen

Anders als die typischen Warnungen, die über Fehler beim Bearbeiten von Daten informieren, sind Benutzungsmeldungen dazu da den Entwickler über veraltete Funktionen und suspekte Funktionsaufrufe in Kenntnis zu setzen.

Allgemeines

Wenn ein MIME::Parser eine Meldung absetzen will, schreibt er diese in das MIME::Parser::Results-Objekt. Dadurch steht bei jedem Parser-Durchlauf ein Trace-Log zur Verfügung.

Konfiguration des MIME-Toolkits

Man kann verschiedenste Routinen der MIME::Tools-Module nutzen um einige Einstellungen für das Toolkit anzupassen (Aktivieren von Debug-Meldungen u.ä.)

Debugging aktivieren

MIME::Tools->debugging(1); # auf 0 setzen zum Deaktivieren

Quiet-Modus

MIME::Tools->quiet(1); # auf 0 setzen zum Deaktivieren

Version des Toolkits abfragen

print MIME::Tools->version, "\n";

Parsen einer MIME-Nachricht in der Praxis

Doch nun genug der Theorie... wenden wir uns der Praxis zu. Schauen wir uns z.B. an, wie wir eine MIME-Nachricht parsen können um die einzelnen Teile in einem bestimmten Ordner ablegen zu können.

Zuerstmal müssen wir unserem Perl-Skript mitteilen, dass wir MIME::Parser nutzen wollen:

use MIME::Parser;

Das ermöglicht uns eine Instanz des Parser-Objekts zu erstellen:

my $parser = new MIME::Parser;

dem wir dann mitteilen können, wo er seinen Output ablegen soll:

$parser->output_under("$ENV{HOME}/mimemail");

Unseren Input holen wir uns hier vom STDIN, aber natürlich wäre auch ein Datei-Handle möglich:

$entity = $parser->parse(\*STDIN) or die "parse failed\n";

Erstellen und Versenden einer Multipart-Nachricht

Um eine Multipart-Email erstellen zu können, müssen wir mit dem MIME::Entity-Objekt arbeiten.

use MIME::Entity;

Zuerst einmal erstellen wir die oberste Entität und setzen darin die Email-Header:

$top = MIME::Entity->build(Type    =>"multipart/mixed",
                           From    => "me\@myhost.com",
                           To      => "you\@yourhost.com",
                           Subject => "Hello, nurse!");

Wie hier zu sehen ist, können wir direkt mit dem Konstruktor die wichtigsten Parameter setzen. Mit dem 'Type' teilen wir der Instanz mit, daß wir eine Multipart-Nachricht erstellen wollen. Bei den From- und To-Adressen muß zwingend darauf geachtet werden, daß Sonderzeichen wie das '@' escaped werden.

Da wir nun eine oberste Nachrichten-Instanz haben, können wir nun ganz einfach neue Teile anfügen. So können wir z.B. den Inhalt einer Text-Datei als ersten Teil einfügen:

$top->attach(Path=>"$ENV{HOME}/meintext.txt");

Wenn wir Bilder anfügen wollen, sollten wir dazu ein paar zusätzliche Informationen bereitstellen, wobei vor allem der Typ der Datei und die zu verwendenden Kodierung wichtig sind:

$top->attach(Path        => "./docs/mime-sm.gif",
             Type        => "image/gif",
             Encoding    => "base64");

Aber natürlich kann auch der Inhalt einer Variable angefügt werden:

$top->attach(Data=>$message);

Nun brauchen wir unsere Nachricht nur noch versenden:

open MAIL, "| /usr/lib/sendmail -t -oi -oem" or die "open: $!"; # ggf. den Pfad anpassen!
$top->print(\*MAIL);
close MAIL;

Parsing eines kompletten Maildirs

Und zu guter Letzt wollen wir uns noch anschauen wie wir ein komplettes Maildir parsen können. Ein Maildir ist ein Ordner, in dem der Mailserver jede einzelne Email in einer extra Datei ablegt. Im Gegensatz zum veralteten mbox-Format, in dem alle Emails in einer einzigen Datei landen, bietet ein Maildir eine bessere Performance und ermöglicht den Einsatz von IMAP direkt im Mailordner des Benutzers.

Wir gehen davon aus, daß sich unser Maildir in /home/vmail/domain/benutzer/new befindet, einem Ordner, in dem alle Emails für diesen Benutzer landen.

$mailbox_new = '/home/vmail/domain/benutzer/new';

Diesen Ordner öffnen wir uns holen uns alle Dateinamen der Emails.

my @newmail_files = glob("$mailbox_new/*");

Falls eine Email in dem Ordner sind, holen wir uns ein paar wichtige Daten aus den Mailheaders. Doch zuerst einmal müssen wir aus der Email eine gültige Entity machen. Dazu schreiben wir uns eine Funktion, der wir den Dateinamen der Email als Parameter übergeben können und die uns ein gültiges Entity-Objekt zurück gibt.

sub parse_MIME_stream
{
    # Parameter ueberpruefen
    if(!$_[0]) {
        print "parse_MIME_stream: Fehlender Parameter\n";
        exit;
    }
    my $file = $_[0];
    die "parse_MIME_stream: Datei $! nicht gefunden." unless defined $file;

    # ein neues Parser-Objekt erzeugen
    my $parser = MIME::Parser->new();

    # den Parser einstellen:
    # - Anhaenge duerfen nicht groesser als der verfuegbare RAM sein
    # - output_to_core('ALL') bedeutet, dass nichts auf die Festplatte geschrieben wird
    $parser->output_to_core('ALL');

    open(INPUT, $file) or die "parse_MIME_Stream: $!\n";
    my $top_entity = $parser->read(\*INPUT);
    close(INPUT) or die "parse_MIME_Stream: $!\n";

    return $top_entity;
}

Mit diesem Entity-Objekt können wir nun arbeiten.

if($newmail_files[0]) {
  foreach(@newmail_files) {
    my $infile = $_; # dies ist der Name der aktuell bearbeiteten Email-Datei
    my $top_entity = &parse_MIME_stream($infile); # das Entity-Objekt fuer diese Email
    my $subject = &handle_Mail_header($top_entity, 'subject'); # Subject der Email
    my $from = &handle_Mail_header($top_entity, 'from'); # Absender
    my $to = &handle_Mail_header($top_entity, 'to'); # Empfaenger
    my $date = &handle_Mail_header($top_entity, 'date'); # Timestamp
    my @body = &get_body($top_entity);
  }
}

Wir bedienen uns hier 2 Funktionen zum Einlesen der Daten, handle_Mail_header() und get_body(). Die Erstere von beiden gibt uns den Inhalt einzelner Header-Felder der Email zurück und hat dabei folgenden Aufbau:

sub handle_Mail_header
{
    my $entity = $_[0];
    my $param = $_[1];
    return unless defined $entity;
    return unless defined $param;
    my $head = $entity->head(); # wir holen uns den Header der Email
    $head->decode; # dekodieren
    $head->unfold; # Newlines beachten
    if($param eq 'subject') {
        if($head->get('Subject')) {
            $retval = $head->get('Subject'); # Betreff zurueck geben
        }
    } elsif($param eq 'from') {
        if($head->get('From')) {
            $retval = $head->get('From'); # Absender zurueck geben
        }
    } elsif($param eq 'date') {
        if($head->get('Date')) {
            $retval = $head->get('Date'); # Timestamp zurueck geben
        }
    } elsif($param eq 'to') {
        if($head->get('To')) {
            $retval = $head->get('To'); # Empfaenger-Adresse zurueck geben
        }
    }
    return $retval;
}

(Wird fortgesetzt)

Persönliche Werkzeuge