#!/usr/local/bin/perl # Copyright (c) 2003-2006 by Jeremy Kister # Author: Jeremy Kister # Name: Email Secretary (ESec) # Function: Challange Response system to deter spam use strict; use DBI; use Net::SMTP; use Net::DNS; my $HAVE_VRFY; eval{ require Mail::VRFY; $HAVE_VRFY=1; }; #%%VERSION%% #%%DBUN%% #%%DBPW%% #%%DSN%% #%%PLUGIN%% #%%PLUGIN_ARGS%% #%%FORWARD_HOST%% #%%AUTORESPOND%% my $VERBOSE = 1; # 1 = more logging to qmail logs (0 = less) my $account = shift || die "syntax error: no account specified\n"; my($maildir,$subject,$string,$noreply); my(@msg,@header,@notes); my $where = 'pnd'; my $ext=$ENV{'EXT'} || die "environment not as expected: EXT\n"; my $host=$ENV{'HOST'} || die "environment not as expected: HOST\n"; my $sender=$ENV{'SENDER'}; # envelope sender: is empty string if bounce my $email = "${ext}\@${host}"; if(($sender ne '') && ($sender !~ /^([a-z0-9_\.\+\-\=\?\^\#])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+$/i)){ warn "dropping message for ${account} (${email}): ${sender} doesnt look like a legit email address\n"; exit; } my $rpline=$ENV{'RPLINE'}; my $dtline=$ENV{'DTLINE'}; if(defined($plugin)){ die "cannot exectute plugin: $plugin: $!\n" unless(-x $plugin); } my $a=0; my $dbh = DBI->connect($dsn, $dbun, $dbpw,{PrintError => 1}); until($dbh){ $a++; die "cannot connect to database\n" if($a == 10); sleep 1; $dbh = DBI->connect($dsn, $dbun, $dbpw,{PrintError => 1}); } # drop mail if person is listed in known_bouncers my $sql = 'SELECT COUNT(*) FROM known_bouncers WHERE addr = ' . $dbh->quote($sender); $sql .= " AND time < ($^T - 172800)"; # in case cron is not running for whatever reason my $sth = $dbh->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; if($row->[0]){ my $sql = "UPDATE known_bouncers SET time = $^T WHERE addr = " . $dbh->quote($sender); my $sth = $dbh->prepare($sql); $sth->execute; exit; } open(M, '/var/qmail/control/me') || die "cannot open /var/qmail/control/me: $!\n"; chomp(my $hostname=); close M; # have to read header before checking if user exists, to find out if # we should bounce the message or not # protect ourselves from MTAs that send a message with no empty line # separating header and body: read only the first 200 lines, max my $n=1; while((defined($_=)) && ($n < 201)){ last if(/^$/); # we only want fields in the header, here. push @header, $_; # save field; we have no maildir, yet if(/^Subject\s*:\s?(.+)/i){ $subject=$1; if($subject =~ /\s\[::#:: ESec Confirmation ::#::\]/){ $noreply=1; # anti-loop protection warn "found ESec Confirmation tag in subject\n" if($VERBOSE); } }elsif(/^X-ESec-Probe:\s(.{16})/i){ # must detect case insensitve above because of stupid MS exchange bounces $string = $1; $noreply=1; push @notes, "string found in header: $string"; warn "found string in header: $string\n" if($VERBOSE); }elsif(/^Precedence\s*:\s*(bulk|list|junk)/i){ $noreply=1; warn "found precedence: $1 in header\n" if($VERBOSE); }elsif(/^List-(\S+.*):/i){ $noreply=1; warn "found List-$1 in header\n" if($VERBOSE); }elsif(/^Mailing-List(.*):/i){ $noreply=1; warn "found Mailing-List$1 in header\n" if($VERBOSE); }elsif(/^X-ESec-(\S+):\s/i){ # case insensive for stupid MS "programmers" $noreply=1; # added anti-loop protection warn "found X-ESec-$1 in header\n" if($VERBOSE); }elsif((!/^\s/) && ((/^[^:]*\s/) || (!/:/))){ # if line does not start with a space AND (there is a space (followed by stuff) before a colon OR there is no colon) # must not be a field in the header warn "out of header detected on: $_\n" if($VERBOSE); last; } $n++; } $noreply=1 if(($ext =~ /^owner-/) || ($ext =~ /-request$/) || ($ext eq 'MAILER-DAEMON')); $sql = 'SELECT emailid,mode FROM accounts WHERE email = ' . $dbh->quote($account); $sth = $dbh->prepare($sql); $sth->execute; $row = $sth->fetchrow_arrayref; my $emailid = $row->[0]; my $mode = $row->[1]; my ($user,$domain) = split(/\@/, $account); if(defined($emailid)){ my $dir; open(A, '/var/qmail/users/assign') || die "cannot open assign: $!\n"; while(){ if(/^\+${domain}\-:[^:]+:\d+:\d+:([^:]+):/){ $dir = $1; last; } } close A; # vpasswd could simply be optional -- could traverse to work with vanilla qmail or # any virtual domains package if(open(V, "${dir}/vpasswd")){ while(){ if(/^${user}:[^:]+:\d+:\d+:[^:]*:([^:]+):/){ $maildir = $1 . '/Maildir'; last; } } close V; unless(defined($maildir)){ exit if(defined($noreply)); bounce('User Unknown #5.9.1',${hostname},$host,@header); } }else{ warn "cannot open ${dir}/vpasswd: $!\n"; exit if(defined($noreply)); bounce('User Unknown #5.9.2',${hostname},$host,@header); } }else{ warn "no account set up for ${email}\n"; exit if(defined($noreply)); bounce('User Unknown #5.9.4',${hostname},$host,@header); } warn "found ${emailid}/${mode}/${maildir} via ${account}\n" if($VERBOSE); unless(-d "${maildir}/tmp"){ unless(-d "${maildir}"){ mkdir("${maildir}",0700) || die "cannot create ${maildir}: $!\n"; } mkdir("${maildir}/tmp",0700) || die "cannot create ${maildir}/tmp: $!\n"; } mkdir("${maildir}/pnd",0700) unless(-d "${maildir}/pnd"); mkdir("${maildir}/new",0700) unless(-d "${maildir}/new"); my $random = getrand(4); my $file="$^T.$$.$random.esec.$hostname"; # seperate file, because we need to know if body contains probe, # but we are not done writing our header info open(B, ">${maildir}/tmp/${file}.body") || die "cannot write to ${maildir}/tmp/${file}.body: $!\n"; $n = 1; my (@body,%pos_bouncers); while(){ print B; if($n < 21){ # save first 20 lines of body in memory in case of a bounce push @body, $_; $n++; } # dont require ^, as some MTA's prepend > and such to body of bounces # case insensitive for MS exchange if(/X-ESec-Probe:\s(.{16})/i){ $string = $1; push @notes, "string found in body via field: $string"; warn "found string in body via field: $string\n" if($VERBOSE); }elsif(/to:[^a-z0-9_\.\+\-\=\?\^\#]*(([a-z0-9_\.\+\-\=\?])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+)/i){ $pos_bouncers{$1} = 1; warn "found possible bouncer in body (via to:): $1\n" if($VERBOSE); }elsif(/<(([a-z0-9_\.\+\-\=\?\^\#])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+)>/i){ $pos_bouncers{$1} = 1; warn "found possible bouncer in body (via <>): $1\n" if($VERBOSE); }elsif(/^\s*(([a-z0-9_\.\+\-\=\?\^\#]+)\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+)$/i){ $pos_bouncers{$1} = 1; warn "found possible bouncer in body (via ^\\s*): $1\n" if($VERBOSE); } unless(defined($string)){ # we look for the authentication link as a last resort if(/eseccgi.pl\?msgid\=([a-zA-Z0-9\_\.]+)/){ warn "found authentication link in body: $1\n" if($VERBOSE); $string=$1; push @notes, "string found in body via url: $string"; unless(length($string) == 16){ # line is broken, read next and concat my $next=; if($next =~ /^([a-zA-Z0-9\_\.]+)/){ $string .= $1; if(length($string) == 16){ warn "concatinated to $string\n" if($VERBOSE); push @notes, "string found via broken line in body"; }else{ # we failed to get a legit msgid, dump msg close B; unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; exit; } } } } } } close B; # bounces after this point must either use @body or not reference body if(defined($string)){ # find out who this guy is my $sql = 'SELECT sender FROM messages WHERE msgid = ' . $dbh->quote($string); my $sth=$dbh->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; my $dbsender = $row->[0]; if(defined($dbsender)){ # this message is a bounce or a reply to my confirmation probe close B; close H; # unlink the tmp file unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; if(($sender eq '') || ($sender =~ /^mailer-daemon\@/i) || ($sender =~ /^postmaster\@/i)){ # a bounce to my probe -- Envelope Sender should be NULL, people... add_known_bouncer($dbh,$dbsender); # remove all messages in queue from this spammer (for this emailid only, not global) my $sql = 'SELECT filename FROM messages WHERE sender = ' . $dbh->quote($dbsender); $sql .= ' AND emailid = ' . $dbh->quote($emailid); my $sth = $dbh->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ my $filename = $row->[0]; unlink("${maildir}/pnd/${filename}") || warn "cannot unlink $maildir/pnd/$filename: $!\n"; } $sql = 'DELETE FROM messages WHERE sender = ' . $dbh->quote($dbsender); $sql .= ' AND emailid = ' . $dbh->quote($emailid); $sth=$dbh->prepare($sql); $sth->execute || warn "could not delete msgs from $dbsender from db: $!\n"; warn "found message was a bounce to my probe\n" if($VERBOSE); }elsif($sender eq $dbsender){ if(($mode == 1) && ($sender ne $email)){ # whitelist user my $sql = 'INSERT INTO email_whitelists (addr,emailid) VALUES (' . $dbh->quote($sender); $sql .= ',' . $dbh->quote($emailid) . ')'; my $sth = $dbh->prepare($sql); $sth->execute; # authenticate all messages from this sender (for this emailid) $sql = 'SELECT filename FROM messages WHERE sender = ' . $dbh->quote($dbsender); $sql .= ' AND emailid = ' . $dbh->quote($emailid); $sth = $dbh->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ my $filename = $row->[0]; if($sender eq $email){ # from and to same address, do not accidentially let esec confirm the msg. # add $sender to known_bouncers unlink("${maildir}/pnd/${filename}") || warn "cannot unlink $maildir/pnd/$filename: $!\n"; add_known_bouncer($dbh,$sender); }elsif($mode == 1){ if($forward_host){ if(open(F, "${maildir}/pnd/${filename}")){ open(SENDMAIL, "|/var/qmail/bin/sendmail ${user}\@${forward_host}") || die "cannot fork sendmail: $!\n"; while(){ print SENDMAIL; } close SENDMAIL; close F; unlink("${maildir}/pnd/${filename}") || die "could not unlink ${maildir}/pnd/${filename}: $!\n"; }else{ die "could not open ${maildir}/pnd/${filename} (2): $!\n"; } }else{ rename("${maildir}/pnd/${filename}","${maildir}/new/${filename}") || die "cannot rename $maildir/pnd/$filename: $!\n"; } } } $sql = 'DELETE FROM messages WHERE sender = ' . $dbh->quote($dbsender); $sql .= ' AND emailid = ' . $dbh->quote($emailid); $sth=$dbh->prepare($sql); $sth->execute || warn "could not delete msgs from $dbsender from db: $!\n"; }else{ exit if(defined($noreply)); bounce('User Unknown #5.9.5',${hostname},$host,@header,@body); } }else{ exit if(defined($noreply)); bounce('User Unknown #5.9.6',${hostname},$host,@header,@body); } exit; }else{ # or else the pending message using this string has been deleted from our queue # Or this is a confirmation message from another user with esec -- could do something special unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; exit; } } # spammers could easily foil this, but we'll let it in for now # there are rumors of laws against spammers forging the subject if(($sender eq '') || ($sender =~ /^mailer-daemon\@/i) || ($sender =~ /^postmaster\@/i)){ if($subject =~ /\[::#:: ESec Confirmation ::#::\]/){ unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; exit; } exit if($noreply == 1); # doesnt smell like a geniuine bounce $noreply=1; # let [most] bounces through if($subject eq 'failure notice'){ $where = 'new'; push @notes, 'Accepted because message looks like a qmail bounce'; }elsif($subject =~ /^Undeliverable:\s\S/){ $where = 'new'; push @notes, 'Accepted because message looks like an exchange bounce'; }elsif($subject =~ /^Undelivered Mail Returned to Sender/){ $where = 'new'; push @notes, 'Accepted because message looks like a postfix bounce'; }elsif($subject =~ /^Returned mail:\s\S/){ $where = 'new'; push @notes, 'Accepted because message looks like a sendmail bounce'; #Delivery Status Notification #Delivery Notification:\s\S #Delivery failure #Undeliverable Mail #Mail delivery failed:\s\S/ }elsif($subject =~ /deliver|fail|status|notification/i){ $where = 'new'; push @notes, 'Accepted because message looks like a bounce'; } # we dont really care if where eq new, but it's more proof this msg is a # geniune bounce if(($where eq 'new') && (%pos_bouncers)){ foreach my $pos_bouncer (keys %pos_bouncers){ warn "checking to see if i've sent a confirmation to ${pos_bouncer}\n" if($VERBOSE); my $sql = 'SELECT filename FROM messages WHERE sender = ' . $dbh->quote(${pos_bouncer}); $sql .= ' AND emailid = ' . $dbh->quote(${emailid}); my $sth = $dbh->prepare($sql); $sth->execute(); my $filename; while(my $row = $sth->fetchrow_arrayref){ $filename = $row->[0]; warn "I did... ${pos_bouncer}'s address bounces (via ${filename})\n" if($VERBOSE); if(unlink("${maildir}/pnd/${filename}")){ my $sql2 = 'DELETE FROM messages WHERE filename = ' . $dbh->quote($filename); my $sth2 = $dbh->prepare($sql2); $sth2->execute || warn "cannot delete $filename from db: $!\n"; }else{ warn "couldnt unlink $maildir/pnd/$filename: $!\n"; } } if(defined($filename)){ add_known_bouncer($dbh,$pos_bouncer); unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; exit; # more than one in the same email doesnt make sense } } } } # could already be new because of a bounce unless($where eq 'new'){ # find out if the person is already known (via whitelist) my $sql = 'SELECT addr FROM email_whitelists WHERE emailid = ' . $dbh->quote($emailid); my $sth = $dbh->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ if($sender =~ /^$row->[0]$/i){ $where = "new"; push @notes, "Accepted via email_whitelists: $row->[0]"; last; } } $sth->finish; unless($where eq 'new'){ # find out if the email contains whitelisted headers my $sql = 'SELECT field FROM header_whitelists WHERE emailid = ' . $dbh->quote($emailid); my $sth = $dbh->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ foreach(@header){ if(/^$row->[0]$/i){ $where = 'new'; push @notes, "Accepted via header_whitelists: $row->[0]"; last; } } } $sth->finish; unless($where eq 'new'){ # find out if the person is on our blacklist my $sql = 'SELECT addr FROM email_blacklists WHERE emailid = ' . $dbh->quote($emailid); my $sth = $dbh->prepare($sql); $sth->execute; while(my $row = $sth->fetchrow_arrayref){ if($sender =~ /^$row->[0]$/i){ # generic error, no red flags to sender unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; exit if(defined($noreply)); bounce('User Unknown #5.9.5',$hostname,$host,@header,@body); } } $sth->finish; $sql = 'SELECT field FROM header_blacklists WHERE emailid = ' . $dbh->quote($emailid); $sth = $dbh->prepare($sql); $sth->execute; while(my $row=$sth->fetchrow_arrayref){ foreach(@header){ if(/^$row->[0]$/i){ # generic error, no red flags to sender unlink("${maildir}/tmp/${file}","${maildir}/tmp/${file}.body") || warn "cannot unlink tmp files: $!\n"; exit if(defined($noreply)); bounce('User Unknown #5.9.7',$hostname,$host,@header,@body); } } } $sth->finish; } } } my $date=getdate(); open(H,">${maildir}/tmp/${file}") || die "cannot write to ${maildir}/tmp/${file}: $!\n"; print H $rpline, $dtline, "X-ESec-Received: ${date}\n", "X-ESec-Version: ${version}\n", "X-Delivery-Agent: ESec/${version}\n"; if((@notes) && (${where} eq 'new')){ print H 'X-ESec-Notes: '; foreach my $note (@notes){ print H " ${note}\n"; } }elsif(${where} eq 'pnd'){ print H 'X-ESec-Notes: '; if($noreply == 1){ print H "Did not send confirmation probe as per noreply rule.\n", " Queued for confirmation\n"; }else{ print H " Queued for confirmation\n"; } } foreach(@header){ print H; } print H "\n"; #empty line separating header from body open(B, "${maildir}/tmp/${file}.body") || die "cannot open ${maildir}/tmp/${file}.body: $!\n"; while(){ print H; } close B; close H; unlink("${maildir}/tmp/${file}.body") || warn "cannot unlink $maildir/tmp/$file.body: $!\n"; # we dont want to send a confirmation on noreplies -- if(($where eq 'pnd') && ($noreply == 1)){ warn "msg for ${email} (from ${sender}) being saved, without confirmation probe\n"; my $sql = 'INSERT INTO messages (filename,sender,time,emailid,msgid) VALUES (' . $dbh->quote($file); $sql .= ',' . $dbh->quote($sender) . ',' . $dbh->quote($^T) . ',' . $dbh->quote($emailid); $sql .= ',' . $dbh->quote($random) . ')'; my $sth = $dbh->prepare($sql); $sth->execute || die "error occurred saving info: $!\n"; rename("${maildir}/tmp/${file}","${maildir}/pnd/${file}") || die "cannot rename $maildir/tmp/$file: $!\n"; exit; } if($where eq 'new'){ # vacation code if(-f "${maildir}/../vacation/active" && -f "${maildir}/../vacation/message"){ if(open(F, "${maildir}/tmp/${file}")){ open(A, "|${autorespond} 86400 3 ${maildir}/../vacation/message ${maildir}/../vacation") || die "cannot fork $autorespond: $!\n"; print A "-----Original Message-----\n"; my $flag; while(){ unless($flag){ if(/^$/){ $flag=1; print A; } next unless(/^(?:from|date|to|subject):/i); } print A; } print H 'X-ESec-Notes: '; close F; close A; }else{ die "could not open ${maildir}/pnd/${file} (3): $!\n"; } } if($forward_host){ if(open(F, "${maildir}/tmp/${file}")){ open(SENDMAIL, "|/var/qmail/bin/sendmail ${user}\@${forward_host}") || die "cannot fork sendmail: $!\n"; while(){ print SENDMAIL; } close SENDMAIL; close F; unlink("${maildir}/tmp/${file}") || die "could not unlink ${maildir}/tmp/${file}: $!\n"; }else{ die "could not open ${maildir}/pnd/${file} (1): $!\n"; } }else{ rename("${maildir}/tmp/${file}","${maildir}/new/${file}") || die "cannot rename $maildir/tmp/$file: $!\n"; } exit; # nothing more to do } if(($plugin || $HAVE_VRFY) && (! $noreply)){ # first check if email addr is valid, if we can my $vrfy_res = Mail::VRFY::CheckAddress($sender) if($HAVE_VRFY); warn "vrfy_res: $vrfy_res\n" if($VERBOSE); # next punt message to plugin, if we can # we're not *really* scanning the _exact_ incoming message here, # our X-ESec fields et al have been forced in.. # dont send confirmation probe to spammy feeling messages # but still keep as pending, so we can see it system("cat ${maildir}/tmp/${file} | $plugin $args >/dev/null 2>&1") if($plugin); my $plugin_res = $? >> 8; warn "plugin_res: $plugin_res\n" if($VERBOSE); if( ($plugin_res > 0) || ($vrfy_res > 0 && $vrfy_res != 6) ){ # mark the message, so user can see we didnt send a confirmation probe open(F, "${maildir}/tmp/${file}") || die "cannot open ${maildir}/tmp/${file}: $!\n"; open(T, ">${maildir}/tmp/${file}.$$") || die "cannot write to ${maildir}/tmp/${file}.$$: $!\n"; while(){ if(/^X-ESec-Notes: (.*)/){ if($plugin_res > 0){ warn "not sending confirmation probe as per ${plugin}: ${plugin_res}\n"; print T "X-ESec-Notes: Did not send confirmation probe as per ${plugin}: ${plugin_res}\n"; } if($vrfy_res > 0 && $vrfy_res != 6){ warn "not sending confirmation probe as per Mail::VRFY: $vrfy_res\n"; if($plugin_res){ print T " "; }else{ print T "X-ESec-Notes: "; } print T "Did not send confirmation probe as per Mail::VRFY: $vrfy_res\n"; } print T " $1\n"; while(){ print T; } }else{ print T; } } close F; close T; rename("${maildir}/tmp/${file}.$$","${maildir}/pnd/${file}") || die "could not rename ${maildir}/tmp/${file}.$$: $!\n"; unlink("${maildir}/tmp/${file}") || die "could not unlink ${maildir}/tmp/${file}: $!\n"; my $random = getrand(16); # still need random msgid for primary key constraint # save the name of the file, who its from, and when we received it in the db my $sql = 'INSERT INTO messages (filename,sender,time,emailid,msgid) VALUES (' . $dbh->quote($file); $sql .= ',' . $dbh->quote($sender) . ',' . $dbh->quote($^T) . ',' . $dbh->quote($emailid); $sql .= ',' . $dbh->quote($random) . ')'; my $sth = $dbh->prepare($sql); $sth->execute || die "error occurred saving info: $!\n"; exit; } } rename("${maildir}/tmp/${file}","${maildir}/pnd/${file}") || die "cannot rename $maildir/tmp/$file: $!\n"; my ($u,$d) = split(/\@/, $sender); my (@mx) = mx($d); foreach my $rr (@mx){ my $exchanger = $rr->exchange; if(($exchanger =~ /^(?:127|0|10|255|224)\./) || ($exchanger =~ /^192\.168\./) || ($exchanger =~ /^172\.(?:1(?:6|7|8|9)|2\d|3(?:0|1))\./) || # stupid [16-31] ($exchanger =~ /^192\.0\.2\./)){ # bad mx record; delete pnd email # will always be pnd because of exit if where eq new above unlink("${maildir}/pnd/${file}") || warn "cannot unlink $maildir/pnd/$file: $!\n"; add_known_bouncer($dbh,$sender); exit; } } if(@mx){ my $random = getrand(16); # save the name of the file, who its from, and when we received it in the db my $sql = 'INSERT INTO messages (filename,sender,time,emailid,msgid) VALUES (' . $dbh->quote($file); $sql .= ',' . $dbh->quote($sender) . ',' . $dbh->quote($^T) . ',' . $dbh->quote($emailid); $sql .= ',' . $dbh->quote($random) . ')'; my $sth = $dbh->prepare($sql); $sth->execute || die "error occurred saving info: $!\n"; $sql = 'SELECT COUNT(*) FROM messages WHERE sender = ' . $dbh->quote($sender); $sql .= ' AND msgid != ' . $dbh->quote($random) . " AND time > ($^T - 86400)"; $sth = $dbh->prepare($sql); $sth->execute; my $row = $sth->fetchrow_arrayref; my $count = $row->[0]; if($count){ # make sure we havent sent over 5 confirmations within 24 hours if($count > 5){ # no confirmation msg, but keep message exit; }else{ # make sure the last time we sent a confirmation to this person was over an hour ago my $sql = 'SELECT COUNT(*) FROM messages WHERE sender = ' . $dbh->quote($sender); $sql .= ' AND msgid != ' . $dbh->quote($random) . " AND time > ($^T - 3600)"; my $sth = $dbh->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; my $recent = $row->[0]; if($recent > 0){ # no confirmation msg, but keep message exit; } } } # get the person's real name and unique message from the db $sql = 'SELECT name,message'; if($mode == 1){ $sql .= ',reply_blurb'; } $sql .= ' FROM accounts WHERE emailid = ' . $dbh->quote($emailid); $sth = $dbh->prepare($sql); $sth->execute; $row=$sth->fetchrow_arrayref; my $name=$row->[0]; my $message=$row->[1]; my $reply_blurb=$row->[2] if($mode == 1); unless(defined($message)){ my $sql = 'SELECT message FROM defaults'; my $sth = $dbh->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; $message=$row->[0]; } if(($mode == 1) && (! defined($reply_blurb))){ my $sql = 'SELECT reply_blurb FROM defaults'; my $sth = $dbh->prepare($sql); $sth->execute; my $row=$sth->fetchrow_arrayref; $reply_blurb=$row->[0]; } $message =~ s/%%name%%/${name}/g; $message =~ s/%%msgid%%/${random}/g; $message =~ s/%%subject%%/${subject}/g; open(INJECT, "|/var/qmail/bin/qmail-inject -f ${email}") || die "cannot fork qmail-inject: $!\n"; print INJECT "Date: ${date}\n", "Precedence: special-delivery\n", "To: <${sender}>\n", "From: \"${name}\" <${email}>\n", "Subject: Auto: $subject [::#:: ESec Confirmation ::#::]\n", "X-ESec-Probe: ${random}\n", "X-ESec-Version: ${version}\n", "\n", "${message}\n"; print INJECT "\n${reply_blurb}\n" if($mode == 1); print INJECT "-- \n", "ESec: The Smarter Challenge-Response Anti-SPAM Solution.\n", "http://jeremy.kister.net/code/esec/\n"; close INJECT; }else{ # no mx record; delete pnd email # where will always be pnd here, because "exit if new" above unlink("${maildir}/pnd/${file}") || die "cannot unlink $maildir/pnd/$file: $!\n"; add_known_bouncer($dbh,$sender); } sub bounce { my (($reason),$hostname,$host,@header,@body) = @_; # make sure domain has an mx record, and can connect on port 25 # not everyone has dnsmx or dig, and dnsmx gives false answers ISO no mx my @mx = mx($host); # already sorted by preference foreach my $rr (@mx){ my $exchanger = $rr->exchange; unless(($exchanger =~ /^(?:127|0|10|255|224)\./) || ($exchanger =~ /^192\.168\./) || ($exchanger =~ /^172\.(?:1(?:6|7|8|9)|2\d|3(?:0|1))\./) || ($exchanger =~ /^192\.0\.2\./)){ my $smtp = Net::SMTP->new($exchanger); if(defined($smtp)){ my $date = getdate(); $smtp->mail(""); $smtp->recipient($sender); $smtp->data(); $smtp->datasend("Precedence: bounce\n"); $smtp->datasend("Date: ${date}\n"); $smtp->datasend("From: \"MAILER-DAEMON\@${hostname}\" <>\n"); $smtp->datasend("To: ${sender}\n"); $smtp->datasend("Subject: failure notice\n"); $smtp->datasend("X-ESec-Version: ${version}\n\n"); $smtp->datasend("Hi. This is the ESec program at ${hostname}.\n"); $smtp->datasend("I'm afraid I wasn't able to deliver your message to the following addresses.\n"); $smtp->datasend("This is a permanent error; I've given up. Sorry it didn't work out.\n\n"); $smtp->datasend("<${email}>:\n"); $smtp->datasend("${reason}\n\n"); $smtp->datasend("--- Below this line is part of the message.\n\n"); foreach(@header){ $smtp->datasend($_); } $smtp->datasend("\n"); if(@body){ foreach(@body){ $smtp->datasend($_); $n++; } }else{ # show only first 20 lines of message my $n = 1; while( (defined($_=)) && ($n < 21)){ # wound past header $smtp->datasend($_); $n++; } } $smtp->dataend(); exit; } } # or else bounce gets dumped } # or else bounce gets dumped } sub getrand { my $length = shift; die "getrand syntax error" unless($length =~ /^\d+$/); my($buf,$x,$random); open(D, "/dev/urandom") || die "cannot open urandom: $!\n"; my @set = ('A'..'Z', 'a'..'z', '0'..'9', '_'); # no dot in a filename foreach(1..$length){ sysread( D, $buf, 1 ); my $v = ord($buf); $x ^= ($v & ~63) >> (rand(7)+1); $random .= $set[ ($x ^ ord($buf)) & 63 ]; } close D; return($random); } sub getdate { # user can set these up themselves # http://atm.geo.nsf.gov/ieis/time.html my($day,$mon,$num,$time,$tz,$year) = split(/\s+/, `date`); my $offset="($tz)"; if($tz eq 'EST'){ $offset='-0500'; }elsif($tz eq 'EDT'){ $offset='-0400'; } my $date = "${day}, ${num} ${mon} ${year} ${time} ${offset}"; return($date); } sub add_known_bouncer { my ($dbh,$bouncer) = @_; # add spammer to known bouncer's list, so we dont keep probing him my $sql = 'INSERT INTO known_bouncers (addr,time) VALUES ('; $sql .= $dbh->quote($bouncer) . ",$^T)"; my $sth = $dbh->prepare($sql); $sth->execute; }