Johnny B.: Email-Text auslesen

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

  1. 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

    1. 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

  2. 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

    --
    X-Self-Code: ie:% fl:( br:> va:) ls:& fo:) rl:? n4:° ss:| de:] js:| ch:| sh:) mo:| zu:)
    X-Self-Code-Url: http://emmanuel.dammerer.at/selfcode.html
  3. 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;

  4. =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;

    $Email_MIME1 = bless( {

    #                  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

    womöglich möchtest du ihn nach STDOUT schreiben, z.B. für ein CGI-Programm?

    use Encode qw(encode_utf8); say encode_utf8 $part->body_str;

    }

    =pod

    Wenn dir dieses Posting gefallen hat, bewerte es als hilfreich.

    =cut

    1. 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

      1. 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.