fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2011-10-03 06:00 am

[dw-free] move cgi-bin/lj*.pl files into proper modules (in cgi-bin/LJ)

[commit: http://hg.dwscoalition.org/dw-free/rev/acfd5eeeffd6]

http://bugs.dwscoalition.org/show_bug.cgi?id=1726

Rename file and update inclusions; no package changes.

Patch by [personal profile] kareila.

Files modified:
  • bin/worker/incoming-email
  • cgi-bin/LJ/Emailpost.pm
  • cgi-bin/LJ/Emailpost/Web.pm
  • cgi-bin/ljemailgateway.pl
  • t/emailpost.t
--------------------------------------------------------------------------------
diff -r 9d6b0a0fc48c -r acfd5eeeffd6 bin/worker/incoming-email
--- a/bin/worker/incoming-email	Mon Oct 03 13:52:08 2011 +0800
+++ b/bin/worker/incoming-email	Mon Oct 03 14:00:05 2011 +0800
@@ -17,7 +17,7 @@
 use LJ::Worker::TheSchwartz;
 
 require "ljlib.pl";
-require "ljemailgateway.pl";
+use LJ::Emailpost;
 use LJ::Support;
 use LJ::Sysban;
 
diff -r 9d6b0a0fc48c -r acfd5eeeffd6 cgi-bin/LJ/Emailpost.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Emailpost.pm	Mon Oct 03 14:00:05 2011 +0800
@@ -0,0 +1,773 @@
+#!/usr/bin/perl
+# This code was forked from the LiveJournal project owned and operated
+# by Live Journal, Inc. The code has been modified and expanded by
+# Dreamwidth Studios, LLC. These files were originally licensed under
+# the terms of the license supplied by Live Journal, Inc, which can
+# currently be found at:
+#
+# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
+#
+# In accordance with the original license, this code and all its
+# modifications are provided under the GNU General Public License.
+# A copy of that license can be found in the LICENSE file included as
+# part of this distribution.
+
+
+package LJ::Emailpost;
+use strict;
+use lib "$LJ::HOME/cgi-bin";
+use LJ::Config;
+
+my $workdir = "/tmp";
+
+BEGIN {
+    LJ::Config->load;
+    if ($LJ::USE_PGP) {
+        eval 'use GnuPG::Interface';
+        die "Could not load GnuPG::Interface." if $@;
+    }
+}
+
+require 'ljlib.pl';
+use LJ::Emailpost::Web;
+require 'ljprotocol.pl';
+use Date::Parse;
+use HTML::Entities;
+use IO::Handle;
+use MIME::Words ();
+use XML::Simple;
+use Unicode::MapUTF8 ();
+use Encode;
+
+# $entity -- MIME object
+# $to -- left part of email address.  either a username, or "username+PIN"
+# $rv - scalar ref from mailgated.
+# set to 1 to dequeue, 0 to leave for further processing.
+#
+sub process {
+    my ($entity, $to, $rv) = @_;
+
+    my (
+        # journal vars
+        $head, $user, $journal,
+        $pin, $u, $req, $post_error,
+
+        # email vars
+        $from, $addrlist, $return_path,
+        $body, $subject, $charset,
+        $format, $tent,
+
+        # pict upload vars
+#       $fb_upload, $fb_upload_errstr,
+    );
+
+    $head = $entity->head;
+    $head->unfold;
+
+    $$rv = 1;  # default dequeue
+
+    # Parse email for lj specific info
+    ($user, $pin) = split(/\+/, $to);
+    ($user, $journal) = split(/\./, $user) if $user =~ /\./;
+    $u = LJ::load_user($user);
+    return unless $u && $u->is_visible;
+
+    # Pick what address to send potential errors to.
+    $addrlist = LJ::Emailpost::Web::get_allowed_senders( $u );
+    $from = ${(Mail::Address->parse( $head->get('From:') ))[0] || []}[1];
+    return unless $from;
+    my $err_addr;
+    foreach (keys %$addrlist) {
+        if (lc($from) eq lc &&
+                $addrlist->{$_}->{'get_errors'}) {
+            $err_addr = $from;
+            last;
+        }
+    }
+
+    my $err = sub {
+        my ($msg, $opt) = @_;
+
+        my $errbody;
+        $errbody .= "There was an error during your email posting:\n\n";
+        $errbody .= $msg;
+        if ($body) {
+            $errbody .= "\n\n\nOriginal posting follows:\n\n";
+            $errbody .= $body;
+        }
+
+        # Rate limit email to 1/5min/address
+        if (! $opt->{nomail} && ! $opt->{retry} && $err_addr &&
+            LJ::MemCache::add("rate_eperr:$err_addr", 5, 300)) {
+            LJ::send_mail({
+                    'to' => $err_addr,
+                    'from' => $LJ::BOGUS_EMAIL,
+                    'fromname' => "$LJ::SITENAME Error",
+                    'subject' => "$LJ::SITENAME posting error: $subject",
+                    'body' => $errbody
+                    });
+        }
+        $$rv = 0 if $opt->{'retry'};
+
+        $opt->{m} = $msg;
+        $opt->{s} = $subject;
+        $opt->{e} = 1;
+        dblog( $u, $opt ) unless $opt->{nolog};
+        return $msg;
+    };
+
+    # The return path should normally not ever be perverted enough to require this,
+    # but some mailers nowadays do some very strange things.
+    $return_path = ${(Mail::Address->parse( $head->get('Return-Path') ))[0] || []}[1];
+
+    # Use text/plain piece first - if it doesn't exist, then fallback to text/html
+    $tent = get_entity( $entity );
+    $tent = get_entity( $entity, 'html' ) unless $tent;
+
+    $body = $tent ? $tent->bodyhandle->as_string : "";
+    $body =~ s/^\s+//;
+    $body =~ s/\s+$//;
+
+    # Snag charset and do utf-8 conversion
+    my $content_type = $tent->head->get('Content-type:');
+    $charset = $1 if $content_type =~ /\bcharset=['\"]?(\S+?)['\"]?[\s\;]/i;
+    $format = $1 if $content_type =~ /\bformat=['\"]?(\S+?)['\"]?[\s\;]/i;
+    my $delsp;
+    $delsp = $1 if $content_type =~ /\bdelsp=['\"]?(\w+?)['\"]?[\s\;]/i;
+
+    if (defined($charset) && $charset !~ /^UTF-?8$/i) { # no charset? assume us-ascii
+        return $err->("Unknown charset encoding type. ($charset)")
+            unless Unicode::MapUTF8::utf8_supported_charset($charset);
+        $body = Unicode::MapUTF8::to_utf8({-string=>$body, -charset=>$charset});
+    }
+
+    # check subject for rfc-1521 junk
+    $subject ||= $head->get('Subject:');
+    chomp $subject;
+    if ($subject =~ /^=\?/) {
+        my @subj_data = MIME::Words::decode_mimewords( $subject );
+        my ( $string, $charset ) = ( $subj_data[0][0], $subj_data[0][1] );
+        if (@subj_data) {
+            if ($subject =~ /utf-8/i) {
+                $subject = $string;
+            } else {
+                return $err->("Unknown subject charset encoding type. ($charset)")
+                    unless $charset && Unicode::MapUTF8::utf8_supported_charset($charset);
+
+                $subject = Unicode::MapUTF8::to_utf8({
+                    -string  => $string,
+                    -charset => $charset,
+                });
+            }
+        }
+    }
+
+    # Strip (and maybe use) pin data from viewable areas
+    if ($subject =~ s/^\s*\+([a-z0-9]+)\b//i) {
+        $pin = $1 unless defined $pin;
+    }
+
+    if ($body =~ s/^\s*\+([a-z0-9]+)\b//i) {
+        $pin = $1 unless defined $pin;
+    }
+
+    # Validity checks.  We only care about these if they aren't using PGP.
+    unless (lc($pin) eq 'pgp' && $LJ::USE_PGP) {
+        return $err->("No allowed senders have been saved for your account.", { nomail => 1 }) unless
+            ref $addrlist && keys %$addrlist;
+
+        # don't mail user due to bounce spam
+        return $err->("Unauthorized sender address: $from")
+            unless grep { lc($from) eq lc($_) } keys %$addrlist;
+
+        return $err->("Unable to locate your PIN.") unless $pin;
+        return $err->("Invalid PIN.")
+            unless lc( $pin ) eq lc( $u->prop( 'emailpost_pin' ) );
+    }
+
+    return $err->("Email gateway access denied for your account type.")
+        unless $LJ::T_ALLOW_EMAILPOST || $u->can_emailpost;
+
+    # Is this message from a sprint PCS phone?  Sprint doesn't support
+    # MMS (yet) - when it does, we should just be able to rip this block
+    # of code completely out.
+    #
+    # Sprint has two methods of non-mms mail sending.
+    #   -  Normal text messaging just sends a text/plain piece.
+    #   -  Sprint "PictureMail".
+    # PictureMail sends a text/html piece, that contains XML with
+    # the location of the image on their servers - and a text/plain as well.
+    # (The text/plain used to be blank, now it's really text/plain.  We still
+    # can't use it, however, without heavy and fragile parsing.)
+    # We assume the existence of a text/html means this is a PictureMail message,
+    # as there is no other method (headers or otherwise) to tell the difference,
+    # and Sprint tells me that their text messaging never contains text/html.
+    # Currently, PictureMail can only contain one image per message
+    # and the image is always a jpeg. (2/2/05)
+    if ($return_path =~ /(?:messaging|pm)\.sprint(?:pcs)?\.com/ &&
+        $content_type =~ m#^multipart/alternative#i) {
+
+        $tent = get_entity( $entity, 'html' );
+
+        return $err->("Unable to find Sprint HTML content in PictureMail message.") unless $tent;
+
+        # ok, parse the XML.
+        my $html = $tent->bodyhandle->as_string();
+        my $xml_string;
+        $xml_string = $1 if $html =~ /<!-- lsPictureMail-Share-\w+-comment\n(.+)\n-->/is;
+        return $err->(
+            "Unable to find XML content in PictureMail message.",
+          ) unless $xml_string;
+
+        HTML::Entities::decode_entities( $xml_string );
+        my $xml = eval { XML::Simple::XMLin( $xml_string ); };
+        return $err->(
+            "Unable to parse XML content in PictureMail message.",
+          ) if ( ! $xml || $@ );
+
+        return $err->(
+            "Sorry, we currently only support image media.",
+          ) unless $xml->{messageContents}->{type} eq 'PICTURE';
+
+        my $url =
+          HTML::Entities::decode_entities(
+            $xml->{messageContents}->{mediaItems}->{mediaItem}->{url} );
+        $url = LJ::trim($url);
+        $url =~ s#</?url>##g;
+
+        return $err->(
+            "Invalid remote SprintPCS URL.",
+          ) unless $url =~ m#^http://pictures.sprintpcs.com/#;
+
+        # we've got the url to the full sized image.
+        # fetch!
+        my ($tmpdir, $tempfile);
+        $tmpdir = File::Temp::tempdir( "ljmailgate_" . 'X' x 20, DIR=> $workdir );
+        ( undef, $tempfile ) = File::Temp::tempfile(
+            'sprintpcs_XXXXX',
+            SUFFIX => '.jpg',
+            OPEN   => 0,
+            DIR    => $tmpdir
+        );
+        my $ua = LJ::get_useragent(
+                                   role => 'emailgateway',
+                                   timeout => 20,
+                                   );
+
+        $ua->agent("Mozilla");
+
+        my $ua_rv = $ua->get( $url, ':content_file' => $tempfile );
+
+        $body = $xml->{messageContents}->{messageText};
+        $body = ref $body ? "" : HTML::Entities::decode( $body );
+
+        if ($ua_rv->is_success) {
+            # (re)create a basic mime entity, so the rest of the
+            # emailgateway can function without modifications.
+            # (We don't need anything but Data, the other parts have
+            # already been pulled from $head->unfold)
+            $subject = 'Picture Post';
+            $entity = MIME::Entity->build( Data => $body );
+            $entity->attach(
+                Path => $tempfile,
+                Type => 'image/jpeg'
+            );
+        }
+        else {
+            # Retry if we are unable to connect to the remote server.
+            # Otherwise, the image has probably expired.  Dequeue.
+            my $reason = $ua_rv->status_line;
+            return $err->(
+                "Unable to fetch SprintPCS image. ($reason)",
+                {
+                    retry => $reason =~ /Connection refused/
+                }
+            );
+        }
+    }
+
+    # tmobile hell.
+    # if there is a message, then they send text/plain and text/html,
+    # with a slew of their tmobile specific images.  If no message
+    # is attached, there is no text/plain piece, and the journal is
+    # polluted with their advertising.  (The tmobile images (both good
+    # and junk) are posted to scrapbook either way.)
+    # gross.  do our best to strip out the nasty stuff.
+    if ($return_path && $return_path =~ /tmomail\.net$/) {
+        # if we aren't using their text/plain, then it's just
+        # advertising, and nothing else.  kill it.
+        $body = "" if $tent->effective_type eq 'text/html';
+
+        # t-mobile has a variety of different file names, so we can't just allow "good"
+        # files through; rather, we can just strip out the bad filenames.
+        my @imgs;
+        foreach my $img ( get_entity($entity, 'image') ) {
+            my $path = $img->bodyhandle->path;
+            $path =~ s!.*/!!;
+            next if $path =~ /^dottedline(350|600).gif$/;
+            next if $path =~ /^audio.gif$/;
+            next if $path =~ /^tmobilelogo.gif$/;
+            next if $path =~ /^tmobilespace.gif$/;
+            push @imgs, $img; # it's a good file if it made it this far.
+        }
+        $entity->parts(\@imgs);
+    }
+
+    # alltel. similar logic to t-mobile.
+    if ($return_path && $return_path =~ /mms\.alltel\.net$/) {
+        my @imgs;
+        foreach my $img ( get_entity($entity, 'image') ) {
+            my $path = $img->bodyhandle->path;
+            $path =~ s!.*/!!;
+            next if $path =~ /^divider\.gif$/;
+            next if $path =~ /^spacer\.gif$/;
+            next if $path =~ /^bluebar\.gif$/;
+            next if $path =~ /^header\.gif$/;
+            next if $path =~ /^greenbar\.gif$/;
+            next if $path =~ /^alltel_logo\.jpg$/;
+
+            push @imgs, $img; # it's a good file if it made it this far.
+        }
+        $entity->parts(\@imgs);
+    }
+
+    # verizon crap.  remove paragraphs of text.
+    $body =~ s/This message was sent using.+?Verizon.+?faster download\.//s;
+
+    # virgin mobile adds text to the *top* of the message, killing post-headers.
+    # Kill this silly (and grammatically incorrect) string.
+    if ($return_path && $return_path =~ /vmpix\.com$/) {
+        $body =~ s/^This is an? MMS message\.\s+//ms;
+    }
+
+    # UK service 'O2' does some bizarre stuff.
+    # No concept of a subject - it uses the first 40 characters from the body,
+    # truncating the rest.  The first text/plain is all advertising.
+    # The text/plain titled 'smil.txt' is the actual body of the message.
+    if ($return_path && $return_path =~ /mediamessaging\.o2\.co\.uk$/) {
+        foreach my $ent ( get_entity($entity, '*') ) {
+            my $path = $ent->bodyhandle->path;
+            $path =~ s#.*/##;
+            if ( $path eq 'smil.txt' ) {
+                $body = $ent->bodyhandle->as_string();
+                last;
+            }
+        }
+        $subject = 'Picture Post';
+    }
+
+    # PGP signed mail?  We'll see about that.
+    if (lc($pin) eq 'pgp' && $LJ::USE_PGP) {
+        my %gpg_errcodes = ( # temp mapping until translation
+                'bad'         => "PGP signature found to be invalid.",
+                'no_key'      => "You don't have a PGP key uploaded.",
+                'bad_tmpdir'  => "Problem generating tempdir: Please try again.",
+                'invalid_key' => "Your PGP key is invalid.  Please upload a proper key.",
+                'not_signed'  => "You specified PGP verification, but your message isn't PGP signed!");
+        my $gpgerr;
+        my $gpgcode = LJ::Emailpost::check_sig($u, $entity, \$gpgerr);
+        unless ($gpgcode eq 'good') {
+            my $errstr = $gpg_errcodes{$gpgcode};
+            $errstr .= "\nGnuPG error output:\n$gpgerr\n" if $gpgerr;
+            return $err->($errstr);
+        }
+
+        # Strip pgp clearsigning and any extra text surrounding it
+        # This takes into account pgp 'dash escaping' and a possible lack of Hash: headers
+        $body =~ s/.*?^-----BEGIN PGP SIGNED MESSAGE-----(?:\n[^\n].*?\n\n|\n\n)//ms;
+        $body =~ s/-----BEGIN PGP SIGNATURE-----.+//s;
+    }
+
+    $body =~ s/^(?:\- )?[\-_]{2,}\s*\r?\n.*//ms; # trim sigs
+
+    # respect flowed text
+    if (lc($format) eq 'flowed') {
+        if ($delsp && lc($delsp) eq 'yes') {
+            $body =~ s/ \n//g;
+        } else {
+            $body =~ s/ \n/ /g;
+        }
+    }
+
+
+    # trim off excess whitespace (html cleaner converts to breaks)
+    $body =~ s/\n+$/\n/;
+
+    # Pull the Date: header details
+    my ( $ss, $mm, $hh, $day, $month, $year, $zone ) =
+            strptime( $head->get( 'Date:' ) );
+
+    # Find and set entry props.
+    my $props = {};
+    my (%post_headers, $amask);
+    # first look for old style lj headers
+    while ($body =~ s/^lj-(.+?):\s*(.+?)\n//is) {
+        $post_headers{lc($1)} = LJ::trim($2);
+    }
+    # next look for new style post headers
+    # so if both are specified, this value will be retained
+    while ($body =~ s/^post-(.+?):\s*(.+?)\n//is) {
+        $post_headers{lc($1)} = LJ::trim($2);
+    }
+    $body =~ s/^\s*//;
+
+    # If we had an lj/post-date pseudo header, override the real Date header
+    ( $ss, $mm, $hh, $day, $month, $year, $zone ) =
+        strptime( $post_headers{date} ) if $post_headers{date};
+
+    # TZ is parsed into seconds, we want something more like -0800
+    $zone = defined $zone ? sprintf( '%+05d', $zone / 36 ) : 'guess';
+
+    $u->preload_props(
+        qw/
+          emailpost_userpic emailpost_security
+          emailpost_comments emailpost_gallery
+          emailpost_imgsecurity /
+    );
+
+    # Get post options, using post-headers first, and falling back
+    # to user props.  If neither exist, the regular journal defaults
+    # are used.
+    $props->{taglist} = $post_headers{tags};
+    $props->{picture_keyword} = $post_headers{'userpic'} ||
+                                $post_headers{'icon'} ||
+                                $u->{'emailpost_userpic'};
+    if ( my $id = DW::Mood->mood_id( $post_headers{'mood'} ) ) {
+        $props->{current_moodid}   = $id;
+    } else {
+        $props->{current_mood}     = $post_headers{'mood'};
+    }
+    $props->{current_music}    = $post_headers{'music'};
+    $props->{current_location} = $post_headers{'location'};
+    $props->{opt_nocomments} = 1
+      if $post_headers{comments}    =~ /off/i
+      || $u->{'emailpost_comments'} =~ /off/i;
+    $props->{opt_noemail} = 1
+      if $post_headers{comments}    =~ /noemail/i
+      || $u->{'emailpost_comments'} =~ /noemail/i;
+
+    $post_headers{security} = lc($post_headers{security}) || $u->{'emailpost_security'};
+    if ( $post_headers{security} =~ /^(public|private|friends|access)$/ ) {
+        if ( $1 eq 'friends' or $1 eq 'access' ) {
+            $post_headers{security} = 'usemask';
+            $amask = 1;
+        }
+    } elsif ($post_headers{security}) { # Assume a friendgroup if unknown security mode.
+        # Get the mask for the requested friends group, or default to private.
+        my $group = $u->trust_groups( 'name' => $post_headers{security} );
+        if ($group) {
+            $amask = (1 << $group->{groupnum});
+            $post_headers{security} = 'usemask';
+        } else {
+            $err->("Access group \"$post_headers{security}\" not found.  Your journal entry was posted privately.",
+                   { nolog => 1 });
+            $post_headers{security} = 'private';
+        }
+    }
+
+    # if they specified a imgsecurity header but it isn't valid, default
+    # to private.  Otherwise, set to what they specified.
+    $post_headers{'imgsecurity'} = lc($post_headers{'imgsecurity'}) ||
+                                   $u->{'emailpost_imgsecurity'}  || 'public';
+    $post_headers{'imgsecurity'} = 'private'
+      unless $post_headers{'imgsecurity'} =~ /^(private|regusers|friends|public)$/;
+
+    # upload picture attachments to fotobilder.
+    # undef return value? retry posting for later.
+#     $fb_upload = upload_images(
+#         $entity, $u,
+#         \$fb_upload_errstr,
+#         {
+#             imgsec  => $post_headers{'imgsecurity'},
+#             galname => $post_headers{'gallery'} || $u->{'emailpost_gallery'}
+#         }
+#       ) || return $err->( $fb_upload_errstr, { retry => 1 } );
+#
+#     # if we found and successfully uploaded some images...
+#     if (ref $fb_upload eq 'ARRAY') {
+#         my $fb_html = LJ::FBUpload::make_html( $u, $fb_upload, \%post_headers );
+#         ##
+#         ## A problem was here:
+#         ## $body is utf-8 text without utf-8 flag (see Unicode::MapUTF8::to_utf8),
+#         ## $fb_html is ASCII with utf-8 flag on (because uploaded image description
+#         ## is parsed by XML::Simple, see cgi-bin/fbupload.pl, line 153).
+#         ## When 2 strings are concatenated, $body is auto-converted (incorrectly)
+#         ## from Latin-1 to UTF-8.
+#         ##
+#         $fb_html = Encode::encode("utf8", $fb_html) if Encode::is_utf8($fb_html);
+#         $body .= $fb_html;
+#     }
+#
+#     # at this point, there are either no images in the message ($fb_upload == 1)
+#     # or we had some error during upload that we may or may not want to retry
+#     # from.  $fb_upload contains the http error code.
+#     if (   $fb_upload == 400   # bad http request
+#         || $fb_upload == 1401  # user has exceeded the fb quota
+#         || $fb_upload == 1402  # user has exceeded the fb quota
+#     ) {
+#         # don't retry these errors, go ahead and post the body
+#         # to the journal, postfixed with the remote error.
+#         $body .= "\n";
+#         $body .= "(Your picture was not posted: $fb_upload_errstr)";
+#     }
+#
+#     # Fotobilder server error.  Retry.
+#     return $err->( $fb_upload_errstr, { retry => 1 } ) if $fb_upload == 500;
+
+    # build lj entry
+    $req = {
+        usejournal  => $journal,
+        ver         => 1,
+        username    => $user,
+        event       => $body,
+        subject     => $subject,
+        security    => $post_headers{security},
+        allowmask   => $amask,
+        props       => $props,
+        tz          => $zone,
+        year        => $year + 1900,
+        mon         => $month + 1,
+        day         => $day,
+        hour        => $hh,
+        min         => $mm,
+    };
+
+    # post!
+    LJ::Protocol::do_request("postevent", $req, \$post_error, { noauth => 1 });
+    return $err->(LJ::Protocol::error_message($post_error)) if $post_error;
+
+    dblog( $u, { s => $subject } );
+    return "Post success";
+}
+
+# By default, returns first plain text entity from email message.
+# Specifying a type will return an array of MIME::Entity handles
+# of that type. (image, application, etc)
+# Specifying a type of 'all' will return all MIME::Entities,
+# regardless of type.
+sub get_entity
+{
+    my ($entity, $type) = @_;
+
+    # old arguments were a hashref
+    $type = $type->{'type'} if ref $type eq "HASH";
+
+    # default to text
+    $type ||= 'text';
+
+    my $head = $entity->head;
+    my $mime_type = $head->mime_type;
+
+    return $entity if $type eq 'text' && $mime_type eq "text/plain";
+    return $entity if $type eq 'html' && $mime_type eq "text/html";
+    my @entities;
+
+    # Only bother looking in messages that advertise attachments
+    my $mimeattach_re = qr{ m|^multipart/(?:alternative|signed|mixed|related)$| };
+    if ($mime_type =~ $mimeattach_re) {
+        my $partcount = $entity->parts;
+        for (my $i=0; $i<$partcount; $i++) {
+            my $alte = $entity->parts($i);
+
+            return $alte if $type eq 'text' && $alte->mime_type eq "text/plain";
+            return $alte if $type eq 'html' && $alte->mime_type eq "text/html";
+            push @entities, $alte if $type eq 'all';
+
+            if ($type eq 'image' &&
+                $alte->mime_type =~ m#^application/octet-stream#) {
+                my $alte_head = $alte->head;
+                my $filename = $alte_head->recommended_filename;
+                push @entities, $alte if $filename =~ /\.(?:gif|png|tiff?|jpe?g)$/;
+            }
+            push @entities, $alte if $alte->mime_type =~ /^$type/ &&
+                                     $type ne 'all';
+
+            # Recursively search through nested MIME for various pieces
+            if ($alte->mime_type =~ $mimeattach_re) {
+                if ($type =~ /^(?:text|html)$/) {
+                    my $text_entity = get_entity($entity->parts($i), $type);
+                    return $text_entity if $text_entity;
+                } else {
+                    push @entities, get_entity($entity->parts($i), $type);
+                }
+            }
+        }
+    }
+
+    return @entities if $type ne 'text' && scalar @entities;
+    return;
+}
+
+# Verifies an email pgp signature as being valid.
+# Returns codes so we can use the pre-existing err subref,
+# without passing everything all over the place.
+#
+# note that gpg interaction requires gpg version 1.2.4 or better.
+sub check_sig {
+    my ($u, $entity, $gpg_err) = @_;
+
+    my $key = LJ::isu( $u ) ? $u->prop( 'public_key' ) : undef;
+    return 'no_key' unless $key;
+
+    # Create work directory.
+    my $tmpdir = File::Temp::tempdir("ljmailgate_" . 'X' x 20, DIR=> $workdir);
+    return 'bad_tmpdir' unless -e $tmpdir;
+
+    my ($in, $out, $err, $status,
+        $gpg_handles, $gpg, $gpg_pid, $ret);
+
+    my $check = sub {
+        my %rets =
+            (
+             'NODATA 1'     => 1,   # no key or no signed data
+             'NODATA 2'     => 2,   # no signed content
+             'NODATA 3'     => 3,   # error checking sig (crc)
+             'IMPORT_RES 0' => 4,   # error importing key (crc)
+             'BADSIG'       => 5,   # good crc, bad sig
+             'GOODSIG'      => 6,   # all is well
+            );
+        while (my $gline = <$status>) {
+            foreach (keys %rets) {
+                next unless $gline =~ /($_)/;
+                return $rets{$1};
+            }
+        }
+        return 0;
+    };
+
+    my $gpg_cleanup = sub {
+        close $in;
+        close $out;
+        waitpid $gpg_pid, 0;
+        undef foreach $gpg, $gpg_handles;
+    };
+
+    my $gpg_pipe = sub {
+        $_ = IO::Handle->new() foreach $in, $out, $err, $status;
+        $gpg_handles = GnuPG::Handles->new( stdin  => $in,  stdout=> $out,
+                                            stderr => $err, status=> $status );
+        $gpg = GnuPG::Interface->new();
+        $gpg->options->hash_init( armor=>1, homedir=>$tmpdir );
+        $gpg->options->meta_interactive( 0 );
+    };
+
+    # Pull in user's key, add to keyring.
+    $gpg_pipe->();
+    $gpg_pid = $gpg->import_keys( handles=>$gpg_handles );
+    print $in $key;
+    $gpg_cleanup->();
+    $ret = $check->();
+    if ($ret && $ret == 1 || $ret == 4) {
+        $$gpg_err .= "    $_" while (<$err>);
+        return 'invalid_key';
+    }
+
+    my ($txt, $txt_f, $txt_e, $sig_e);
+    $txt_e = (get_entity($entity))[0];
+    return 'bad' unless $txt_e;
+
+    if ($entity->effective_type() eq 'multipart/signed') {
+        # attached signature
+        $sig_e = (get_entity($entity, 'application/pgp-signature'))[0];
+        $txt = $txt_e->as_string();
+        my $txt_fh;
+        ($txt_fh, $txt_f) =
+            File::Temp::tempfile('plaintext_XXXXXXXX', DIR => $tmpdir);
+        print $txt_fh $txt;
+        close $txt_fh;
+    } # otherwise, it's clearsigned
+
+    # Validate message.
+    # txt_e->bodyhandle->path() is clearsigned message in its entirety.
+    # txt_f is the ascii text that was signed (in the event of sig-as-attachment),
+    #     with MIME headers attached.
+    $gpg_pipe->();
+    $gpg_pid =
+        $gpg->wrap_call( handles => $gpg_handles,
+                         commands => [qw( --trust-model always --verify )],
+                         command_args => $sig_e ?
+                             [$sig_e->bodyhandle->path(), $txt_f] :
+                             $txt_e->bodyhandle->path()
+                    );
+    $gpg_cleanup->();
+    $ret = $check->();
+    if ($ret && $ret != 6) {
+        $$gpg_err .= "    $_" while (<$err>);
+        return 'bad' if $ret =~ /[35]/;
+        return 'not_signed' if $ret =~ /[12]/;
+    }
+
+    return 'good' if $ret == 6;
+    return undef;
+}
+
+# Upload images to a Fotobilder installation.
+# Return codes:
+# 1 - no images found in mime entity
+# undef - failure during upload
+# http_code - failure during upload w/ code
+# hashref - { title => url } for each image uploaded
+# sub upload_images
+# {
+#     my ($entity, $u, $rv, $opts) = @_;
+#     return 1 unless LJ::get_cap($u, 'fb_can_upload') && $LJ::FB_SITEROOT;
+#
+#     my @imgs = get_entity($entity, 'image');
+#     return 1 unless scalar @imgs;
+#
+#     my @images;
+#     foreach my $img_entity (@imgs) {
+#         my $img     = $img_entity->bodyhandle;
+#         my $path    = $img->path;
+#
+#         my $result = LJ::FBUpload::do_upload(
+#             $u, $rv,
+#             {
+#                 path    => $path,
+#                 rawdata => \$img->as_string,
+#                 imgsec  => $opts->{'imgsec'},
+#                 galname => $opts->{'galname'},
+#             }
+#         );
+#
+#         # do upload() returned undef?  This is a posting error
+#         # that should most likely be retried, due to something
+#         # wrong on our side of things.
+#         return if ! defined $result && $$rv;
+#
+#         # http error during upload attempt
+#         # decide retry based on error type in caller
+#         return $result unless ref $result;
+#
+#         # examine $result for errors
+#         if ($result->{Error}->{code}) {
+#             $$rv = $result->{Error}->{content};
+#
+#             # add 1000 to error code, so we can easily tell the
+#             # difference between fb protocol error and
+#             # http error when checking results.
+#             return $result->{Error}->{code} + 1000;
+#         }
+#
+#         push @images, {
+#             url     => $result->{URL},
+#             width   => $result->{Width},
+#             height  => $result->{Height},
+#             title   => $result->{Title},
+#         };
+#     }
+#
+#     return \@images if scalar @images;
+#     return;
+# }
+
+sub dblog
+{
+    my ( $u, $info ) = @_;
+    chomp $info->{s};
+    $u->log_event( 'emailpost', $info );
+    return;
+}
+
+1;
+
diff -r 9d6b0a0fc48c -r acfd5eeeffd6 cgi-bin/LJ/Emailpost/Web.pm
--- a/cgi-bin/LJ/Emailpost/Web.pm	Mon Oct 03 13:52:08 2011 +0800
+++ b/cgi-bin/LJ/Emailpost/Web.pm	Mon Oct 03 14:00:05 2011 +0800
@@ -12,8 +12,7 @@
 # part of this distribution.
 
 # these are the email gateway functions needed from web land.  they're
-# also available from ljemailgateway.pl (which contains the full
-# libraries)
+# also available from LJ/Emailpost.pm (which contains the full libraries)
 
 package LJ::Emailpost::Web;
 use strict;
diff -r 9d6b0a0fc48c -r acfd5eeeffd6 cgi-bin/ljemailgateway.pl
--- a/cgi-bin/ljemailgateway.pl	Mon Oct 03 13:52:08 2011 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,773 +0,0 @@
-#!/usr/bin/perl
-# This code was forked from the LiveJournal project owned and operated
-# by Live Journal, Inc. The code has been modified and expanded by
-# Dreamwidth Studios, LLC. These files were originally licensed under
-# the terms of the license supplied by Live Journal, Inc, which can
-# currently be found at:
-#
-# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
-#
-# In accordance with the original license, this code and all its
-# modifications are provided under the GNU General Public License.
-# A copy of that license can be found in the LICENSE file included as
-# part of this distribution.
-
-
-package LJ::Emailpost;
-use strict;
-use lib "$LJ::HOME/cgi-bin";
-use LJ::Config;
-
-my $workdir = "/tmp";
-
-BEGIN {
-    LJ::Config->load;
-    if ($LJ::USE_PGP) {
-        eval 'use GnuPG::Interface';
-        die "Could not load GnuPG::Interface." if $@;
-    }
-}
-
-require 'ljlib.pl';
-use LJ::Emailpost::Web;
-require 'ljprotocol.pl';
-use Date::Parse;
-use HTML::Entities;
-use IO::Handle;
-use MIME::Words ();
-use XML::Simple;
-use Unicode::MapUTF8 ();
-use Encode;
-
-# $entity -- MIME object
-# $to -- left part of email address.  either a username, or "username+PIN"
-# $rv - scalar ref from mailgated.
-# set to 1 to dequeue, 0 to leave for further processing.
-#
-sub process {
-    my ($entity, $to, $rv) = @_;
-
-    my (
-        # journal vars
-        $head, $user, $journal,
-        $pin, $u, $req, $post_error,
-
-        # email vars
-        $from, $addrlist, $return_path,
-        $body, $subject, $charset,
-        $format, $tent,
-
-        # pict upload vars
-#       $fb_upload, $fb_upload_errstr,
-    );
-
-    $head = $entity->head;
-    $head->unfold;
-
-    $$rv = 1;  # default dequeue
-
-    # Parse email for lj specific info
-    ($user, $pin) = split(/\+/, $to);
-    ($user, $journal) = split(/\./, $user) if $user =~ /\./;
-    $u = LJ::load_user($user);
-    return unless $u && $u->is_visible;
-
-    # Pick what address to send potential errors to.
-    $addrlist = LJ::Emailpost::Web::get_allowed_senders( $u );
-    $from = ${(Mail::Address->parse( $head->get('From:') ))[0] || []}[1];
-    return unless $from;
-    my $err_addr;
-    foreach (keys %$addrlist) {
-        if (lc($from) eq lc &&
-                $addrlist->{$_}->{'get_errors'}) {
-            $err_addr = $from;
-            last;
-        }
-    }
-
-    my $err = sub {
-        my ($msg, $opt) = @_;
-
-        my $errbody;
-        $errbody .= "There was an error during your email posting:\n\n";
-        $errbody .= $msg;
-        if ($body) {
-            $errbody .= "\n\n\nOriginal posting follows:\n\n";
-            $errbody .= $body;
-        }
-
-        # Rate limit email to 1/5min/address
-        if (! $opt->{nomail} && ! $opt->{retry} && $err_addr &&
-            LJ::MemCache::add("rate_eperr:$err_addr", 5, 300)) {
-            LJ::send_mail({
-                    'to' => $err_addr,
-                    'from' => $LJ::BOGUS_EMAIL,
-                    'fromname' => "$LJ::SITENAME Error",
-                    'subject' => "$LJ::SITENAME posting error: $subject",
-                    'body' => $errbody
-                    });
-        }
-        $$rv = 0 if $opt->{'retry'};
-
-        $opt->{m} = $msg;
-        $opt->{s} = $subject;
-        $opt->{e} = 1;
-        dblog( $u, $opt ) unless $opt->{nolog};
-        return $msg;
-    };
-
-    # The return path should normally not ever be perverted enough to require this,
-    # but some mailers nowadays do some very strange things.
-    $return_path = ${(Mail::Address->parse( $head->get('Return-Path') ))[0] || []}[1];
-
-    # Use text/plain piece first - if it doesn't exist, then fallback to text/html
-    $tent = get_entity( $entity );
-    $tent = get_entity( $entity, 'html' ) unless $tent;
-
-    $body = $tent ? $tent->bodyhandle->as_string : "";
-    $body =~ s/^\s+//;
-    $body =~ s/\s+$//;
-
-    # Snag charset and do utf-8 conversion
-    my $content_type = $tent->head->get('Content-type:');
-    $charset = $1 if $content_type =~ /\bcharset=['\"]?(\S+?)['\"]?[\s\;]/i;
-    $format = $1 if $content_type =~ /\bformat=['\"]?(\S+?)['\"]?[\s\;]/i;
-    my $delsp;
-    $delsp = $1 if $content_type =~ /\bdelsp=['\"]?(\w+?)['\"]?[\s\;]/i;
-
-    if (defined($charset) && $charset !~ /^UTF-?8$/i) { # no charset? assume us-ascii
-        return $err->("Unknown charset encoding type. ($charset)")
-            unless Unicode::MapUTF8::utf8_supported_charset($charset);
-        $body = Unicode::MapUTF8::to_utf8({-string=>$body, -charset=>$charset});
-    }
-
-    # check subject for rfc-1521 junk
-    $subject ||= $head->get('Subject:');
-    chomp $subject;
-    if ($subject =~ /^=\?/) {
-        my @subj_data = MIME::Words::decode_mimewords( $subject );
-        my ( $string, $charset ) = ( $subj_data[0][0], $subj_data[0][1] );
-        if (@subj_data) {
-            if ($subject =~ /utf-8/i) {
-                $subject = $string;
-            } else {
-                return $err->("Unknown subject charset encoding type. ($charset)")
-                    unless $charset && Unicode::MapUTF8::utf8_supported_charset($charset);
-
-                $subject = Unicode::MapUTF8::to_utf8({
-                    -string  => $string,
-                    -charset => $charset,
-                });
-            }
-        }
-    }
-
-    # Strip (and maybe use) pin data from viewable areas
-    if ($subject =~ s/^\s*\+([a-z0-9]+)\b//i) {
-        $pin = $1 unless defined $pin;
-    }
-
-    if ($body =~ s/^\s*\+([a-z0-9]+)\b//i) {
-        $pin = $1 unless defined $pin;
-    }
-
-    # Validity checks.  We only care about these if they aren't using PGP.
-    unless (lc($pin) eq 'pgp' && $LJ::USE_PGP) {
-        return $err->("No allowed senders have been saved for your account.", { nomail => 1 }) unless
-            ref $addrlist && keys %$addrlist;
-
-        # don't mail user due to bounce spam
-        return $err->("Unauthorized sender address: $from")
-            unless grep { lc($from) eq lc($_) } keys %$addrlist;
-
-        return $err->("Unable to locate your PIN.") unless $pin;
-        return $err->("Invalid PIN.")
-            unless lc( $pin ) eq lc( $u->prop( 'emailpost_pin' ) );
-    }
-
-    return $err->("Email gateway access denied for your account type.")
-        unless $LJ::T_ALLOW_EMAILPOST || $u->can_emailpost;
-
-    # Is this message from a sprint PCS phone?  Sprint doesn't support
-    # MMS (yet) - when it does, we should just be able to rip this block
-    # of code completely out.
-    #
-    # Sprint has two methods of non-mms mail sending.
-    #   -  Normal text messaging just sends a text/plain piece.
-    #   -  Sprint "PictureMail".
-    # PictureMail sends a text/html piece, that contains XML with
-    # the location of the image on their servers - and a text/plain as well.
-    # (The text/plain used to be blank, now it's really text/plain.  We still
-    # can't use it, however, without heavy and fragile parsing.)
-    # We assume the existence of a text/html means this is a PictureMail message,
-    # as there is no other method (headers or otherwise) to tell the difference,
-    # and Sprint tells me that their text messaging never contains text/html.
-    # Currently, PictureMail can only contain one image per message
-    # and the image is always a jpeg. (2/2/05)
-    if ($return_path =~ /(?:messaging|pm)\.sprint(?:pcs)?\.com/ &&
-        $content_type =~ m#^multipart/alternative#i) {
-
-        $tent = get_entity( $entity, 'html' );
-
-        return $err->("Unable to find Sprint HTML content in PictureMail message.") unless $tent;
-
-        # ok, parse the XML.
-        my $html = $tent->bodyhandle->as_string();
-        my $xml_string;
-        $xml_string = $1 if $html =~ /<!-- lsPictureMail-Share-\w+-comment\n(.+)\n-->/is;
-        return $err->(
-            "Unable to find XML content in PictureMail message.",
-          ) unless $xml_string;
-
-        HTML::Entities::decode_entities( $xml_string );
-        my $xml = eval { XML::Simple::XMLin( $xml_string ); };
-        return $err->(
-            "Unable to parse XML content in PictureMail message.",
-          ) if ( ! $xml || $@ );
-
-        return $err->(
-            "Sorry, we currently only support image media.",
-          ) unless $xml->{messageContents}->{type} eq 'PICTURE';
-
-        my $url =
-          HTML::Entities::decode_entities(
-            $xml->{messageContents}->{mediaItems}->{mediaItem}->{url} );
-        $url = LJ::trim($url);
-        $url =~ s#</?url>##g;
-
-        return $err->(
-            "Invalid remote SprintPCS URL.",
-          ) unless $url =~ m#^http://pictures.sprintpcs.com/#;
-
-        # we've got the url to the full sized image.
-        # fetch!
-        my ($tmpdir, $tempfile);
-        $tmpdir = File::Temp::tempdir( "ljmailgate_" . 'X' x 20, DIR=> $workdir );
-        ( undef, $tempfile ) = File::Temp::tempfile(
-            'sprintpcs_XXXXX',
-            SUFFIX => '.jpg',
-            OPEN   => 0,
-            DIR    => $tmpdir
-        );
-        my $ua = LJ::get_useragent(
-                                   role => 'emailgateway',
-                                   timeout => 20,
-                                   );
-
-        $ua->agent("Mozilla");
-
-        my $ua_rv = $ua->get( $url, ':content_file' => $tempfile );
-
-        $body = $xml->{messageContents}->{messageText};
-        $body = ref $body ? "" : HTML::Entities::decode( $body );
-
-        if ($ua_rv->is_success) {
-            # (re)create a basic mime entity, so the rest of the
-            # emailgateway can function without modifications.
-            # (We don't need anything but Data, the other parts have
-            # already been pulled from $head->unfold)
-            $subject = 'Picture Post';
-            $entity = MIME::Entity->build( Data => $body );
-            $entity->attach(
-                Path => $tempfile,
-                Type => 'image/jpeg'
-            );
-        }
-        else {
-            # Retry if we are unable to connect to the remote server.
-            # Otherwise, the image has probably expired.  Dequeue.
-            my $reason = $ua_rv->status_line;
-            return $err->(
-                "Unable to fetch SprintPCS image. ($reason)",
-                {
-                    retry => $reason =~ /Connection refused/
-                }
-            );
-        }
-    }
-
-    # tmobile hell.
-    # if there is a message, then they send text/plain and text/html,
-    # with a slew of their tmobile specific images.  If no message
-    # is attached, there is no text/plain piece, and the journal is
-    # polluted with their advertising.  (The tmobile images (both good
-    # and junk) are posted to scrapbook either way.)
-    # gross.  do our best to strip out the nasty stuff.
-    if ($return_path && $return_path =~ /tmomail\.net$/) {
-        # if we aren't using their text/plain, then it's just
-        # advertising, and nothing else.  kill it.
-        $body = "" if $tent->effective_type eq 'text/html';
-
-        # t-mobile has a variety of different file names, so we can't just allow "good"
-        # files through; rather, we can just strip out the bad filenames.
-        my @imgs;
-        foreach my $img ( get_entity($entity, 'image') ) {
-            my $path = $img->bodyhandle->path;
-            $path =~ s!.*/!!;
-            next if $path =~ /^dottedline(350|600).gif$/;
-            next if $path =~ /^audio.gif$/;
-            next if $path =~ /^tmobilelogo.gif$/;
-            next if $path =~ /^tmobilespace.gif$/;
-            push @imgs, $img; # it's a good file if it made it this far.
-        }
-        $entity->parts(\@imgs);
-    }
-
-    # alltel. similar logic to t-mobile.
-    if ($return_path && $return_path =~ /mms\.alltel\.net$/) {
-        my @imgs;
-        foreach my $img ( get_entity($entity, 'image') ) {
-            my $path = $img->bodyhandle->path;
-            $path =~ s!.*/!!;
-            next if $path =~ /^divider\.gif$/;
-            next if $path =~ /^spacer\.gif$/;
-            next if $path =~ /^bluebar\.gif$/;
-            next if $path =~ /^header\.gif$/;
-            next if $path =~ /^greenbar\.gif$/;
-            next if $path =~ /^alltel_logo\.jpg$/;
-
-            push @imgs, $img; # it's a good file if it made it this far.
-        }
-        $entity->parts(\@imgs);
-    }
-
-    # verizon crap.  remove paragraphs of text.
-    $body =~ s/This message was sent using.+?Verizon.+?faster download\.//s;
-
-    # virgin mobile adds text to the *top* of the message, killing post-headers.
-    # Kill this silly (and grammatically incorrect) string.
-    if ($return_path && $return_path =~ /vmpix\.com$/) {
-        $body =~ s/^This is an? MMS message\.\s+//ms;
-    }
-
-    # UK service 'O2' does some bizarre stuff.
-    # No concept of a subject - it uses the first 40 characters from the body,
-    # truncating the rest.  The first text/plain is all advertising.
-    # The text/plain titled 'smil.txt' is the actual body of the message.
-    if ($return_path && $return_path =~ /mediamessaging\.o2\.co\.uk$/) {
-        foreach my $ent ( get_entity($entity, '*') ) {
-            my $path = $ent->bodyhandle->path;
-            $path =~ s#.*/##;
-            if ( $path eq 'smil.txt' ) {
-                $body = $ent->bodyhandle->as_string();
-                last;
-            }
-        }
-        $subject = 'Picture Post';
-    }
-
-    # PGP signed mail?  We'll see about that.
-    if (lc($pin) eq 'pgp' && $LJ::USE_PGP) {
-        my %gpg_errcodes = ( # temp mapping until translation
-                'bad'         => "PGP signature found to be invalid.",
-                'no_key'      => "You don't have a PGP key uploaded.",
-                'bad_tmpdir'  => "Problem generating tempdir: Please try again.",
-                'invalid_key' => "Your PGP key is invalid.  Please upload a proper key.",
-                'not_signed'  => "You specified PGP verification, but your message isn't PGP signed!");
-        my $gpgerr;
-        my $gpgcode = LJ::Emailpost::check_sig($u, $entity, \$gpgerr);
-        unless ($gpgcode eq 'good') {
-            my $errstr = $gpg_errcodes{$gpgcode};
-            $errstr .= "\nGnuPG error output:\n$gpgerr\n" if $gpgerr;
-            return $err->($errstr);
-        }
-
-        # Strip pgp clearsigning and any extra text surrounding it
-        # This takes into account pgp 'dash escaping' and a possible lack of Hash: headers
-        $body =~ s/.*?^-----BEGIN PGP SIGNED MESSAGE-----(?:\n[^\n].*?\n\n|\n\n)//ms;
-        $body =~ s/-----BEGIN PGP SIGNATURE-----.+//s;
-    }
-
-    $body =~ s/^(?:\- )?[\-_]{2,}\s*\r?\n.*//ms; # trim sigs
-
-    # respect flowed text
-    if (lc($format) eq 'flowed') {
-        if ($delsp && lc($delsp) eq 'yes') {
-            $body =~ s/ \n//g;
-        } else {
-            $body =~ s/ \n/ /g;
-        }
-    }
-
-
-    # trim off excess whitespace (html cleaner converts to breaks)
-    $body =~ s/\n+$/\n/;
-
-    # Pull the Date: header details
-    my ( $ss, $mm, $hh, $day, $month, $year, $zone ) =
-            strptime( $head->get( 'Date:' ) );
-
-    # Find and set entry props.
-    my $props = {};
-    my (%post_headers, $amask);
-    # first look for old style lj headers
-    while ($body =~ s/^lj-(.+?):\s*(.+?)\n//is) {
-        $post_headers{lc($1)} = LJ::trim($2);
-    }
-    # next look for new style post headers
-    # so if both are specified, this value will be retained
-    while ($body =~ s/^post-(.+?):\s*(.+?)\n//is) {
-        $post_headers{lc($1)} = LJ::trim($2);
-    }
-    $body =~ s/^\s*//;
-
-    # If we had an lj/post-date pseudo header, override the real Date header
-    ( $ss, $mm, $hh, $day, $month, $year, $zone ) =
-        strptime( $post_headers{date} ) if $post_headers{date};
-
-    # TZ is parsed into seconds, we want something more like -0800
-    $zone = defined $zone ? sprintf( '%+05d', $zone / 36 ) : 'guess';
-
-    $u->preload_props(
-        qw/
-          emailpost_userpic emailpost_security
-          emailpost_comments emailpost_gallery
-          emailpost_imgsecurity /
-    );
-
-    # Get post options, using post-headers first, and falling back
-    # to user props.  If neither exist, the regular journal defaults
-    # are used.
-    $props->{taglist} = $post_headers{tags};
-    $props->{picture_keyword} = $post_headers{'userpic'} ||
-                                $post_headers{'icon'} ||
-                                $u->{'emailpost_userpic'};
-    if ( my $id = DW::Mood->mood_id( $post_headers{'mood'} ) ) {
-        $props->{current_moodid}   = $id;
-    } else {
-        $props->{current_mood}     = $post_headers{'mood'};
-    }
-    $props->{current_music}    = $post_headers{'music'};
-    $props->{current_location} = $post_headers{'location'};
-    $props->{opt_nocomments} = 1
-      if $post_headers{comments}    =~ /off/i
-      || $u->{'emailpost_comments'} =~ /off/i;
-    $props->{opt_noemail} = 1
-      if $post_headers{comments}    =~ /noemail/i
-      || $u->{'emailpost_comments'} =~ /noemail/i;
-
-    $post_headers{security} = lc($post_headers{security}) || $u->{'emailpost_security'};
-    if ( $post_headers{security} =~ /^(public|private|friends|access)$/ ) {
-        if ( $1 eq 'friends' or $1 eq 'access' ) {
-            $post_headers{security} = 'usemask';
-            $amask = 1;
-        }
-    } elsif ($post_headers{security}) { # Assume a friendgroup if unknown security mode.
-        # Get the mask for the requested friends group, or default to private.
-        my $group = $u->trust_groups( 'name' => $post_headers{security} );
-        if ($group) {
-            $amask = (1 << $group->{groupnum});
-            $post_headers{security} = 'usemask';
-        } else {
-            $err->("Access group \"$post_headers{security}\" not found.  Your journal entry was posted privately.",
-                   { nolog => 1 });
-            $post_headers{security} = 'private';
-        }
-    }
-
-    # if they specified a imgsecurity header but it isn't valid, default
-    # to private.  Otherwise, set to what they specified.
-    $post_headers{'imgsecurity'} = lc($post_headers{'imgsecurity'}) ||
-                                   $u->{'emailpost_imgsecurity'}  || 'public';
-    $post_headers{'imgsecurity'} = 'private'
-      unless $post_headers{'imgsecurity'} =~ /^(private|regusers|friends|public)$/;
-
-    # upload picture attachments to fotobilder.
-    # undef return value? retry posting for later.
-#     $fb_upload = upload_images(
-#         $entity, $u,
-#         \$fb_upload_errstr,
-#         {
-#             imgsec  => $post_headers{'imgsecurity'},
-#             galname => $post_headers{'gallery'} || $u->{'emailpost_gallery'}
-#         }
-#       ) || return $err->( $fb_upload_errstr, { retry => 1 } );
-#
-#     # if we found and successfully uploaded some images...
-#     if (ref $fb_upload eq 'ARRAY') {
-#         my $fb_html = LJ::FBUpload::make_html( $u, $fb_upload, \%post_headers );
-#         ##
-#         ## A problem was here:
-#         ## $body is utf-8 text without utf-8 flag (see Unicode::MapUTF8::to_utf8),
-#         ## $fb_html is ASCII with utf-8 flag on (because uploaded image description
-#         ## is parsed by XML::Simple, see cgi-bin/fbupload.pl, line 153).
-#         ## When 2 strings are concatenated, $body is auto-converted (incorrectly)
-#         ## from Latin-1 to UTF-8.
-#         ##
-#         $fb_html = Encode::encode("utf8", $fb_html) if Encode::is_utf8($fb_html);
-#         $body .= $fb_html;
-#     }
-#
-#     # at this point, there are either no images in the message ($fb_upload == 1)
-#     # or we had some error during upload that we may or may not want to retry
-#     # from.  $fb_upload contains the http error code.
-#     if (   $fb_upload == 400   # bad http request
-#         || $fb_upload == 1401  # user has exceeded the fb quota
-#         || $fb_upload == 1402  # user has exceeded the fb quota
-#     ) {
-#         # don't retry these errors, go ahead and post the body
-#         # to the journal, postfixed with the remote error.
-#         $body .= "\n";
-#         $body .= "(Your picture was not posted: $fb_upload_errstr)";
-#     }
-#
-#     # Fotobilder server error.  Retry.
-#     return $err->( $fb_upload_errstr, { retry => 1 } ) if $fb_upload == 500;
-
-    # build lj entry
-    $req = {
-        usejournal  => $journal,
-        ver         => 1,
-        username    => $user,
-        event       => $body,
-        subject     => $subject,
-        security    => $post_headers{security},
-        allowmask   => $amask,
-        props       => $props,
-        tz          => $zone,
-        year        => $year + 1900,
-        mon         => $month + 1,
-        day         => $day,
-        hour        => $hh,
-        min         => $mm,
-    };
-
-    # post!
-    LJ::Protocol::do_request("postevent", $req, \$post_error, { noauth => 1 });
-    return $err->(LJ::Protocol::error_message($post_error)) if $post_error;
-
-    dblog( $u, { s => $subject } );
-    return "Post success";
-}
-
-# By default, returns first plain text entity from email message.
-# Specifying a type will return an array of MIME::Entity handles
-# of that type. (image, application, etc)
-# Specifying a type of 'all' will return all MIME::Entities,
-# regardless of type.
-sub get_entity
-{
-    my ($entity, $type) = @_;
-
-    # old arguments were a hashref
-    $type = $type->{'type'} if ref $type eq "HASH";
-
-    # default to text
-    $type ||= 'text';
-
-    my $head = $entity->head;
-    my $mime_type = $head->mime_type;
-
-    return $entity if $type eq 'text' && $mime_type eq "text/plain";
-    return $entity if $type eq 'html' && $mime_type eq "text/html";
-    my @entities;
-
-    # Only bother looking in messages that advertise attachments
-    my $mimeattach_re = qr{ m|^multipart/(?:alternative|signed|mixed|related)$| };
-    if ($mime_type =~ $mimeattach_re) {
-        my $partcount = $entity->parts;
-        for (my $i=0; $i<$partcount; $i++) {
-            my $alte = $entity->parts($i);
-
-            return $alte if $type eq 'text' && $alte->mime_type eq "text/plain";
-            return $alte if $type eq 'html' && $alte->mime_type eq "text/html";
-            push @entities, $alte if $type eq 'all';
-
-            if ($type eq 'image' &&
-                $alte->mime_type =~ m#^application/octet-stream#) {
-                my $alte_head = $alte->head;
-                my $filename = $alte_head->recommended_filename;
-                push @entities, $alte if $filename =~ /\.(?:gif|png|tiff?|jpe?g)$/;
-            }
-            push @entities, $alte if $alte->mime_type =~ /^$type/ &&
-                                     $type ne 'all';
-
-            # Recursively search through nested MIME for various pieces
-            if ($alte->mime_type =~ $mimeattach_re) {
-                if ($type =~ /^(?:text|html)$/) {
-                    my $text_entity = get_entity($entity->parts($i), $type);
-                    return $text_entity if $text_entity;
-                } else {
-                    push @entities, get_entity($entity->parts($i), $type);
-                }
-            }
-        }
-    }
-
-    return @entities if $type ne 'text' && scalar @entities;
-    return;
-}
-
-# Verifies an email pgp signature as being valid.
-# Returns codes so we can use the pre-existing err subref,
-# without passing everything all over the place.
-#
-# note that gpg interaction requires gpg version 1.2.4 or better.
-sub check_sig {
-    my ($u, $entity, $gpg_err) = @_;
-
-    my $key = LJ::isu( $u ) ? $u->prop( 'public_key' ) : undef;
-    return 'no_key' unless $key;
-
-    # Create work directory.
-    my $tmpdir = File::Temp::tempdir("ljmailgate_" . 'X' x 20, DIR=> $workdir);
-    return 'bad_tmpdir' unless -e $tmpdir;
-
-    my ($in, $out, $err, $status,
-        $gpg_handles, $gpg, $gpg_pid, $ret);
-
-    my $check = sub {
-        my %rets =
-            (
-             'NODATA 1'     => 1,   # no key or no signed data
-             'NODATA 2'     => 2,   # no signed content
-             'NODATA 3'     => 3,   # error checking sig (crc)
-             'IMPORT_RES 0' => 4,   # error importing key (crc)
-             'BADSIG'       => 5,   # good crc, bad sig
-             'GOODSIG'      => 6,   # all is well
-            );
-        while (my $gline = <$status>) {
-            foreach (keys %rets) {
-                next unless $gline =~ /($_)/;
-                return $rets{$1};
-            }
-        }
-        return 0;
-    };
-
-    my $gpg_cleanup = sub {
-        close $in;
-        close $out;
-        waitpid $gpg_pid, 0;
-        undef foreach $gpg, $gpg_handles;
-    };
-
-    my $gpg_pipe = sub {
-        $_ = IO::Handle->new() foreach $in, $out, $err, $status;
-        $gpg_handles = GnuPG::Handles->new( stdin  => $in,  stdout=> $out,
-                                            stderr => $err, status=> $status );
-        $gpg = GnuPG::Interface->new();
-        $gpg->options->hash_init( armor=>1, homedir=>$tmpdir );
-        $gpg->options->meta_interactive( 0 );
-    };
-
-    # Pull in user's key, add to keyring.
-    $gpg_pipe->();
-    $gpg_pid = $gpg->import_keys( handles=>$gpg_handles );
-    print $in $key;
-    $gpg_cleanup->();
-    $ret = $check->();
-    if ($ret && $ret == 1 || $ret == 4) {
-        $$gpg_err .= "    $_" while (<$err>);
-        return 'invalid_key';
-    }
-
-    my ($txt, $txt_f, $txt_e, $sig_e);
-    $txt_e = (get_entity($entity))[0];
-    return 'bad' unless $txt_e;
-
-    if ($entity->effective_type() eq 'multipart/signed') {
-        # attached signature
-        $sig_e = (get_entity($entity, 'application/pgp-signature'))[0];
-        $txt = $txt_e->as_string();
-        my $txt_fh;
-        ($txt_fh, $txt_f) =
-            File::Temp::tempfile('plaintext_XXXXXXXX', DIR => $tmpdir);
-        print $txt_fh $txt;
-        close $txt_fh;
-    } # otherwise, it's clearsigned
-
-    # Validate message.
-    # txt_e->bodyhandle->path() is clearsigned message in its entirety.
-    # txt_f is the ascii text that was signed (in the event of sig-as-attachment),
-    #     with MIME headers attached.
-    $gpg_pipe->();
-    $gpg_pid =
-        $gpg->wrap_call( handles => $gpg_handles,
-                         commands => [qw( --trust-model always --verify )],
-                         command_args => $sig_e ?
-                             [$sig_e->bodyhandle->path(), $txt_f] :
-                             $txt_e->bodyhandle->path()
-                    );
-    $gpg_cleanup->();
-    $ret = $check->();
-    if ($ret && $ret != 6) {
-        $$gpg_err .= "    $_" while (<$err>);
-        return 'bad' if $ret =~ /[35]/;
-        return 'not_signed' if $ret =~ /[12]/;
-    }
-
-    return 'good' if $ret == 6;
-    return undef;
-}
-
-# Upload images to a Fotobilder installation.
-# Return codes:
-# 1 - no images found in mime entity
-# undef - failure during upload
-# http_code - failure during upload w/ code
-# hashref - { title => url } for each image uploaded
-# sub upload_images
-# {
-#     my ($entity, $u, $rv, $opts) = @_;
-#     return 1 unless LJ::get_cap($u, 'fb_can_upload') && $LJ::FB_SITEROOT;
-#
-#     my @imgs = get_entity($entity, 'image');
-#     return 1 unless scalar @imgs;
-#
-#     my @images;
-#     foreach my $img_entity (@imgs) {
-#         my $img     = $img_entity->bodyhandle;
-#         my $path    = $img->path;
-#
-#         my $result = LJ::FBUpload::do_upload(
-#             $u, $rv,
-#             {
-#                 path    => $path,
-#                 rawdata => \$img->as_string,
-#                 imgsec  => $opts->{'imgsec'},
-#                 galname => $opts->{'galname'},
-#             }
-#         );
-#
-#         # do upload() returned undef?  This is a posting error
-#         # that should most likely be retried, due to something
-#         # wrong on our side of things.
-#         return if ! defined $result && $$rv;
-#
-#         # http error during upload attempt
-#         # decide retry based on error type in caller
-#         return $result unless ref $result;
-#
-#         # examine $result for errors
-#         if ($result->{Error}->{code}) {
-#             $$rv = $result->{Error}->{content};
-#
-#             # add 1000 to error code, so we can easily tell the
-#             # difference between fb protocol error and
-#             # http error when checking results.
-#             return $result->{Error}->{code} + 1000;
-#         }
-#
-#         push @images, {
-#             url     => $result->{URL},
-#             width   => $result->{Width},
-#             height  => $result->{Height},
-#             title   => $result->{Title},
-#         };
-#     }
-#
-#     return \@images if scalar @images;
-#     return;
-# }
-
-sub dblog
-{
-    my ( $u, $info ) = @_;
-    chomp $info->{s};
-    $u->log_event( 'emailpost', $info );
-    return;
-}
-
-1;
-
diff -r 9d6b0a0fc48c -r acfd5eeeffd6 t/emailpost.t
--- a/t/emailpost.t	Mon Oct 03 13:52:08 2011 +0800
+++ b/t/emailpost.t	Mon Oct 03 14:00:05 2011 +0800
@@ -4,8 +4,7 @@
 use Test::More tests => 13;
 use lib "$ENV{LJHOME}/cgi-bin";
 require 'ljlib.pl';
-use LJ::Emailpost::Web;
-require 'ljemailgateway.pl';
+use LJ::Emailpost;  # includes LJ::Emailpost::Web
 use LJ::Test;
 use FindBin qw($Bin);
 use File::Temp;
--------------------------------------------------------------------------------