#!/usr/bin/env perl =head1 NAME parp - Perl Anti-spam Replacement for Procmail =head1 SYNOPSIS For information on usage from the command-line, type: parp -h =cut # $Id: parp,v 1.140 2002/04/29 15:16:50 adams Exp $ use strict; use warnings; require 5.6.0; our $VERSION = '0.62'; use Fcntl qw(:flock); use Mail::Internet; # use MIME::Tools; # MIME::Tools->debugging(1); use Proc::Daemon; use Parp::Config qw(config); use Parp::Filter qw(filter); use Parp::IdCache; use Parp::Friends; use Parp::Options qw(opt usage); use Parp::Mail; use Parp::Utils qw(init_logfile vprint diagnose log_rule log_mode error fatal); $SIG{__DIE__} = \&die_handler; # Process options Parp::Options::process(); # Prepare for output $| = 1; init_logfile() unless opt('test_run'); kill_daemon_and_quit() if opt('kill_daemon'); SWITCH: { if (opt('filter_files')) { filter_argv(); last SWITCH; } if (opt('daemon')) { do_daemon_mode(); last SWITCH; } do_filter_mode(); } exit 0; sub do_daemon_mode { usage() if @ARGV; Proc::Daemon::Init(); # Proc::Daemon closes all open handles, so they need reopening. We # also need to set up error handling as soon as we have somewhere to # log. init_logfile(); $SIG{TERM} = $SIG{INT} = $SIG{QUIT} = \&quit_handler; $SIG{__WARN__} = \&warn_handler; # There could be a rogue shutdown file lying around # if parp -k was run when no daemon was running. my $shutdown_file = config->shutdown_file; unlink $shutdown_file; my $spool_dir = opt('daemon'); opendir(SPOOL, $spool_dir) or fatal(7, "Couldn't open $spool_dir: $!"); my $now = localtime(); log_mode "parp $VERSION started in daemon mode (pid $$) at ", scalar(localtime()); while (1) { my @mails = grep /^\d{9,}\.\d{1,}/, readdir(SPOOL); filter_incoming(@mails); rewinddir(SPOOL); check_shutdown(); sleep(config->daemon_poll_interval); } } sub kill_daemon_and_quit { my $shutdown = config->shutdown_file; open(SHUTDOWN, ">$shutdown") or fatal(8, "Couldn't open $shutdown: $!"); close(SHUTDOWN); exit 0; } sub filter_incoming { start_batch(); foreach my $file (@_) { my $full = opt('daemon') . "/$file"; open(MAIL, $full) or error("Couldn't open $full: $!"); flock(MAIL, LOCK_EX | LOCK_NB) or next; my $mail = new Mail::Internet( [] ); filter->process_mail($mail); close(MAIL); unlink $full; } end_batch(); } sub do_filter_mode { # Behave like a filter; take one e-mail from STDIN usage() if @ARGV; my $mail = new Mail::Internet( [] ); start_batch(); filter->process_mail($mail); end_batch(); } sub filter_argv { # Filter all folders given in @ARGV usage() unless @ARGV; log_mode "pid $$ started run on folders in ARGV at ", scalar(localtime()); start_batch(); my %counts = ( parsed => 0, total => 0, main => 0, aux => 0, spam => 0, dups => 0, special => 0, ); my %inodes_seen = (); foreach my $file (@ARGV) { unless (-f $file) { # TODO: allow symlinks vprint "Skipping non-file $file.\n"; next; } my ($inode, $size) = (stat $file)[1, 7]; if ($size == 0) { vprint "Skipping empty file $file.\n"; next; } if ($inodes_seen{$inode}) { vprint "Skipping $file\n" . " (already seen file $inodes_seen{$inode} with inode $inode\n"; next; } $inodes_seen{$inode} = $file; filter_folder($file, \%counts); } end_batch(); # FIXME # diagnose <allMessages) { $counts->{total}++; $mail->{parp_foldername} = $file; if (! $mail) { error(ref($folder) . '::allMessages() failed', "\$mail:\n", Dumper $mail); next; } my $rv = filter->process_mail($mail); $counts->{parsed}++ if $rv; $counts->{dups}++ if $rv =~ /IS_DUPLICATE/; $counts->{spam}++ if $rv =~ /IS_SPAM/; $counts->{main}++ if $rv =~ /TO_MAIN/; $counts->{aux}++ if $rv =~ /TO_AUX/; $counts->{special}++ if $rv =~ /IS_SPECIAL/; $counts->{friends}++ if $rv eq 'EXTRACTED_FRIEND'; $counts->{file_total}++; last if opt('sample') && $counts->{total} >= opt('sample'); } $mgr->close($folder); } sub start_batch { Parp::Friends::attach(); Parp::IdCache::attach(); } sub end_batch { Parp::Friends::release(); Parp::IdCache::release(); } sub quit_handler { my ($sig) = @_; my $shutdown = config->shutdown_file; if (! open(SHUTDOWN, ">$shutdown")) { log_mode "pid $$ caught a SIG$sig at ", scalar(localtime), "\nbut failed to open $shutdown: $!; shutting down immediately"; } print SHUTDOWN $sig; close(SHUTDOWN); } sub warn_handler { (my $warning = shift) =~ s/^/!!! /gm; $warning =~ s/\n*$/\n/; diagnose($warning); } sub check_shutdown { my $shutdown = config->shutdown_file; return unless open(SHUTDOWN, $shutdown); my $sig = || ''; chomp $sig; close(SHUTDOWN); unlink $shutdown; log_mode "pid $$ ", $sig ? "caught a SIG$sig" : "received shutdown via -k", " --\nshutting down gracefully at ", scalar(localtime); exit 0; } sub die_handler { my ($error) = @_; error($error, "Called via DIE handler\n"); exit 255; } =head1 DESCRIPTION parp is a powerful, extensible, hackerware e-mail filter with sophisticated anti-spam capabilities. FIXME: Include an overview of all the modules here. For now, see . =head1 SEE ALSO L, L, L, L, L, L, L, L, L, L, L, L, L, L =head1 LICENSE Copyright (c) 1999 Adam Spiers . All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut