#!/usr/bin/perl # # Authors: # Afuna # # Copyright (c) 2013-2018 by Dreamwidth Studios, LLC. # # This code is a refactoring and extension of code originally forked # from the LiveJournal project owned and operated by Live Journal, Inc. # The code has been refactored, 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. # # This file is a refactoring of "cgi-bin/LJ/Emailpost.pm" # from the original LiveJournal repository package DW::EmailPost::Entry; use base qw(DW::EmailPost::Base); use strict; use LJ::Protocol; use Date::Parse; use IO::Handle; use XML::Simple; use DW::Media; my $workdir = "/tmp"; =head1 NAME DW::EmailPost::Entry - Handle entries posted through email =cut sub _find_destination { my ( $class, @to_addresses ) = @_; foreach my $dest (@to_addresses) { next unless $dest =~ /^(\S+?)\@\Q$LJ::EMAIL_POST_DOMAIN\E$/i; return $1; } return; } sub _parse_destination { my ( $self, $auth_string ) = @_; # user and journal my ( $user, $journal, $pin ); # ignore pin, handle it later ( $user, $pin ) = split /\+/, $auth_string; ( $user, $journal ) = split /\./, $user if $user =~ /\./; $self->{u} = LJ::load_user($user); return unless $self->{u}; $self->{journal} = $journal || $self->{u}->user; return 1; } sub _process { my $self = $_[0]; $self->_extract_pin; $self->_check_pin_validity or return $self->send_error; $self->_cleanup_mobile_carriers or return $self->send_error; # figure out what entryprops should be based on post headers in email # and user's defaults $self->_set_props( $self->{u}, $self->{email_date}, %{ $self->{post_headers} || {} } ) or return $self->send_error; # insert any images. # must be done after we've processed the props, to make sure we respect security settings $self->insert_images; # do a final cleanup of the body text $self->cleanup_body_final; # build the entry my $time = $self->{time}; my $req = { usejournal => $self->{journal}, ver => 1, username => $self->{u}->user, event => $self->{body}, subject => $self->{subject}, security => $self->{security}, allowmask => $self->{amask}, props => $self->{props}, tz => $time->{zone}, year => $time->{year} + 1900, mon => $time->{mon} + 1, day => $time->{day}, hour => $time->{hour}, min => $time->{min}, }; # post! my $post_error; LJ::Protocol::do_request( "postevent", $req, \$post_error, { noauth => 1, allow_truncated_subject => 1 } ); return $self->send_error( LJ::Protocol::error_message($post_error) ) if $post_error; $self->dblog( s => $self->{subject} ); return ( 1, "Post success" ); } sub _extract_pin { my $self = $_[0]; my ( undef, $pin ) = split /\+/, $self->{destination}; $self->{pin} = $pin; # Strip (and maybe use) pin data from viewable areas my $strip_pin = sub { my $textref = $_[0]; my $pin; if ( $$textref =~ s/^\s*\+([a-z0-9]+)\b//i ) { $pin = $1; } return $pin; }; $self->{pin} ||= $strip_pin->( \$self->{subject} ); $self->{pin} ||= $strip_pin->( \$self->{body} ); } sub _set_props { my ( $self, $u, $email_date, %post_headers ) = @_; my $props = {}; my $time = {}; # Pull the Date: header details my ( $ss, $mm, $hh, $day, $month, $year, $zone ) = strptime($email_date); # 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'; $time = { sec => $ss, min => $mm, hour => $hh, day => $day, mon => $month, year => $year, zone => $zone, }; $u->preload_props( qw/ emailpost_userpic emailpost_security emailpost_comments emailpost_gallery / ); # 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->{editor} = $self->_choose_editor( $post_headers{format} ); $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; if ( exists $post_headers{screenlevel} ) { if ( $post_headers{screenlevel} =~ /^all$/i ) { $props->{opt_screening} = 'A'; } elsif ( $post_headers{screenlevel} =~ /^untrusted$/i ) { $props->{opt_screening} = 'F'; } elsif ( $post_headers{screenlevel} =~ /^(anonymous|anon)$/i ) { $props->{opt_screening} = 'R'; # needs-Remote } elsif ( $post_headers{screenlevel} =~ /^(disabled|none)$/i ) { $props->{opt_screening} = 'N'; } elsif ( $post_headers{screenlevel} ne '' ) { $props->{opt_screening} = 'A'; $self->send_error( "Unrecognized screening keyword. Your entry was posted with all comments screened.", { nolog => 1 } ); } else { # blank $props->{opt_screening} = ''; # User default } } else { # unspecified $props->{opt_screening} = ''; # User default } my $security; my $amask; # "lc" is right here because groupnames are forcibly lowercased in # LJ::User->trust_groups; $security = lc $post_headers{security} || $u->emailpost_security; # FIXME: relies on emailpost_security ne 'usemask'? if ( $security =~ /^(public|private|friends|access)$/ ) { if ( $1 eq 'friends' or $1 eq 'access' ) { $security = 'usemask'; $amask = 1; } } elsif ($security) { # Assume a trust group list if unknown security. # Get the mask for the requested trust group list, discarding those # that don't exist. $amask = 0; my @unrecognized = (); foreach my $groupname ( split( /\s*,\s*/, $security ) ) { my $group = $u->trust_groups( name => $groupname ); if ($group) { $amask |= ( 1 << $group->{groupnum} ); } else { push @unrecognized, $groupname; } } $security = 'usemask'; if (@unrecognized) { # send the error, but not shortcircuiting the posting process # probably the only time that we call $self->send_error inside of a convenience sub my $unrecognized = join( ', ', @unrecognized ); $self->send_error( "Access group(s) \"$unrecognized\" not found. Your journal entry was posted to the other groups, or privately if no groups exist.", { nolog => 1 } ); } } $self->{props} = $props; $self->{security} = $security; $self->{amask} = $amask; $self->{time} = $time; return 1; } =head2 C<< $self->insert_images >> Take images from the email body and insert them into the entry =cut # could hypothetically be refactored out into Base.pm so that other subclasses could use # but you'd probably want to pass in the variables instead of referring to $self sub insert_images { my ($self) = @_; # upload picture attachments # undef return value? retry posting for later. my $fb_upload = $self->_upload_images( security => $self->{security}, allowmask => $self->{amask}, ); # if we found and successfully uploaded some images... if ( ref $fb_upload eq 'ARRAY' ) { my $fb_html = join( '
', map { '' } @$fb_upload ); ## ## 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); $self->{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. $self->{body} .= "\n"; $self->{body} .= "(Your picture was not posted)"; } } # 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 ( $self, %opts ) = @_; my @imgs = $self->get_entity( $self->{_entity}, 'image' ); return 1 unless scalar @imgs; return 1401 unless DW::Media->can_upload_media( $self->{u} ); # error code from insert_images my @images; foreach my $img_entity (@imgs) { my $obj = DW::Media->upload_media( user => $self->{u}, data => $img_entity->bodyhandle->as_string, %opts, # Should contain security. ); push @images, $obj if $obj; } return unless scalar @images; return \@images; } sub _check_pin_validity { my $self = $_[0]; my $from = $self->{from}; my $pin = $self->{pin}; my $u = $self->{u}; my $addrlist = LJ::Emailpost::Web::get_allowed_senders( $self->{u} ); unless ( ref $addrlist && keys %$addrlist ) { return $self->err( "No allowed senders have been saved for your account.", { nomail => 1 } ); return; } return $self->err("Unauthorized sender address: $from") unless grep { lc $from eq lc $_ } keys %$addrlist; return $self->err("Unable to locate your PIN.") unless $pin; return $self->err("Invalid PIN.") unless lc $pin eq lc $u->prop('emailpost_pin'); return 1; } sub _cleanup_mobile_carriers { my $self = $_[0]; # 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) my $return_path = $self->{return_path}; my $content_type = $self->{content_type}; my $tent = $self->{_tent}; if ( $return_path =~ /(?:messaging|pm)\.sprint(?:pcs)?\.com/ && $content_type->{"_orig"} =~ m#^multipart/alternative#i ) { $tent = $self->get_entity( $self->{_entity}, 'html' ); return $self->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 =~ //is; return $self->err("Unable to find XML content in PictureMail message.") unless $xml_string; LJ::dhtml($xml_string); # $xml_string is being modified by this function call # special characters are replaced with equivalent HTML entities my $xml = eval { XML::Simple::XMLin($xml_string); }; return $self->err("Unable to parse XML content in PictureMail message.") if !$xml || $@; return $self->err("Sorry, we currently only support image media.") unless $xml->{messageContents}->{type} eq 'PICTURE'; my $url = LJ::dhtml( $xml->{messageContents}->{mediaItems}->{mediaItem}->{url} ); $url = LJ::trim($url); $url =~ s###g; return $self->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 ); $self->{body} = $xml->{messageContents}->{messageText}; $self->{body} = ref $self->{body} ? "" : LJ::dhtml( $self->{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) $self->{subject} = 'Picture Post'; $self->{_entity} = MIME::Entity->build( Data => $self->{body} ); $self->{_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 $self->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. $self->{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 ( $self->get_entity( $self->{_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. } $self->{entity}->parts( \@imgs ); } # alltel. similar logic to t-mobile. if ( $return_path && $return_path =~ /mms\.alltel\.net$/ ) { my @imgs; foreach my $img ( $self->get_entity( $self->{_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. } $self->{_entity}->parts( \@imgs ); } # verizon crap. remove paragraphs of text. $self->{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$/ ) { $self->{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 ( $self->get_entity( $self->{_entity}, '*' ) ) { my $path = $ent->bodyhandle->path; $path =~ s#.*/##; if ( $path eq 'smil.txt' ) { $self->{body} = $ent->bodyhandle->as_string(); last; } } $self->{subject} = 'Picture Post'; } return 1; } 1;