Email-Text auslesen
Johnny B.
- perl
Hallo geehrtes Forum,
es schien so einfach:
Ich will eine POP3-Mailbox aufrufen, die Emails auslesen und weiterverarbeiten. Mailbox aufrufen funktioniert mit Net::POP3. An die Emails komme ich ran. Hier benötige ich nur den eigentlichen Emailtext; HTML oder Anhänge sind vorerst unwichtig.
Ich probierte es mit Email::Simple, Email::MIME, MIME::Parser und Mail::MboxParser. Letzteres Modul machte so einen schönen Eindruck:
(hier gefunden)
my $mbox= Net::POP3->new('some mailbox');
my $mb = Mail::MboxParser->new($mbox);
for my $msg ($mb->get_messages) {
my $to = $msg->header->{to};
my $from = $msg->header->{from};
my $cc = $msg->header->{cc} || " ",
my $subject = $msg->header->{subject} || '<No Subject:>',
my $body = $msg->body($msg->find_body,0);
my $body_str = $body->as_string || '<No message text>';
print "To: $to\n",
"From: $from\n",
"Cc: $cc\n",
"Subject: $subject\n",
"Message Text: $body_str\n";
print "~" x 77, "\n\n";
}
Das ist im Prinzip genau, was ich brauche. Doch 'some mailbox' setzt wohl einen Direktzugriff auf einen Mailserver voraus? Ich habe aber nur Zugriff mittels POP3, wenn ich das richtig sehe?
Mit Email::Simple komme ich soweit:
my $email = Email::Simple->new($str);
my $from_header = $email->header("From");
my @received = $email->header("Received");
my $body = $email->body;
$body enthält die gesamte Nachricht, mit HTML und Anhängen.
Mit Email::MIME hatte ich folgendes getan:
my $parsed = Email::MIME->new($body);
my @parts = $parsed->parts; # These will be Email::MIME objects, too.
In @parts sind dann wohl die verschiedenen MIME-Parts drin, allerdings als referenzierte Hashes (keinen Plan: Email::MIME objects)?!?. Naiv versuche ich $parts[0] anzuschauen, aber irgendwie blick ich nicht, wie ich da an den plain/text-Teil herankomme.
Dann habe ich was gelesen von MIME::Parser und probierte folgendes:
my $parser = MIME::Parser->new();
$parser->output_under('/tmp');
my $message = $parser->parse_data($str);
my $num_parts = $message->parts;
for (my $i=0; $i < $num_parts; $i++) {
my $part = $message->parts($i);
print "$i - $part<hr>";
}
Hier habe ich als Ergebnis dann MIME::Entity=HASH(0x17eb6b0). Das hinterläßt mich genauso ahnungslos wie Email::MIME. Wie geht's von hier zum Text?
Was ich suche ist eine Funktion, die den Nachrichtentext korrekt formatiert (also nicht: "PS.:=20 Danke f=FCr die Links") aus der Nachricht extrahiert. Das kann doch eigentlich nicht so schwer sein...
Somit bitte ich um Hilfe. Vielleicht gibt es ja noch ein passenderes Modul oder einen einfacheren Weg?
Mille Gracie
JOhnnY
hi,
Somit bitte ich um Hilfe. Vielleicht gibt es ja noch ein passenderes Modul oder einen einfacheren Weg?
Welches Modul Du nimmst, ist egal. Du musst nur den Parser richtig einsetzen, sprich, insbesondere die header auslesen und das auch hinsichtlich Zeichenkodierung. Aber Du hast schon recht, es gibt ungezählte Module für Mail-Tools, da fällt die Wahl manchmal schwer.
Horst
Hallo Horst,
Welches Modul Du nimmst, ist egal. Du musst nur den Parser richtig einsetzen, sprich, insbesondere die header auslesen und das auch hinsichtlich Zeichenkodierung. Aber Du hast schon recht, es gibt ungezählte Module für Mail-Tools, da fällt die Wahl manchmal schwer.
--- sei mir nicht böse, aber der Hinweis, daß wenn ich den Parser richtig einsetzen würde, mein Problem gelöst wäre, hilft mir nicht wirklich weiter... :(
Verwirrte Grüße
JOhnnY
Hi Johnny
Hier habe ich als Ergebnis dann MIME::Entity=HASH(0x17eb6b0). Das hinterläßt mich genauso ahnungslos wie Email::MIME. Wie geht's von hier zum Text?
Was ich suche ist eine Funktion, die den Nachrichtentext korrekt formatiert (also nicht: "PS.:=20 Danke f=FCr die Links") aus der Nachricht extrahiert. Das kann doch eigentlich nicht so schwer sein...
Somit bitte ich um Hilfe. Vielleicht gibt es ja noch ein passenderes Modul oder einen einfacheren Weg?
Mille Gracie
JOhnnY
ich weiss jetzt nicht genau, woran Du an dieser Stelle hängst. ist es der Hash-Ausdruck? In dem Fall - ich habs im Moment leider nicht mehr parat, wie der Zugriff genau erfolgt -, es handelt sich hier um ein sogenanntes assoziatives Array. Unter diesem Stichwort oder eben unter "Hash" solltest Du in der Perl Literatur genug finden.
Ansonsten sorry für den Fall, dass ich Dein Problem nicht erfasst habe.
Liebe Grüsse
Gina
So, ich hab's zum Laufen gebracht.
Allerdings über den Umweg, mittels des MIME::Parsers erst die Dateien zu speichern, dann die Textdatei wieder auszulesen und abschließend zu löschen. Das dürfte einige Extrapunkte in Ineffizienz geben...
...aber es funktioniert! <stolz>
Der Nachrichtentext liegt korrekt formatiert vor. Ich denke, daß es eine Möglichkeit gibt, auf den Text zugreifen zu können, ohne die MIME-Teile vorher abzuspeichern?
Mit $parser->output_to_core(1); kann ich beim Parser einstellen, daß nicht gespeichert wird. Nur wo finde ich dann den Text? Vielleicht kann mir ja noch jemand beim Optimieren dieses "plumpen Gebräus, welches unendlich langsam läuft und Systemressourcen frißt" (Perl-Kochbuch S. 383) helfen?
Hier das Gebräu:
#!/usr/bin/perl -w
use strict;
use warnings;
use CGI::Carp "fatalsToBrowser";
use CGI qw(:standard :html3);
use MIME::Parser;
use Net::POP3;
my $username = 'mail@adresse.de';
my $password = 'passwort';
my $dir = '../mailbox';
my $pop = Net::POP3->new('pop.adresse.de', Timeout => 60);
print 'Content-type: text/html\n\n';
print 'Nachrichten-Center...<hr>';
# Nachrichten holen
if ( $pop->login( $username, $password ) > 0) {
my $msgnums = $pop->list;
foreach my $msgnum (keys %$msgnums) {
my $msg = $pop->get($msgnum);
my $parser = MIME::Parser->new();
# hier werden die Nachrichtendateien temporär abgelegt
$parser->output_dir($dir);
# hole Nachricht und teile sie
my $message = $parser->parse_data( join ( '', @$msg ) );#
my $subject = $message->head->get('Subject');
my $from = $message->head->get('From');
my $body;
my $text;
# wenn mehrteilige Nachricht
if ($message->head->mime_type() =~ m/multipart/i) {
my $i;
my $anzahl_teile = $message->parts();
my $unterteile;
# alle Teile der Nachricht abarbeiten
# auf der Suche nach text/plain
for ($i = 0; $i < $anzahl_teile; $i++) {
$unterteile = $message->parts($i);
# wenn mehrteilige Nachricht noch Unterteile hat
if ($unterteile->mime_type() =~ m/multipart/i) {
my $j;
my $anzahl_sub_teile = $unterteile->parts();
my $sub_unterteile;
# alle Unterteile abarbeiten
for ($j = 0; $j < $anzahl_sub_teile; $j++) {
$sub_unterteile = $unterteile->parts($j);
if ($sub_unterteile->mime_type() =~ m/text/plain/i){
$body = $sub_unterteile->bodyhandle();
last;
}
}
}
if ($unterteile->mime_type() =~ m/text/plain/i){
$body = $unterteile->bodyhandle();
last;
}
}
}
# einteilige Nachricht
else{
$body = $message->bodyhandle();
}
# Text der Nachricht holen
open FILE, $body->path();
while (<FILE>) {
$text .= $_.'<br>';
}
close FILE;
print "$from - $subject - ".$body->path()."<br>";
print "$text<hr>";
$pop->delete($msgnum);
}
}
$pop->quit;
# alle gespeicherten Nachrichtendateien löschen
opendir my $dirhandle, $dir or die $!;
while( my $entry = readdir $dirhandle ){ # nach und nach die Einträge im Verzeichnis holen
my $path = $dir . '/' . $entry;
if( -f $path ){ # wenn es eine Datei ist
unlink $path; # lösche die Datei
}
}
closedir $dirhandle;
exit;
=pod
Dieses Posting ist gleichzeitig ein funktionierendes Perlprogramm. Bitte in UTF-8 abspeichern. Führ - mich - aus! (Ich liebe Literate Programming als Lehrwerkzeug; mehr dazu in der Wikipedia.)
Ich ignoriere dein Posting mit dem Workaround. Du lernst mehr, wenn ich auf dieses hier eingehe.
Betrachten wir zunächst die Synopse von LNet::POP3:
my $msg = $pop->get($msgnum);
Die Methode get liefert eine Mail-Nachricht als String zurück. Mangels POP3-Account für Testzwecke konnte ich das nicht ausprobieren. Angenommen, eine Nachricht sei dann:
=cut
use 5.010; use utf8; # Ordnung muss sein.
my $message = <<'LOLINTERNET';
Subject: Foobar =?UTF-8?B?4pi6?=
From: foo@example.com
To: bar@example.net,
=?UTF-8?B?SGVyciDDm8Oxw6zDp8O4xJHhuJ0g?=quux@example.org
Cc: baz@example.com
Date: Thu, 1 Jan 1970 00:00:00 +0000
In-Reply-To: msgid@example.net
References: msgid@example.net
Message-Id: msgid@example.com
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="=_limes"
--=_limes
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Hallo Welt! ♡
--=_limes
Content-Type: application/x-perl;
name="foo.pl"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename="foo.pl"
Ceci n'est pas une programme.
--=_limes--
LOLINTERNET
=pod
Die weitaus besten Werkzeuge zum Zerlegen liefert ohne Zweifel das PEP. Lhttp://emailproject.perl.org/
=cut
use Email::MIME qw();
my $parsed = Email::MIME->new($message);
=pod
Dies ist objektorientierte Programmierung. Wenn du ein bares Objekt C<print>en willst, tust du es falsch. Unter Stringifizierung gibt's, den Typen (meist geC<bless>ter Hashrefs, wie du gezeigt hast) und die Hexadresse, wo's im Speicher liegt. Objekte muss man sich mit speziellen Dumpern anschauen. Ich empfehle L<Data::Dump::Streamer>, den König unter den Dumpern.
=cut
use DDS; Dump $parsed;
# body => \do { my $v = '' },
# body_raw => "--=_limes\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encod".
# "ing: 8bit\n\nHallo Welt! \x{2661}\n\n--=_limes\nContent-Type: application/x-perl;\n n".
# "ame="foo.pl"\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attach".
# "ment;\n filename="foo.pl"\n\nCeci n'est pas une programme.\n\n--=_limes--\n",
# ct => {
# attributes => { boundary => '=_limes' },
# composite => 'alternative',
# discrete => 'multipart'
# },
# header => bless( {
# headers => [
# 'Subject',
# 'Foobar =?UTF-8?B?4pi6?=',
# 'From',
# 'foo@example.com',
# 'To',
# 'bar@example.net, =?UTF-8?B?SGVyciDDm8Oxw6zDp8O4xJHhuJ0g?=quux@example.org',
# 'Cc',
# 'baz@example.com',
# 'Date',
# 'Thu, 1 Jan 1970 00:00:00 +0000',
# 'In-Reply-To',
# 'msgid@example.net',
# 'References',
# 'msgid@example.net',
# 'Message-Id',
# 'msgid@example.com',
# 'Mime-Version',
# '1.0',
# 'Content-Type',
# 'multipart/alternative; boundary="=_limes"'
# ],
# mycrlf => "\n"
# }, 'Email::MIME::Header' ),
# mycrlf => "\n",
# parts => [
# bless( {
# body => \do { my $v = "Hallo Welt! \x{2661}\n\n" },
# body_raw => "Hallo Welt! \x{2661}\n\n",
# ct => {
# attributes => { charset => 'UTF-8' },
# composite => 'plain',
# discrete => 'text'
# },
# header => bless( {
# headers => [
# 'Content-Type',
# 'text/plain; charset=UTF-8',
# 'Content-Transfer-Encoding',
# '8bit'
# ],
# mycrlf => "\n"
# }, 'Email::MIME::Header' ),
# mycrlf => "\n",
# parts => []
# }, 'Email::MIME' ),
# bless( {
# body => \do { my $v = "Ceci n'est pas une programme.\n\n" },
# body_raw => "Ceci n'est pas une programme.\n\n",
# ct => {
# attributes => { name => 'foo.pl' },
# composite => 'x-perl',
# discrete => 'application'
# },
# header => bless( {
# headers => [
# 'Content-Type',
# 'application/x-perl; name="foo.pl"',
# 'Content-Transfer-Encoding',
# '7bit',
# 'Content-Disposition',
# 'attachment; filename="foo.pl"'
# ],
# mycrlf => "\n"
# }, 'Email::MIME::Header' ),
# mycrlf => "\n",
# parts => []
# }, 'Email::MIME' )
# ]
# }, 'Email::MIME' );
=pod
Allerdings ist es Pfuibäh, mittels einfacher Dereferenzierung auf die Innereien zuzugreifen. Jemand kann gewiss einen passenden Link dazu beisteuern, warum.
Ein Blick in die Doku von LEmail::MIME verrät dann wieder, was man mit der Objektinstanz machen kann. Die MIME-Parts hattest du ja schon. Du interessierst dich aber nur für die, die normaler Text sind.
=cut
for my $part ($parsed->parts) { # $part ist wieder vom Typ Email::MIME
next unless $part->content_type ~~ m|\A text/plain|msx;
$part->body_str; # liefert dekodierten Inhalt als Textstring zurück
}
=pod
Wenn dir dieses Posting gefallen hat, bewerte es als hilfreich.
=cut
Hallo CPAN, (lustige Ansprache an sich... ;)
use DDS; Dump $parsed;
--- das sieht super aus! Ich habe mich immer gefragt, woher ich wissen soll, was in so einem Object "drin" ist. Der Dumper ist die Lösung, allerdings habe ich ihn nicht korrekt installieren können... :(
Ich habe erst den Dumper, dann das benötigte B::Utils einbezogen, aber es kommt bereits bei use Data::Dump::Streamer; die Fehlermeldung:
"Can't locate loadable object for module B::Utils in @INC"
Hierbei sei auf mein immer noch latent ungelöstes Problem mit dem korrekten Installieren der Perl-Module verwiesen; ich weiß nicht, ob es damit zusammenhängt: http://forum.de.selfhtml.org/archiv/2009/3/t184214/#m1221054
Ich habe die Module einfach kopiert, so wie Struppi es mir im Thread empfohlen hat. Bisher hat das auch gut funktioniert. Die Fehlermeldung oben krieg ich allerdings nicht weg. Der König der Dumper mag ohne seine Untergebenen nicht regieren...
for my $part ($parsed->parts) { # $part ist wieder vom Typ Email::MIME
next unless $part->content_type ~~ m|\A text/plain|msx;
--- hier hat sich ein kleiner Tippfehler eingeschlichen: =~ statt ~~, aber egal, nur so als Randbemerkung.
$part->body_str; # liefert dekodierten Inhalt als Textstring zurück
--- yau! Sehr schön! Genial! Cool! Sauber! GENAU SO! Yippey! :)
Der Vollständigkeit halber sei erwähnt, daß dies nur bei einfachen Nachrichten funktioniert. Bei multipart/alternative-Nachrichten muß man ja noch eine Ebene in die Tiefe runter. Das geht aber auch, so:
if ($part->content_type =~ m/multipart/i ) {
for my $part2 ($part->parts) {
next unless $part2->content_type =~ m|\A text/plain|msx;
$part2->body_str;
Vielen Dank für Deine Antwort, war sehr aufschlußreich.
Lieben Gruß
JOhnnY
P.S.
Wieso sind da seltsame Großbuchstaben vor jeder öffnenden Klammer in Deinem Posting?
geC<bless>ter L<http://emailproject LEmail::MIME
allerdings habe ich [DDS] nicht korrekt installieren können... :(
[...] "Can't locate loadable object for module B::Utils in @INC"
[...] Ich habe die Module einfach kopiert, so wie Struppi es mir im Thread empfohlen hat. Bisher hat das auch gut funktioniert.
XS-basierte Module kann man aber nicht einfach kopieren, die müssen kompiliert werden. Das Ergebnis davon sind u.a. besagte "loadable objects", z.B.:
site_perl/5.10.1/x86_64-linux-thread-multi-ld/auto/B/Utils/Utils.so
site_perl/5.10.1/x86_64-linux-thread-multi-ld/auto/Data/Dump/Streamer/Streamer.so
Der König der Dumper mag ohne seine Untergebenen nicht regieren...
Alternative in Pure Perl: Data::Dumper. Nicht so mächtig und hübsch, dafür schon seit langem in der Core-Distro dabei.
use Data::Dumper; say Dumper $something;
next unless $part->content_type ~~ m|\A text/plain|msx;
--- hier hat sich ein kleiner Tippfehler eingeschlichen: =~ statt ~~, aber egal, nur so als Randbemerkung.
Danke fürs Aufspüren, das war nicht so beabsichtigt. Dies ist aber kein Syntaxfehler, das Programm ist trotzdem ausführbar, und das überraschenderweise sogar wie gewollt. Eigentlich hatte ich schreiben wollen ~~ qr|\A text/plain|msx;
Hm, *kinnkratz* ich müsste glatt mal eine Perl-Critic-Policy schreiben, die ~~ in Verbindung mit m// ankreidet.
Wieso sind da seltsame Großbuchstaben vor jeder öffnenden Klammer in Deinem Posting?
geC<bless>ter L<http://emailproject LEmail::MIME
Mir war entfallen, dass ich das Forum auch selber krückenhaftes Markup zur Verfügung stellt. Das ist Pod, siehe http://perldoc.perl.org/perlpod.html http://perldoc.perl.org/perlpodspec.html
Angenommen, du hast mein Posting-Programm von threadaufwärts als t=192831m=1287967.pl gespeichert, dann probier mal aus:
# Shellcode
$ perldoc t=192831m=1287967.pl
$ pod2html t=192831m=1287967.pl > t=192831m=1287967.html
perldoc ruft den Pager auf, bspw. "less". Dies mit Taste q beenden, oder Interrupt mittels Strg+c sollte auch immer gehen.