#!/usr/bin/perl

package Mail::SpamAssassin::POP3Client;

use strict;
use vars qw($VERSION);

use Net::POP3;
use IPC::Open2;
use File::Spec;

$VERSION = sprintf("%d.%02d", q$Revision: 1.3 $ =~ /(\d+)\.(\d+)/);

sub popspam {
    my(%args) = @_;

    my($pop, $pop_handled_outside, $user, $pw, $host);

    if ($args{pop3}) {
	$pop = delete $args{pop3};
	$pop_handled_outside = 1;
    } else {
	$host = delete $args{host} || die "Argument host missing";
	$user = delete $args{user} || die "Argument user missing";
	$pw   = delete $args{pw}   || die "Argument pw missing";

	my $timeout    = delete $args{timeout};

	$pop = Net::POP3->new($host,
			      defined $timeout ? (Timeout => $timeout) : (),
			     );
	if (!$pop) {
	    die "Can't construct Net::POP3 object";
	}
    }

    my $v          = delete $args{v};
    my $headeronly = delete $args{headeronly};
    my $action     = delete $args{action} || "report";
    my $seenmsgid  = delete $args{seenmsgid};

    if (keys %args) {
	die "Unrecognized arguments: " . join ", ", keys %args;
    }

    if (!$pop_handled_outside) {
	if (!defined $pop->login($user, $pw)) {
	    die "Can't login to $host as $user";
	}
    }

    my $msgnums = $pop->list;
    my $uidl = $pop->uidl;
    my $devnull = File::Spec->can("devnull") ? File::Spec->devnull : "/dev/null";
    open(DEVNULL, ">$devnull") or die $!;
    foreach my $msgnum (sort { $a <=> $b } keys %$msgnums) {
	if ($seenmsgid && exists $seenmsgid->{$uidl->{$msgnum}}) {
	    if ($v) { print STDERR "Skipping message $msgnum ...\n" }
	    next;
	}
	if ($v) { print STDERR "Check message $msgnum ...\n" }
	my $buf;
	if ($headeronly) {
	    $buf = join "", @{ $pop->top($msgnum, 0) };
	} else {
	    $buf = join "", @{ $pop->get($msgnum) };
	}
	my $pid = open2(">&DEVNULL", my $wtrfh, "spamassassin", "-e");
	print $wtrfh $buf;
	close $wtrfh;
	waitpid $pid, 0;
	my $is_spam = ($?/256 != 0);
	if ($is_spam) {
	    warn "Mail number $msgnum is SPAM\n";
	    if ($buf =~ /^from:\s*(.*)/im) {
		warn "From:    $1\n";
	    }
	    if ($buf =~ /^subject:\s*(.*)/im) {
		warn "Subject: $1\n";
	    }
	}
	if ($is_spam) {
	    if ($action eq 'delete') {
		warn "Deleting message $msgnum...\n";
		$pop->delete($msgnum);
	    }
	}
	if ($seenmsgid) {
	    $seenmsgid->{$uidl->{$msgnum}} = 1;
	}
    }
    close DEVNULL;

    if (!$pop_handled_outside) {
	$pop->quit;
    }
}

return 1 if caller;

require Getopt::Long;

my %opt;
Getopt::Long::GetOptions(\%opt,
			 "host=s", "user=s", "pw=s", "timeout=i",
			 "headeronly!", "action=s", "seenmsgid=s",
			 "v!") or die "usage!";
if (!defined $opt{pw}) {
    print STDERR "Password for $opt{user}\@$opt{host}: ";
    require Term::ReadKey;
    Term::ReadKey::ReadMode('noecho');
    $opt{pw} = Term::ReadKey::ReadLine(0);
    Term::ReadKey::ReadMode('restore');
    print STDERR "\n";
}
if (defined $opt{seenmsgid}) {
    require DB_File;
    require Fcntl;
    tie my %seen, 'DB_File', $opt{seenmsgid}, &Fcntl::O_RDWR|&Fcntl::O_CREAT,
	0600 or die $!;
    $opt{seenmsgid} = \%seen;
}

popspam(%opt);

=head1 NAME

popspam - apply spamassassin rules on remote POP3 mailboxes

=head1 SYNOPSIS

    popspam -user <user> -host <host> [-pw <password>] [-timeout <seconds>]
            [-headeronly] [-action <action>] [-v] [-seenmsgid <dbfile>]

=head1 DESCRIPTION

B<popspam> applies L<Mail::SpamAssassin> rules to a POP3 mailbox,
optionally deleting mails classified as spam.

=head2 OPTIONS

=over

=item -user <user>

User name of POP3 mailbox.

=item -host <host>

Host name of POP3 mailbox.

=item -pw <password>

Supply password on command line. This is optional, otherwise the
password is asked interactively. Note that supplying the password on
command line is a security risk, as command line arguments may be
visible in programs like C<ps> or C<top>.

=item -headeronly

Only fetch mail headers, not the mail body.

=item -action <action>

What to do with spam: if I<action> is C<delete>, then the mail will be
deleted, otherwise it will only be reported on the console.

=item -v

Be verbose.

=item -seenmsgid <dbfile>

Path to a file where to store a hash of seen messages. This may
improve performance.

=back

=head1 README

popspam applies Mail::SpamAssassin rules to a POP3 mailbox, optionally
deleting mails classified as spam.

=head1 PREREQUISITES

Mail::SpamAssassin, Net::POP3, Term::ReadKey

=head1 COREQUISITES

DB_File

=head1 OSNAMES

Platform independent, but only tested on Linux and FreeBSD

=head1 SCRIPT CATEGORIES

Mail

=head1 CAVEAT

This script is quite new and not well tested. Use at your own risk,
especially when using the B<delete> action!

=head1 AUTHOR

Slaven Rezic <slaven@rezic.de>

=head1 SEE ALSO

L<spamassassin>

=cut