Klicken lassen

Webmail abholen mit Perl

Praxis & Tipps | Praxis

Manche Webmail-Provider erlauben den POP3-Abruf der Post mit einem normalen Mailprogramm gar nicht oder nur gegen Aufpreis. Ihre Kunden müssen die Mails dann unkomfortabel und mit tickendem Online-Gebührenzähler im Browser lesen - oder die lästige Klickerei einem Perl-Skript überlassen.

Das Perl-Modul LWP (Library for WWW in Perl) simuliert einen Web-Browser und den Surfer, der ihn bedient. Es kann daher fast alle Seiten aus dem Web laden, auch die der meisten Webmail-Anbieter. In einem Skript zum automatisierten Mail-Abruf geht der simulierte Surfer genauso vor, wie es ein menschlicher täte: Login-Seite des Providers aufrufen, die Login-Felder ausfüllen und absenden, dann bis zur Mailbox-Übersicht durchklicken und alle neuen Nachrichten anschauen. Für das Skript heißt „anschauen“, die Mail an ein E-Mail-Programm weiterzureichen oder ins Mail-System des Rechners einzuspeisen, auf dem es läuft.

Dieser Artikel stellt schrittweise den Umgang mit LWP und einige typische Verfahren zum Mailabruf vor. Als Beispiel dienen dabei die Webmail-Seiten von T-Online. Ungeändert funktioniert das Skript, das wie üblich über den Soft-Link zum Download bereit steht, nur mit diesem Web-Interface und auch nur in der Fassung, die derzeit online steht. Wenn T-Online die Seiten ändert, muss auch das Skript angepasst werden. Doch dazu sind nur Grundkenntnisse in Perl [1] und minimales Wissen über HTML [2] erforderlich.

Perl landet bei der Installation aller aktuellen Linux-Distributionen mit auf der Festplatte. Für Windows stellt die Firma ActiveState ein leicht zu installierendes Paket kostenlos zum Download bereit (siehe Soft-Link).

Das LWP-Modul gehört zum Lieferumfang von Perl und stellt dem Programmierer eine Reihe von Objekten zur Verfügung. Er muss daher zuerst ein neues Browser-Objekt vom Typ UserAgent anlegen und dann dessen Methoden benutzen. Um mit LWP mal eben die Startseite des Webmail-Systems von T-Online zu lesen, genügen schon ein paar Zeilen:

use LWP;
$url = 'https://webmail.t-online.de';
$ua = LWP::UserAgent->new();
$form = $ua->get($url) or die "Couldn't fetch $url";
$form->is_success() or die $form->message(); print $form->content();

Die Methode get() ruft eine URL ab wie ein interaktiver Browser. Als Antwort liefert sie ein neues Objekt vom Typ Response, über dessen Methode is_success() man prüfen kann, ob der Abruf geklappt hat. In jedem Fall liefert die Methode message() den Text der HTTP-Statusmeldung, der einen Fehler halbwegs verständlich beschreibt. Wenn kein Fehler aufgetreten ist, versteht das Response-Objekt den Methodenaufruf $form->content(), der den Inhalt der abgerufenen Seite zurückliefert.

Besonders hilfreich beim Testen ist die zusätzliche Zeile use LWP::Debug qw(+); am Anfang eines Skripts. Damit erzeugt LWP auf der Kommandozeile einen ausführlichen Debugging-Text. Wenn alles funktioniert, kann man die Zeile natürlich auskommentieren.

Wenn der erste Seitenabruf klappt, beginnt die Analyse der HTML-Seite. Dabei gilt das besondere Augenmerk den enthaltenen Formularen und Links. Denn ein menschlicher Anwender teilt dem Server ja mit, was er als Nächstes sehen will, indem er auf einen Link klickt und eventuell vorher ein Formular ausfüllt. Um per Skript die richtigen Anfragen zu stellen, muss der Programmierer präzise wissen, welche Klicks und Eingaben der Server erwartet.

Dazu schaut man sich den Quelltext genau an, und zwar zweckmäßigerweise in der von $form->content() gelieferten Fassung. Denn viele Web-Seiten sind für einzelne Browser aufbereitet und laden je nach Browserkennung unterschiedliche Inhalte. Der HTML-Code im Response-Objekt muss also nicht derjenige sein, den man mit Mozilla oder dem Internet Explorer sehen würde.

Wenn der ganze Quelltext sich als zu kompliziert erweist, hilft oft Mozilla weiter. Wenn man im Browser einen Teil der Seite markiert, erscheint im Kontextmenü der Eintrag „Show selection source“, über den man direkt den passenden Ausschnitt aus dem Quelltext erreicht.

Wenn auch das nicht die nötigen Informationen erbringt, klickt man sich im Browser durch die Webmail-Seiten und beobachtet dabei die Kommunikation mit dem Server. Netzwerk-Sniffer wie Ethereal oder TCPDump tun dabei gute Dienste: Eine ganz normale HTTP-Verbindung kann jeder im Klartext mitlesen [3]. Der Geheimtipp zur Analyse ist der textbasierte Browser Lynx - der kann nämlich ein Trace-Log seiner eigenen Kommunikation anlegen. Mit lynx -trace erhält man ein ausführliches Logfile der Browser- und Webserver-Kommunikation und kann ganz genau ermitteln, wie das Frage-Antwort-Spiel aussieht.

Bei T-Online genügt ein Blick auf den Quelltext. Die erste Seite enthält das Login-Formular; der (erheblich gekürzte) Quelltext lautet

<form name="loginform" action="main.cgp" method="POST" onSubmit="this.js.value=1">
<input type="text" name="s6mBw4zjtTWAMq0Dfx6cTYE3w3wlog1n" value="" size="15">
<input type="password" name="s6mBw4zjtTWAMq0Dfx6cTYE3w3wpassw8rd">
<input type=hidden name="js" value="0">
<input type=hidden name="sessionid" value="s6mBw4zjtTWAMq0Dfx6cTYE3w3w">
</form>

Das Formular hat also vier Felder, zwei sichtbare und zwei unsichtbare. Das Form-Tag verrät, dass das Skript die Eingaben an die aktuelle URL plus dem String „main.cgp“ schicken soll. Dabei muss es die im HTTP-Methode POST benutzen, für die LWP die gleichnamige Funktion zur Verfügung stellt.

Das erste Feld nimmt den Usernamen auf; seine Bezeichnung besteht aus einer Session-ID und dem festen Anteil „log1n“ (die Ziffer 1 ist kein Druckfehler!). Den Namen des Passwortfeldes bilden die Session-ID und das Anhängsel „passw8rd“. Die beiden Werte legt man sinnigerweise am Anfang des Perl-Skripts in zwei Variablen (z. B. $uname und $pword).

Das versteckte Feld „js“ dient dazu, festzustellen, ob der Browser JavaScript ausführt: Es ist mit „0“ vorbesetzt, doch aufgrund des onSubmit-Eintrags im Form-Tag ändern Browser mit aktivem JavaScript den Wert beim Abschicken auf „1“. Da JavaScript die automatische Auswertung der folgenden HTML-Seiten extrem erschwert, übergibt das Perl-Skript in dieser Variable eine Null.

Die vierte Variable „sessionid“ enthält einen String, der die gerade startende Webmail-Session eindeutig kennzeichnet, die Session-ID. Der automatisierte Browser braucht sie nicht nur, um die Feldnamen für Usernamen und Passwort zusammenzubauen, sondern muss sie bei allen weiteren Anfragen an den Server mit angeben. Daher klaubt das Skript die Session-ID per Regular Expression aus der ersten Webseite. Im HTML-Quelltext taucht die ID mehrfach auf, beispielsweise so:

<frame src="/s6mBw4zjtTWAMq0Dfx6cTYE3w3w/login_in_frame.cgp"> 

Die variable Session-ID steht hier zwischen zwei Schrägstrichen und enthält nahezu beliebige Zeichen - außer Schrägstrichen. Da der String login_in_frame.cgp bei jedem Aufruf gleich ist und nur einmal in der Seite vorkommt, eignet er sich hervorragend, um die Session-ID zu finden:

($id) = $form->content() =~ m{/([^/]+)/login_in_frame\.cgp}s; 

Wegen der Klammer auf der linken Seite wird der Ausdruck im „Listenkontext“ ausgewertet. Man erhält also die Liste der Treffer und nicht bloß ihre Anzahl, wie im skalaren Kontext. Die Klammer innerhalb des Ausdrucks bewirkt, dass in der Liste nur der Text landet, auf den der geklammerte Teilausdruck passt - also die Session-ID ().

Mit der so gewonnenen Session-ID und den durch Analyse des Formulars ermittelten Feldnamen sieht der Code zum Login so aus:

$form_login = $id . "log1n";
$form_pass = $id . "passw8rd";
$resp = $ua->post( $url . "/main.cgp",
[ $form_login => $uname, $form_pass => $pword,
'js' => '0', 'sessionid' => $id ] );

Der Funktion post gibt man die Form-Variablen und ihre Werte einfach als Paare aus Namen und Werten in eckigen Klammern mit. Wer es genauer wissen will, schaut im Fortgeschrittenen-Kapitel der einschlägigen Perl-Literatur unter „anonyme Hash-Referenz“ nach.

Der Aufruf der post-Funktion entspricht dem Klick auf den „Login“-Knopf im Webmail-Formular. Wer die Seite mit seinem Browser besucht, erhält daraufhin die Übersichtsseite mit den Mails. Tatsächlich antwortet der T-Online-Server jedoch zuerst mit einer Weiterleitungsseite (redirect), die ein normaler Browser sofort ausführt, statt sie anzuzeigen. Das Perl-Skript muss diesen Schritt selbst tun. Dazu prüft es mit der is_redirect()-Methode des Response-Objekts, ob die auf das POST hin übertragene Seite eine Weiterleitung ist, und holt sich dann aus dem passenden Header-Feld der Server-Antwort (Location) die nächste URL.

if($resp->is_redirect()) {
($location) = ($resp->headers()->{'location'} =~
m/(https.+?)main.cgp/); }

Den größten Teil der Weiterleitungs-URL merkt sich das Skript, da es eine enthaltene erweiterte Session-ID für alle weiteren Seitenaufrufe braucht. Nun genügt eine unscheinbare Zeile, um die Mail-Übersichtsseite zu laden:

$inbox = $ua->get("$location/main.cgp");  

liefert eine lange Webseite mit der Auflistung der Inbox-Mails zurück. In der Grundeinstellung verteilt T-Online diese Liste auf mehrere Seiten, daher stellt man vor dem ersten Abholen die Liste auf „alle Mails“ um.

Post!

Aus dieser Übersicht stellt das Skript nun eine Liste der Nachrichten zusammen, um sie einzeln zu bearbeiten. Freundlicherweise übermittelt T-Online in der Übersicht zu jeder E-Mail eine eindeutige Zahl, über die das Skript die Nachricht auswählen kann. Diese Mail-ID steht unter anderem in dem Link, mit dem man eine Mail zum Lesen öffnet, zum Beispiel:

<a href="read.cgp?MAIL=1464800074"><span class="sanserif">Post</span></a>

Daraus lässt sich die Liste der Mail-IDs leicht sammeln:

foreach my $key($inbox->content()=~m/MAIL=(\d+?)\"/g) { $ids{$key} = 1; }

Wieder tut „=~“ gute Dienste: Diesmal trägt der Match-Ausdruck das Flag „g“. Daher liefert er eine Liste sämtlicher Treffer der Regular Expression zurück. Allerdings passt der Ausdruck pro Mail zweimal; durch die Verwendung als Hash-Key wird die Liste eindeutig. Im Hash %ids steht also nun eine Liste der eindeutigen Mail-IDs aller Nachrichten.

Um eine Nachricht zuzustellen, braucht das Skript sie als reinen Text ohne HTML-Auszeichnungen, inklusive des Headers und der Attachments. Viele Webmail-Dienstleister bieten an, eine Nachricht als Datei zu speichern. Da der Browser dabei nur den Inhalt der zu speichernden Datei erhält, ist dies die einfachste Möglichkeit, an die Mail im richtigen Format zu kommen. Bei T-Online setzt man im Browser den Haken vor der Nachricht und klickt auf das „Speichern“-Symbol am unteren Rand. Das kann natürlich auch LWP.

Die ganze Übersichtsseite ist im Prinzip ein großes Formular, das allerdings anders als das Login-Formular nicht die POST-, sondern die GET-Methode verlangt; es erwartet die Parameter also als Bestandteil der URL. Die einzelnen Checkboxen sind so definiert:

<input type="checkbox" name="MAIL[1]" value="1464800074">

Der Name der Checkboxen enthält in der eckigen Klammer eine laufende Nummer, die allerdings für die Abfrage nicht erforderlich ist, es genügt, den Parameter „MAIL[0]“ auf die ID der gewünschten Mail zu setzen. Dies ist eine vereinfachende Spezialität der T-Online-Seite, bei anderen Anbietern muss sich ein Skript immer auch die Namen der Elemente merken.

Der Speichern-Knopf ist gemeinerweise kein einfacher Link, sondern eine Image-Map, wie sie beispielsweise für klickbare Landkarten eingesetzt wird:

<input type=image src="/i/standard/button/b-speichern1.gif" width="36" height="36" name="Speichern" ...

Daher muss das Skript zum „Drücken“ des Knopfes nicht einfach dessen Namen angeben, sondern die Koordinaten, an denen der simulierte Klick das Bildchen getroffen hat. Als Antwort erhält LWP wieder eine Weiterleitungsseite, die es selbst auswerten muss:

$resp = $ua->get( $url . "main.cgp?MAIL[0]=" . $mail_id . "&Speichern.x=1&Speichern.y=1");
if ($resp->is_redirect()){$mail = $ua->get($resp->headers()->{'location'}); }

Mit $mail->content() steht danach der gesamte Text der Mail inklusive aller Attachments zur Weiterbearbeitung zur Verfügung.

Bei Webmail-Dienstleistern, die das Speichern der Mails als Datei nicht anbieten, muss das Skript die Nachrichten aus den HTML-Seiten herausoperieren, auf denen ein menschlicher Betrachter sie lesen würde. Auch dieses Verfahren lässt sich am T-Online-Interface demonstrieren.

Den HTML-Text einer Mail holt man sich mit einem einfachen GET-Aufruf des Links, der in der Übersicht hinter der Betreffzeile liegt:

$mail = $ua->get($url . "read.cgp?MAIL=" . $id . "&HEADER=1");

Das Feld „HEADER“ sorgt dafür, dass eben dieser mit in der Seite enthalten ist. Für die erste grobe Unterteilung der Seite bieten sich die -Tags an, die T-Online freundlicherweise zwischen dem Mail-Header und dem Text sowie zwischen dem Text und der Liste der Attachments einstreut:

my($header,$body,$txt)=$html=~m{<tt>(.+?)</tt>}sg;

Mit diesem Ausdruck formuliert man, was erhalten bleibt, der Rest außerhalb des fällt einfach weg. Aber innerhalb der Blöcke steckt noch eine Menge HTML, das mit ein paar Ausdrücken auch verschwindet. Dazu betrachtet man ein HTML-Tag einfach als „ein < gefolgt von allerhand Zeichen, die kein > sind. In Perl löscht man dann die Tags mit

$header =~ s/<[^>]+>//g;

Für das relativ einfache HTML, das bei T-Online die Mail formatiert, reicht dieser Ausdruck. Allerdings genügt er für andere Seiten gelegentlich nicht, denn er kommt mit etwas komplizierteren Fällen nicht zurecht, zum Beispiel mit auskommentierten HTML-Tags:

<!-- <b> -->unfett<!-- </b> -->

Da der einfache Ausdruck das erste > als Ende des HTML-Tags betrachtet, liefert er hier „ -->unfett -->“ zurück. Nun kann jeder beginnen, die Sonderfälle einzeln zu behandeln, die bei seinem Webmailer auftreten, doch mit den Modulen HTML::Format und HTML::Tree geht es einfacher. Mit use HTML::FormatText; importiert man dazu ein Objekt in das Skript, das HTML in ähnlich formatierten ASCII-Text umwandelt. Der Aufruf lautet dann:

$header_txt = HTML::FormatText->format_string($header, leftmargin => 0, rightmargin => 256);

Die beiden Angaben zur Randbreite sind erforderlich, da FormatText sonst links drei Leerzeichen einfügt und jede Zeile rechts bei 72 Zeichen umbricht. FormatText wandelt auch gleich die HTML-Entities (ü statt ü) in die richtigen Zeichen um.

Damit stehen Header und Body der Mail als reiner Text zur Verfügung, als Nächstes sind die Attachments dran. T-Online baut in die Anzeige einer Mail auch die Liste aller Anhänge ein. Diese Liste fischt man sich aus dem HTML-Code heraus, holt mit get() jedes Attachment, baut es zu einem „echten“ Attachment um und hängt es dann an den Mailbody. Im Beispielskript erledigt das die Funktion createAttachment(), die bislang nur ein paar der vielen möglichen MIME-Types behandelt. Da sie trotzdem recht lang ist, hier nur der Kern:

my @attachlinks =
($html =~ m/<a href="(storeattachment\/.+?)">/sg);
foreach my $link (@attachlinks)
{ my ($file) = ($link =~ m/storeattachment\/(.+?)\?/);
$resp = $ua->get($url . $link);
$attachbody = encode_base64($resp->content());
...
push(@attachments, $attachment); }

Wieder tut „=~“ gute Dienste: Diesmal trägt der Match-Ausdruck das Flag „g“ für global und Perl schafft in einer Zeile folgende Schritte: Suche den Link. Dank der Klammerung speichere den Link in $1. Weise $1 dem Array @attachlinks zu. Mach das so lange, wie Treffer erzielt werden - und schon enthält die Liste alle Links auf Attachments. Die durchläuft die foreach-Schleife, sucht sich wieder mit Hilfe von „=~“ den Filename heraus, den man für den MIME-Header braucht.

Endlich liegt die ganze Nachricht ohne störendes HTML vor und harrt ihrer Zustellung an das E-Mail-Programm. Von den vielen denkbaren Methoden demonstriert das Skript zwei: Wer einen eigenen SMTP-Server wie Hamster [4] oder Postfix [5] betreibt, benutzt dazu einfach das Standard-Modul Mail::SMTP. Auf Unix-Systemen kann man die Nachricht alternativ auch in die Spool-Datei schreiben, meist /var/spool/mail/username. Diese muss im mbox-Format vorliegen, braucht also vor jeder Mail eine spezielle From-Zeile, die das Beispielskript in der Funktion ebuildHeader() zusammenbaut.

Um zu verhindern, dass es Mails mehrfach abruft, sollte das Skript die erfolgreich abgeholten Nachrichten löschen. Bei T-Online geht das wie das oben beschriebene Speichern, nur dass die Image-Map nicht „Speichern“, sondern „Loeschen“ heißt. Auch das abschließende Ausloggen ist nur Formsache; ein einfaches get() genügt, um den „Ende“-Kopf oben rechts anzuklicken.

Das Beispielskript fasst die hier besprochenen Schritte zusammen und holt die Nachrichten aus dem Webinterface von T-Online ab. Es ist trotzdem nicht als Komplettlösung gemeint, sondern als Grundlage für eigene Weiterentwicklungen. Ansätze gibt es viele: Eine ausgefeilte Fehlerkontrolle, Behandlung von HTML-Mails, Logging der Aktivitäten, eine verschlüsselter Speicher für das Passwort und natürlich die Unterstützung anderer Webmailer. EPost ist ein lohnendes Objekt für Perl- und JavaScript-Profis. Damit das Rad dabei von möglichst wenigen Bastlern neu erfunden wird, stellen wir das Skript mit Erscheinen dieser c't als Open-Source-Projekt auf Sourceforge zur Verfügung. Die Projektseite ist ebenfalls über den Soft-Link zu finden. (je)

[1] R. Schwartz, T. Phoenix, Einführung in Perl, O’Reilly, ISBN 3-89721-147-5

[2] Stefan Münz, Die Energie des Verstehens, HTML-Dateien selbst erstellen, http://selfhtml.teamone.de

[3] Johannes Endres, Die Nase am Netz, Netzwerk-Sniffer als Diagnose-Tool, c't 3/03, S. 182

[4] Inga Rapp, Fleißiger Sammler, Hamster als Mail-Server unter Windows, c't 20/02, S. 128

[5] Henning Emmrich, Jürgen Schmidt, Die Profilösung, Der eigene Mail-Server mit Linux, c't 20/02, S. 130

Soft-Link

Die meisten im Artikel verwendeten Perl-Module gehören zum Lieferumfang von Perl, die fehlenden finden sich im „Comprehensive Perl Archive Network“ (CPAN, search.cpan.org). Die Installation ist in der jeweils beiliegenden Dokumentation erklärt. In der Regel braucht man das Programm make, das unter Windows meist fehlt. Microsoft stellt eine passende Version als nmake.exe bereit (siehe Soft-Link).

Das Modul Crypt::SSLeay, das LWP auch den URL-Typ https beibringt, also den http-Abruf über eine verschlüsselte Verbindung nach SSL (Secure Socket Layer), erfordert unter Windows zwei DLLs. Wer diese nicht selbst kompilieren will, installiert mit Hilfe des Installationstool PPM ein fertiges Paket:

ppm install http://theoryx5.uwinnipeg.ca/ppms/Crypt-SSLeay.ppd 

Für die ältere Version liegt die passende Datei in einem anderen Verzeichnis. Der Aufruf lautet daher:

ppm install http://theoryx5.uwinnipeg.ca/ppmpackages/Crypt-SSLeay.ppd 

Im Verlauf der Installation schlägt der PPM vor, die zwei DLLs einzuspielen. Sie müssen so abgelegt werden, dass Perl sie auch findet, am besten im Unterverzeichnis System32 des Windows-Verzeichnisses.

Kommentare

Anzeige