#!/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.

use strict;
BEGIN {
    require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
}

use LJ::Worker::TheSchwartz;
use DW::EmailPost;
use LJ::Support;
use LJ::Sysban;

schwartz_decl('LJ::Worker::IncomingEmail');
schwartz_work();

package TempDirObj;
use File::Path ();

sub new {
    my ($class) = @_;
    my $tmpdir = File::Temp::tempdir();
    die "No tempdir made?" unless -d $tmpdir && -w $tmpdir;
    return bless {
        dir => $tmpdir,
    }, $class;
}

sub dir { $_[0]{dir} }

sub DESTROY {
    my $self = shift;
    File::Path::rmtree($self->{dir}) if -d $self->{dir};
}


package LJ::Worker::IncomingEmail;
use strict;
use base 'TheSchwartz::Worker';

use MIME::Parser;
use File::Temp ();
my $last_job;

use DW::BlobStore;

sub max_retries {   5 }
sub retry_delay { 100 }
sub grab_for { 300 }
sub keep_exit_status_for { 86400 }

sub completed {
    $last_job->completed;
    return;
}

sub dequeue {
    my $msg = shift;
    $last_job->permanent_failure($msg);
    return;
}

sub retry {
    my $msg = shift;
    $last_job->failed($msg);
    return;
}

# examine message contents and decide what to do
# with it.
sub work {
    my ($class, $job) = @_;
    $last_job = $job;
    my $arg   = $job->arg;

    my $tmpdiro = TempDirObj->new;
    my $tmpdir  = $tmpdiro->dir;

    my $parser = MIME::Parser->new;
    $parser->output_dir($tmpdir);

    my $entity;
    if ($arg =~ /^ie:.+$/) {
        my $email = DW::BlobStore->retrieve( temp => $arg )
            or return dequeue("Can't retrieve from BlobStore: $arg");
        $entity = eval { $parser->parse_data( $$email ) };
    } else {
        $entity = eval { $parser->parse_data($arg) };
    }
    return dequeue("Can't parse MIME: $@") if $@;

    my $head = $entity->head;
    $head->unfold;

    my $subject = $head->get('Subject');
    chomp $subject;
    $subject = LJ::trim( $subject );

    # simple/effective spam/bounce/virus checks:
    return dequeue("Bounce")      if $head->get("Return-Path") =~ /^\s*<>\s*$/;
    return dequeue("Spam")        if subject_is_bogus($subject);
    return dequeue("Virus found") if virus_check($entity);
    return dequeue("Spam")        if $subject && $subject =~ /^\[SPAM: \d+\.?\d*\]/;

    # see if a hook is registered to handle this message
    if (LJ::Hooks::are_hooks("incoming_email_handler")) {

        my $errmsg = "";
        my $retry  = 0;

        # incoming_email_handler hook will return a true value
        # if it chose to handle this incoming email
        my $rv = LJ::Hooks::run_hook("incoming_email_handler",
                              entity => $entity,
                              errmsg => \$errmsg,
                              retry  => \$retry);

        # success is signaled by a true $rv
        if ($rv) {

            # temporary retry case
            if ($retry) {
                return retry($errmsg);
            }

            # total failure case
            if ($errmsg) {
                return dequeue($errmsg);
            }

            return completed();
        }

        # hook didn't want to handle this email...
    }

    # see if it's a post-by-email
    my $email_post = DW::EmailPost->get_handler( $entity );
    if ( $email_post ) {
        my ( $ok, $status_msg ) = $email_post->process;

        # on success: $status_msg eq 'Post success"
        # on failure: $status_msg is something else
        #             -- then we check $email_post->dequeue
        return completed() if $ok;

        # failure. do we retry?
        return $email_post->dequeue ? dequeue( $status_msg ) : retry( $status_msg );
    }

    # stop more spam, based on body text checks
    my $tent = DW::EmailPost->get_entity( $entity );
    $tent ||= DW::EmailPost->get_entity( $entity, 'html' );
    return dequeue("Can't find text or html entity") unless $tent;
    my $body = $tent->bodyhandle->as_string;
    $body = LJ::trim($body);

    ### spam
    if (   $body =~ /I send you this file in order to have your advice/i
        || $body =~ /^Content-Type: application\/octet-stream/i
        || $body =~ /^(Please see|See) the attached file for details\.?$/i
        || $body =~ /^I apologize for this automatic reply to your email/i )
    {
        return dequeue("Spam");
    }

    # From this point on we know it's a support request of some type,
    my $email2cat = LJ::Support::load_email_to_cat_map();

    my $to;
    my $toarg;
    foreach my $a ( Mail::Address->parse( $head->get('To') ), Mail::Address->parse( $head->get('Cc') ) ) {
        my $address = $a->address;
        my $arg;
        if ( $address =~ /^(.+)\+(.*)\@(.+)$/ ) {
            ( $address, $arg ) = ( lc "$1\@$3", $2 );
        }
        if ( defined $LJ::ALIAS_TO_SUPPORTCAT{$address} ) {
            $address = $LJ::ALIAS_TO_SUPPORTCAT{$address};
        }
        if ( defined $email2cat->{$address} ) {
            $to    = $address;
            $toarg = $arg;
        }
    }

    return dequeue("Not deliverable to support system (no match To:)")
      unless $to;

    my $adf = ( Mail::Address->parse( $head->get('From') ) )[0];
    return dequeue("Bogus From: header") unless $adf;

    my $name = $adf->name;
    my $from = $adf->address;
    $subject ||= "(No Subject)";

    # is this a reply to another post?
    if ( $toarg =~ /^(\d+)z(.+)$/ ) {
        my $spid     = $1;
        my $miniauth = $2;
        my $sp       = LJ::Support::load_request($spid);

        LJ::Support::mini_auth($sp) eq $miniauth
          or die "Invalid authentication?";

        if ( LJ::sysban_check( 'support_email', $from ) ) {
            my $msg = "Support request blocked based on email.";
            LJ::Sysban::block( 0, $msg, { 'email' => $from } );
            return dequeue($msg);
        }

        # make sure it's not locked
        return dequeue("Request is locked, can't append comment.")
          if LJ::Support::is_locked($sp);

        # valid.  need to strip out stuff now with authcodes:
        $body =~ s!https?://.+/support/act\S+![snipped]!g;
        $body =~ s!\+(\d)+z\w{1,10}\@!\@!g;
        $body =~ s!&auth=\S+!!g;

        ## try to get rid of reply stuff.
        # Outlook Express:
        $body =~ s!(\S+.*?)-{4,10} Original Message -{4,10}.+!$1!s;

        # Pine/Netscape
        $body =~ s!(\S+.*?)\bOn [^\n]+ wrote:\n.+!$1!s;

        # append the comment, re-open the request if necessary
        my $splid = LJ::Support::append_request(
            $sp,
            {
                'type' => 'comment',
                'body' => $body,
            }
          )
          or return dequeue("Error appending request?");

        LJ::Support::add_email_address( $sp, $from );

        LJ::Support::touch_request($spid);

        return completed();
    }

    # Now see if we want to ignore this particular email and bounce it back with
    # the contents from a file.  Check $LJ::DENY_REQUEST_FROM_EMAIL first.  Note
    # that this will only bounce initial emails; if a user replies to an email
    # from a request that's open, it'll be accepted above.
    my ( $content_file, $content );
    if ( %LJ::DENY_REQUEST_FROM_EMAIL && $LJ::DENY_REQUEST_FROM_EMAIL{$to} ) {
        $content_file = $LJ::DENY_REQUEST_FROM_EMAIL{$to};
        $content      = LJ::load_include($content_file);
    }
    if ( $content_file && $content ) {

        # construct mail to send to user
        my $email = <<EMAIL_END;
$content

Your original message:

$body
EMAIL_END

        # send the message
        LJ::send_mail(
            {
                'to'      => $from,
                'from'    => $LJ::BOGUS_EMAIL,
                'subject' => "Your Email to $to",
                'body'    => $email,
                'wrap'    => 1,
            }
        );

        # all done
        return completed();
    }

    # make a new post.
    my @errors;

    # convert email body to utf-8
    my $content_type = $head->get('Content-type:');
    if ( $content_type =~ /\bcharset=[\'\"]?(\S+?)[\'\"]?[\s\;]/i ) {
        my $charset = $1;
        if ( defined $charset
             && $charset !~ /^UTF-?8$/i
             && Unicode::MapUTF8::utf8_supported_charset( $charset )
           ) {
            $body = Unicode::MapUTF8::to_utf8(
                    { -string => $body, -charset => $charset } );
        }
    }

    my $spid = LJ::Support::file_request(
        \@errors,
        {
            'spcatid'  => $email2cat->{$to}->{'spcatid'},
            'subject'  => $subject,
            'reqtype'  => 'email',
            'reqname'  => $name,
            'reqemail' => $from,
            'body'     => $body,
        }
    );

    if (@errors) {
        # FIXME: detect trasient vs. permanent errors (changes to
        # file_request above, probably) and either dequeue or try
        # later
        return dequeue("Support errors: @errors");
    }
    else {
        return completed();
    }
}

# returns true on found virus
sub virus_check {
    my $entity = shift;
    return unless $entity;

    my @exe = DW::EmailPost->get_entity( $entity, 'all' );
    return unless scalar @exe;

    # If an attachment's encoding begins with one of these strings,
    # we want to completely drop the message.
    # (Other 'clean' attachments are silently ignored, and the
    # message is allowed.)
    my @virus_sigs =
      qw(
      TVqQAAMAA TVpQAAIAA TVpAALQAc TVpyAXkAX TVrmAU4AA
      TVrhARwAk TVoFAQUAA TVoAAAQAA TVoIARMAA TVouARsAA
      TVrQAT8AA UEsDBBQAA UEsDBAoAAA
      R0lGODlhaAA7APcAAP///+rp6puSp6GZrDUjUUc6Zn53mFJMdbGvvVtXh2xre8bF1x8cU4yLprOy
    );

    # get the length of the longest virus signature
    my $maxlength =
      length( ( sort { length $b <=> length $a } @virus_sigs )[0] );
    $maxlength = 1024 if $maxlength >= 1024;    # capped at 1k

    foreach my $part (@exe) {
        my $contents = $part->stringify_body;
        $contents = substr $contents, 0, $maxlength;

        foreach (@virus_sigs) {
            return 1 if index( $contents, $_ ) == 0;
        }
    }

    return;
}

sub subject_is_bogus {
    my $subject = shift;
    # ignore spam/vacation/auto-reply messages
    return $subject =~ /auto.?(response|reply)/i
        || $subject =~ /^(Undelive|Mail System Error - |ScanMail Message: |\+\s*SPAM|Norton AntiVirus)/i
        || $subject =~ /^(Mail Delivery Problem|Mail delivery failed)/i
        || $subject =~ /^failure notice$/i
        || $subject =~ /\[BOUNCED SPAM\]/i
        || $subject =~ /^Symantec AVF /i
        || $subject =~ /Attachment block message/i
        || $subject =~ /Use this patch immediately/i
        || $subject =~ /^YOUR PAYPAL\.COM ACCOUNT EXPIRES/i
        || $subject =~ /^don\'t be late! ([\w\-]{1,25})$/i
        || $subject =~ /^your account ([\w\-]{1,25})$/i
        || $subject =~ /Message Undeliverable/i;
}
