#!/usr/bin/perl
#
# reassassinate - run SpamAssassin on an IMAP mailbox, then reupload
# 
# usage:
#   ./reassassinate --user jmason --host mail.example.com 
#                           --inbox INBOX --junkfolder INBOX.crap
#
# Runs SpamAssassin over all mail messages in an IMAP mailbox, skipping
# ones it's processed before.  It then reuploads the rewritten
# messages to two locations depending on whether they are spam or not;
# nonspam messages are simply re-saved to the original mailbox, spam
# messages are sent to the mailbox specified in "--junkfolder".
# 
# This is especially handy if some time passed since the mails were
# originally delivered, allowing more of the message contents of spam
# mails to be blacklisted by third-party DNSBLs and URIBLs in the meantime.
#
# Prerequisites:
#   Mail::IMAPClient
#   Mail::SpamAssassin
#
# installing those on Ubuntu:
#   sudo apt-get install libmail-spamassassin-perl libmail-imapclient-perl
#
# TODO: ignore messages that are too young
#
# Version: 1.0

use strict;
use warnings;

### Modules
use Mail::IMAPClient;
use Mail::SpamAssassin;
use Getopt::Long;

### Variables
my ($spamtest, $imap, $status);
my ($flag, $password, $msg, $msgtxt);
my ($opt_user, $opt_host, $opt_help, $opt_interval, $opt_daemon);


### Initial values
my $inbox = "INBOX";      # Name of mailbox to check for SPAM in
my $junk  = "INBOX/SPAM"; # Name of mailbox to move SPAM to



### Main program
print <<_EoT_;
IMAP interface to SpamAssassin V1.1
 (c)2003  Alexander J. Trentini (thanks to anithri of perlmonks.org)
 Updated 2009 by Justin Mason (jm at jmason.org)

_EoT_

# Check commandline parameters
GetOptions("user=s"     => \$opt_user,      # IMAP user name
           "host=s"     => \$opt_host,      # IMAP host name or IP address
           "interval=s" => \$opt_interval,  # Check interval
           "inbox=s"    => \$inbox,         # INBOX folder
           "junkfolder=s" => \$junk,        # junk folder
           "daemon" => \$opt_daemon,        # Run as daemon
           "help"       => \$opt_help);     # Usage text

# Help text?
if (defined($opt_help) || !$opt_host || !$opt_user) {
    print <<_EoT_;
Usage: $0 [--user --host --interval] [--help]

If no parameters are given on the commandline, the script enters
interactive mode.

Parameters:
  --help            Prints this usage text
  --user username   IMAP user name to use
  --host hostname   IMAP host name or IP address to use
  --inbox folder    IMAP path to folder to process
  --junkfolder folder  IMAP path to folder to move spam messages to
  --interval nsecs  Check interval (for --daemon)
  --daemon          Daemonize

Note: The password will ALWAYS be read via interactive mode, due to
security reasons!

_EoT_

    exit(1);
}

# Get password (always interactive)
print "Password for $opt_user: ";
system("stty -echo");
$password = <>;
system("stty echo");
chomp($password);
print("\n");

# Get rid of zombie problems
$SIG{CHLD} = 'IGNORE';

# Initialize SpamAssassin
$spamtest = Mail::SpamAssassin->new();

# Loop endlessly
while (1) {
    # Connect to IMAP server
    $imap = Mail::IMAPClient->new(User     => $opt_user,
                                  Password => $password,
                                  Server   => $opt_host,
                                  UID      => 1,
                                  Debug    => 0,
                                  Peek     => 1) or die("Couldn't connect" .
        " to $opt_host using $opt_user (Incorrect password?).\n");
        
    $imap->select($inbox) or die "cannot select $inbox: $@";

    # Create $junk folder if not existant
    unless ($imap->exists($junk)) {
        $imap->create($junk) or die "cannot create $junk: $@";
    }
    
    # Omit already checked messages and messages that are too fresh
    #my $before_date = "12-Jan-2009 01:00:00";
    my @messages = $imap->search(
            'NOT HEADER X-Spam-Reassassinated "run"');
    if ($@) {
        die "Error in search: $@\n";
    }

    my $now = scalar localtime time;

    # Traverse messages
    foreach my $msg (@messages) {
        next unless defined $msg;
        print "checking $inbox:$msg...\n";

        # Only check if mail is smaller than 512KB (performance)
        my $size = $imap->size($msg);
        next if (defined $size && $size > 512 * 1024);
        
        # Get message and pass to SpamAssassin
        $msgtxt = $imap->message_string($msg);
        next unless $msgtxt;

        my $samail = $spamtest->parse($msgtxt);
        $status = $spamtest->check($samail);
        $msgtxt = $status->rewrite_mail();
        $msgtxt = "X-Spam-Reassassinated: run at $now\n".$msgtxt;

        if ($status->is_spam()) {
            $flag = $imap->append_string($junk, $msgtxt);
        } else {
            $flag = $imap->append_string($inbox, $msgtxt);
        }
        
        # If successfully created, delete original from inbox
        $imap->delete_message($msg) if ($flag); 
        $status->finish();
        $samail->finish();
    }

    # Close IMAP session
    $imap->close();

    last unless $opt_daemon;
    sleep $opt_interval if $opt_daemon;
}


