Ein kleines Mail-Programm

(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)

Ein anderes Beispiel: Wir wollen auf eine E-Mail-Nachricht antworten. Die Meldung ist in einer Datei vorhanden, und wir wollen in der Antwort den ursprünglichen Text in der üblichen Art zitieren, also »quoten«. Von den Kopfzeilen sollen alle uninteressanten gelöscht werden, und aus den restlichen soll ein neuer Header gebildet werden.

Beispiel einer E-Mail-Nachricht
From elvis Fri Jun 15 11:15 2007
Received: from elvis@localhost by tabloid.org (8.11.3) id KA8CMY
Received: from tabloid.org by gateway.net.net (8.12.5/2) id N8XBK
Received: from gateway.net.net Fri Jun 15 11:16 2007
To: jfriedl@regex.info (Jeffrey Friedl)
From: elvis@tabloid.org (The King)
Date: Fri, Jun 15 2007 11:15
Message­Id: <2007061539939.KA8CMY@tabloid.org>
Subject: Be seein' ya around
Reply­To: elvis@hh.tabloid.org
X­Mailer: Madam Zelda's Psychic Orb [version 3.7 PL92]
Sorry I haven't been around lately. A few years back I checked
into that ole heartbreak hotel in the sky, ifyaknowwhatImean.
The Duke says "hi".
   Elvis

In diesem Kasten ist eine typische E-Mail-Nachricht dargestellt. In den Kopfzeilen stehen ein paar interessante Informationen -– Datum, Betreff usw. -–, aber auch viel Irrelevantes, das gelöscht werden soll. Unser Skript soll mkreply heißen und die Datei king.in verarbeiten. Das Programm soll wie folgt aufgerufen werden:

% perl ­-w mkreply king.in > king.out

(Zur Erinnerung: Mit der Option -­w gibt Perl zusätzliche Warnungen aus, siehe Zu den Beispielen: Eine kleine Einführung in Perl.)

Die erzeugte Datei king.out soll etwa so aussehen:

To: elvis@hh.tabloid.org (The King) 
From: jfriedl@regex.info (Jeffrey Friedl) 
Subject: Re: Be seein ya around

On Fri, Jun 15 2007 11:15 The King wrote: 
|> Sorry I havent been around lately. A few years back I checked 
|> into that ole heartbreak hotel in the sky, ifyaknowwhatImean. 
|> The Duke says "hi". 
|>      Elvis

Analysieren wir das. Für die Kopfzeilen der Antwort-Mail benötigen wir die Adresse (hier elvis@hh.tabloid.org), den vollen Namen des Adressaten (The King), unsere eigene Adresse sowie den Betreff. Für die erste Zeile der Antwort brauchen wir außerdem das Datum der empfangenen Meldung.

Die Arbeit kann in drei Schritte aufgeteilt werden:

  1. Relevante Informationen aus den alten Kopfzeilen entnehmen.
  2. Den neuen Header herausschreiben.
  3. Die Meldung mit ››|>‹‹ eingerückt herausschreiben.

Ich greife etwas vor –- wir können uns eigentlich erst dann um das Verarbeiten von Daten kümmern, wenn wir wissen, wie Daten in ein Programm eingelesen werden. Perl macht das mit dem magischen »<>«-Operator sehr einfach. Dieses merkwürdige Konstrukt liefert die nächste Eingabezeile, wenn man es einer normalen $variable zuweist. Die Eingabezeile kommt aus den Dateien, die auf der Befehlszeile nach der Datei mit dem Perl-Skript angegeben werden; in diesem Falle von king.in.

Der zweibuchstabige <>-Operator von Perl soll nicht mit der Ausgabeumleitung »> datei« der Shell oder mit den Perl-Operatoren Größer-als/Kleiner-als verwechselt werden. <> ist einfach Perls merkwürdige Variante einer getline()-Funktion.

Wenn alle Zeilen gelesen sind, gibt <> praktischerweise den undefinierten Wert (undef, wird in einer Bedingung als »falsch« interpretiert) zurück. Mit folgender Phrase wird eine Datei zeilenweise gelesen:

while ($zeile = <>) {
   ... verarbeite $zeile ...
}

Wir werden so etwas verwenden, aber das Problem verlangt, dass die Kopfzeilen und die eigentliche Meldung separat behandelt werden. Der Kopf umfasst alles bis zur ersten Leerzeile, was danach kommt, ist der Text der Meldung. Um nur den Kopf zu lesen, verwenden wir etwa:

# Kopfzeilen verarbeiten

while ($zeile = <>) {    
   if ($zeile =~ m/^\s*$/) {
       last; # springt hinter das Ende der while-Schleife
   }
    .
    .
    .
   ... Kopfzeile verarbeiten ...
    .
    .
    .
}
... Rest der Meldung verarbeiten ...
    .
    .
    .

Auf das Ende des E-Mail-Kopfs testen wir mit dem regulären Ausdruck ˹^\s*$˼. Dieser prüft, ob der String einen Anfang hat (haben alle), ob darauf eine beliebige Menge Whitespace folgt (solchen erwarten wir zwar nicht) und ob darauf das Ende des Strings erreicht wird. (Anmerkung: Ich benutze hier das Wort »String« statt »Zeile«, obwohl das in diesem Fall aufs Gleiche hinausläuft. Mit Perl kann man nämlich einen regulären Ausdruck auch auf Strings anwenden, die mehrzeilige Texte enthalten. Die Anker Zirkumflex und Dollar beziehen sich (normalerweise) auf den Anfang und das Ende eines ganzen solchen Strings und nicht auf die im String enthaltenen Zeilen (später in diesem Kapitel sehen wir ein Gegenbeispiel dazu). Wie gesagt, ist das hier nicht wichtig, weil wir wissen, dass $zeile immer nur exakt eine wirkliche Zeile der E-Mail enthält.) Mit dem Schlüsselwort last wird aus der einschließenden while-Schleife herausgesprungen. Damit ist die Verarbeitung der Kopfzeilen beendet.

Nun können wir nach dem Test auf eine Leerzeile mit den Header-Zeilen tun, was zu tun ist. Wir wollen aus ihnen Informationen wie das Datum und den Betreff entnehmen.

Um den Betreff herauszuholen, wenden wir eine Technik an, die uns noch öfter begegnen wird:

if ($zeile =~ m/^Subject: (.*)/i) {
    $subject = $1;
}

Hier wird versucht, einen String zu finden, der mit ››››Subject: (egal, ob Groß- oder Kleinschreibung) beginnt. Wenn so viel des regulären Ausdrucks passt, muss der Rest ˹.*˼ auf alles passen, was weiter hinten in der Zeile folgt. Weil das ˹.*˼ in Klammern steht, können wir nachher den Betreff in $1 weiterverarbeiten. In unserem Fall speichern wir den Text einfach in der Variablen $subject. Wenn der reguläre Ausdruck nicht passt (und das wird bei den meisten Zeilen so sein) und also »falsch« zurückgibt, ist auch die Bedingung unwahr, und die Variable $subject wird für eine solche Zeile nicht gesetzt.

Vorsicht bei ˹.*˼
Der Ausdruck ˹.*˼ wird häug benutzt, wenn »beliebig viel von irgendwas« erlaubt sein soll. Der Punkt passt auf jedes Zeichen (oder, in einigen Sprachen, auf jedes Zeichen außer Newline), und der Stern lässt beliebig viele davon zu, erfordert aber keines. Das kann sehr nützlich sein.
Es gibt hier aber ein paar versteckte Fallen, die dem Benutzer dann gefährlich werden, wenn die Auswirkungen innerhalb größerer Ausdrücke nicht verstanden werden. Wir haben dazu bereits ein Beispiel gesehen; unter Wie Regex-Maschinen arbeiten wird dies eingehender behandelt.

Auf ähnliche Art testen wir Zeilen auf Date und Reply­To:

if ($zeile =~ m/^Date: (.*)/i) {     
     $datum = $1;
}
if ($zeile =~ m/^Reply­To: (.*)/i) {
     $reply_adresse = $1;
}

Die From:-Zeile erfordert etwas mehr Arbeit. Zunächst wollen wir die Zeile, die mit ››From:‹‹ beginnt, nicht die kryptische ››From‹‹-Zeile ohne Doppelpunkt am Anfang. Wir wollen:

From: elvis@tabloid.org (The King)

Diese Kopfzeile enthält sowohl die E-Mail-Adresse als auch den vollen Namen des Absenders in Klammern. Wir wollen daraus den Namen extrahieren.

˹^From:(\S+)˼ passt bis zur und auf die Adresse. Wie man erraten kann, passt ˹\S˼ auf alles, was nicht Whitespace ist. Damit erkennt ˹\S+˼ Zeichen bis zum ersten »weißen« Zeichen (oder bis zum Ende des Strings). In unserem Fall ist das die Absenderadresse. Danach sind wir am vollen Namen interessiert, der in Klammern erscheint. Dazu müssen wir auf Klammern testen und benutzen dafür ˹\(...\)˼. Die zwei Backslashes werden benötigt, damit die Klammern ihre normale Metazeichen-Bedeutung verlieren. Innerhalb der Klammern sind wir an allem interessiert, außer an weiteren Klammern! Das erreichen wir mit ˹[^()]*˼. Sie erinnern sich: Metazeichen in Zeichenklassen sind verschieden von den »normalen« Regex-Metazeichen. Innerhalb einer Zeichenklasse sind Klammern nichts Besonderes und brauchen keinen Backslash.

Alles zusammengesetzt ergibt:

˹^From:l(\S+)\(([^()]*)\)˼

Das sieht mit all den Klammern verwirrend aus, deshalb wird es in der nächsten Abbildung klarer dargestellt.

Verschachtelte Klammern, $1 und $2

Abbildung: Verschachtelte Klammern, $1 und $2.

Wenn die Regex aus dieser Abbildung passt, haben wir den vollen Namen des Absenders in $2 und auch eine mögliche Rücksendeadresse in $1:

if ($zeile =~ m/^From: (\S+) \(([^()]*)\)/i) {
    $reply_adresse = $1;
    $from_name = $2;
}

Weil nicht alle E-Mail-Nachrichten eine Reply­-To-Kopfzeile besitzen, behalten wir $1 als provisorische Rücksendeadresse. Wenn später in der Datei doch eine Reply­-To-Zeile auftaucht, werden wir $reply_adresse einfach mit dieser Adresse überschreiben. Das Programm sieht nun so aus:

while ($zeile = <>)
{
    if ($zeile =~ m/^\s*$/ ) {  # bei einer Leerzeile ...
        last;                   # ... springen wir sofort aus der ›while‹-Schleife heraus.
    }
    if ($zeile =~ m/^Subject: (.*)/i) {
        $subject = $1;
    }
    if ($zeile =~ m/^Date: (.*)/i) {
        $datum = $1;
    }
    if ($zeile =~ m/^Reply­To: (\S+)/i) {
        $reply_adresse = $1;
    }
    if ($zeile =~ m/^From: (\S+) \(([^()]*)\)/i) {
        $reply_adresse = $1;
        $from_name = $2;
    }
}

Jede Kopfzeile wird mit jedem regulären Ausdruck verglichen, und wenn die Zeile passt, wird eine entsprechende Variable gesetzt. Viele Zeilen werden auf keinen der regulären Ausdrücke passen und werden so ignoriert.

Nach dieser while-Schleife können wir den Kopf der Antwort ausgeben: (Anmerkung: In Perl muss das @-Zeichen in regulären Ausdrücken und in Strings in Anführungszeichen meist mit einem Backslash geschützt werden.)

print "To: $reply_adresse ($from_name)\n";
print "From: jfriedl\@regex.info (Jeffrey Friedl)\n";
print "Subject: Re: $subject\n";
print "\n" ; # Eine Leerzeile trennt die Kopfzeilen vom Text der Nachricht.

Beachten Sie, dass auf der neuen Subject-Zeile ein ››Re:‹‹ (Anmerkung: Dieses »Re« ist keine Abkürzung für »Reply«, sondern kommt vom lateinischen »res«, zur Sache; daher und nach RFC 2822 ist das leider oft anzutreffende »Aw:« doppelt falsch. (Anm. d.Ü.)) eingefügt wird, um anzuzeigen, dass es sich um eine Antwort zum selben Betreff handelt. Als einführende Zeile geben wir aus:

print "On $datum $from_name wrote:\n";

Die eigentliche Meldung lesen und schreiben wir Zeile für Zeile und fügen vorne jeweils ein ››|>‹‹ ein:

while ($zeile = <>) {
    print "|> $zeile";
}

Wir brauchen hier kein Newline auszugeben, weil wir wissen, dass jede der gelesenen Zeilen bereits ein solches besitzt.

Auch das Einrücken und »Quoten« der Zeilen mit print "|> $zeile" ließe sich mit einem Regex-Konstrukt erledigen:

$zeile =~ s/^/|> /;
print $zeile;

Die Substitution sucht nach einem ˹^˼ und findet das (natürlich) am Anfang des Strings. Der reguläre Ausdruck ist zwar erfolgreich, »verbraucht« aber keine Zeichen im String; somit ersetzt die Substitution das »Nichts« am Anfang des Strings durch ›|>‹; anders ausgedrückt, wird ››|>‹ vorne angefügt. Das ist ein unziemlicher Missbrauch von regulären Ausdrücken für einen banalen Zweck, doch wir werden etwas recht Ähnliches am Ende des Kapitels noch einmal sehen.

Probleme aus der Praxis, praxistaugliche Lösungen

Wenn ich Probleme aus der Praxis behandle, müssen auch deren Grenzen aufgezeigt werden. Zunächst muss ich aber betonen, dass es hier darum geht, den Gebrauch von regulären Ausdrücken zu demonstrieren, und Perl macht das einfach. Der Perl-Code ist nicht unbedingt der effizienteste oder der eleganteste, aber ich hoffe, dass er dafür leicht verständlich ist.

Nun ist aber die reale Welt weit komplizierter, als wir uns das bei unserem simplen Beispiel gedacht haben. Eine From:-Zeile kann in einer Reihe von verschiedenen Formaten auftreten, unser Programm erkennt aber nur eins davon. Wenn es aber keine From:-Zeile erkennt, wird der Variablen $from_name nie etwas zugewiesen. Sie behält damit den undefinierten Wert (eine Art »kein Wert«-Wert), wenn wir sie ausgeben wollen. Eine korrekte Lösung wäre die Erweiterung der Regex, so dass alle möglichen Adress-/Namensformate erkannt werden; aber als ersten Schritt können wir den folgenden Test einfügen, nachdem der Header eingelesen wurde und bevor der Text der Meldung ausgegeben wird:

if (   not defined($reply_adresse)
    or not defined($from_name)
    or not defined($subject)
    or not defined($datum) )
{
    die "Kann nicht alle nötigen Informationen finden!\n";
}

Die defined-Funktion von Perl gibt an, ob die Variable im Argument einen gültigen Wert besitzt oder nicht; die (»stirb!«) gibt eine Fehlermeldung aus und beendet das Programm.

Außerdem nimmt unser Programm an, dass eine From:-Zeile vor der Reply­-To:-Zeile auftritt, wenn beide vorhanden sind. Wenn die From:-Zeile später kommt, wird $reply_adresse, das bereits die korrekte Rücksendeadresse enthält, mit einer möglicherweise falschen Adresse überschrieben.

Die wirkliche Praxis

E-Mail-Nachrichten werden von sehr vielen verschiedenen Programmen erzeugt, und jedes hat seine eigene Vorstellung davon, wie korrekte Header-Zeilen aussehen müssten und wie die Standards zu interpretieren sind. Die Verarbeitung von E-Mail kann daher äußerst vertrackt sein. Ich bin auf dieses Problem gestoßen, als ich an einem Pascal-Programm arbeitete. Dabei ist mir klar geworden, dass die Behandlung dieses Problems ohne reguläre Ausdrücke extrem schwierig ist. So schwierig, dass es für mich einfacher war, ein kleines Perl-artiges Regex-Paket für Pascal zu schreiben, als das Problem mit »nacktem« Pascal anzugehen. Ich hielt die Kraft und Flexibilität der regulären Ausdrücke für selbstverständlich, bis ich mit einer Welt konfrontiert wurde, in der es sie nicht gab! Ich bin dieser Welt schnell wieder entflohen.

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Reguläre Ausdrücke bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:
   

Copyright der deutschen Ausgabe © 2008 by O’Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Reguläre Ausdrücke" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstr. 81, 50670 Köln