INIT {
require Sys::Syslog;
- Sys::Syslog::openlog("lacemail","pid","mail");
+ Sys::Syslog::openlog("perlmail","pid","mail");
my @syslogging_stack;
sub syslogging_on_save
{
}
+use File::Basename;
+BEGIN {
+ use lib $ENV{"PERLMAIL_BASEDIR"} || File::Basename::dirname($0);
+ use PerlMail::Config;
+ use PerlMail::Lib;
+ }
+
use Mail::Audit qw(MAPS);
require IO::Handle;
use Carp qw(cluck confess);
use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
require POSIX; # for ceil
use User::Utmp;
-require File::Basename;
use Getopt::Long;
require Mail::Address;
require MIME::Words;
require HTTP::Request;
require LWP::UserAgent;
use URI::Escape 'uri_escape';
+require WWW::SMS;
-my $HOME="/home/lace";
-my $Mail="$HOME/Mail";
-my @ValidUsers=qw(root lace short kratochvil _local);
-my $IdleMax=60;
-my $MaxBodySMS=0x1000; # max bytes to pass to Lingua::EN::Squeeze
-my $SMSmailError='short+err@ucw.cz';
-my $SMSwebRcpt='00420602431329';
-my $SMSmailRcpt=$SMSwebRcpt.'@sms.eurotel.cz';
-my $SMScontact='<short@ucw.cz>';
-
our($Message,$Audit,@AuditStored,$store_ignore,$store_ignorenewmail,$store_profile,$DoBell);
-our(%audit_profile,@sms_squeezes); # imported
+our(%audit_profile,@sms_squeezes,@alternates_host,@dnsbl_whitelist); # imported
+my %alternates_host; # from @alternates_host
+my %dnsbl_whitelist; # from @dnsbl_whitelist
# from RedHat "procmail-3.22-5"
# /i should be only $procmailFROM_DAEMON but how it can hurt to /i all?
my $opt_mode;
my $opt_smstest; # 1 or $smscount
my $opt_idle;
+my $opt_dry;
sub process;
exit 0;
}
-# FIXME: separate 'lacemail'-transfer together with lacemail-submit away
+# FIXME: separate 'perlmail'-transfer together with perlmail-submit away
sub inetd
{
die "Excessive arguments" if @ARGV;
sub useridle
{
+ return 0 if ! -e "$HOME/away";
my %valid_users=map(($_=>1),@ValidUsers);
my($idlebest,$linebest);
for my $utmp (User::Utmp::getut(),{ "ut_line"=>"psaux" }) {
return "";
}
-# FIXME: rewrite &send_cz_eurotel properly by own code
-# patch for http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
-my $agent=LWP::UserAgent->new();
-$agent->agent("LaceMail $VERSION; contact=$SMScontact; ");
-my($request1,$response1); # for &send_cz_eurotel
-my($name,$value,$type,$disabled,$q2);
-
-# &send_cz_eurotel returns: error
-# BEGIN http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
-sub parse_inputs
-{
- my ($resp) = @_;
- my @inputs;
- my $ct;
- my @c;
- $ct=$resp->content();
-
- @c=split '>', $ct;
- grep {
- if (/(<input|<select|<textarea)([^<>]*)(>|$)/i) {
- my $txt=$2, $name="", $value="", $type="x", $disabled=0;
- my $ipoc;
- if ($txt =~ /type="([^"]*)"/i) { $type=$1; }
- elsif ($txt =~ /type=([^" ]*)[ >]/i) { $type=$1; }
- if ($txt =~ /name="([^"]*)"/i) { $name=$1; }
- elsif ($txt =~ /name=([^" ]*)[ >]/i) { $name=$1; }
- if ($txt =~ /value="([^"]*)"/i) { $value=$1; }
- elsif ($txt =~ /value=([^" ]*)[ >]/i) { $value=$1; }
- if ($txt =~ /disabled/i) { $disabled=1; }
- if ($name ne "" && $type ne "" && $type ne "button" && ! $disabled)
- {
- $ipoc=$#inputs;
- $inputs[$ipoc+1][0]=$name;
- $inputs[$ipoc+1][1]=$value;
- }
- }
- 0; } @c;
- return @inputs;
-}
-
-sub make_query
-{
- my (@inputs) = @_;
-
- my $i;
- my $query = "";
-
- for ($i=0; $i<=$#inputs; $i++)
- {
- my($q1, $q2);
- if ($i>0) { $query="$query&"; }
- $q1=uri_escape($inputs[$i][0]);
- $q2=uri_escape($inputs[$i][1]);
- $query="$query$q1=$q2";
- }
-
- #change @ and space back
- $query=~ s/%20/+/g;
- $query=~ s/%40/@/g;
- return $query;
-}
-
-sub send_cz_eurotel
-{
- my ($id,$text,$mail,$directd) = @_;
- my $src_url = "http://www2.eurotel.cz/sms/index.html";
- my @inputs;
- my $query = "";
- my $cookie = HTTP::Cookies->new;
- my $pref;
-
- #check if correct number
- if (substr($id,0,5)!="00420") { return -1; }
- $pref=substr($id,5,3);
- if (!($pref eq "601" || $pref eq "602" || $pref eq "606" || $pref eq "607" || ($pref ge "720" && $pref le "729"))) { return -1; }
-
- #get form page, extract cookies
- $request1=new HTTP::Request('GET', "$src_url?n_pagestyle=new");
- $response1=$agent->request($request1);
- if ($response1->code != 200) { return -3; }
- $cookie->extract_cookies($response1);
-
- #parse the form
- @inputs=parse_inputs($response1);
-
- #fill the form
- $inputs[2][1]=substr($id,5,3);
- $inputs[3][1]=substr($id,8,6);
- $inputs[4][1]=$mail;
-
- #direct display
- $inputs[6][1]="sms";
- $inputs[6][1]="show" if ($directd>0);
-
- $inputs[7][1]=$text;
-
-# for ($i=0; $i<=$#inputs; $i++) { print "[$i] $inputs[$i][0] $inputs[$i][1]\n"; }
-
- #make query
- $query=make_query(@inputs);
-
- #POST the form
- my $header = new HTTP::Headers(
- 'Content-Length' => length($query),
- 'Content-Type' => 'application/x-www-form-urlencoded',
- 'Accept' => '*/*',
- 'Referer' => $src_url
- );
- my $request2 = new HTTP::Request('POST',$src_url, $header, $query);
- $cookie->add_cookie_header($request2);
- my $response2 = $agent->request($request2);
-
- if ($response2->code != 200) { return -3; }
-
- #check for success
- if ($response2->content() !~ /byla.*odesl.*na na SMS centrum/)
- {
- return -2;
- }
- return 0;
-}
-# END http://kiwi.ms.mff.cuni.cz/%7Etom/programming/src/sendsms.tar.gz/sendsms.pl
-
sub smslens
{
my($ignorenewmail,$smscount,%args)=@_;
return map({
my $l=160;
if (!$ignorenewmail) { # send by mail
- $l-=length("Z emailu $SMSmailError: ");
+ $l-=length("Z emailu FIXME SMSmailError: ");
$l-=length(smsbuild($_,$smscount));
}
else { # send by web
- $l-=length("Z WWW x/5: ");
- $l-=length(smsbuild($_,POSIX::ceil($smscount/5)));
+ $l-=6; # 154 is the max length before split; why?
}
$l;
} (0..$smscount-1));
$squeezed=~/^.{0,$len}/s;
my $frag=$&;
$squeezed=$';
- return 0 if send_cz_eurotel($SMSwebRcpt,$frag,"",0);
+ return 0 if 3!=@SMSwebRcpt;
+ local *F;
+ open F,"$HOME/priv/WWW-SMS-$SMSwebRcpt_username.pwd" or return 0;
+ my $pwd=<F>;
+ chomp $pwd;
+ close F;
+ my $sms=WWW::SMS->new(@SMSwebRcpt,$frag,"username"=>$SMSwebRcpt_username,"passwd"=>$pwd);
+ for ($sms->gateways("sorted"=>"reliability")) {
+ last if $sms->send($_);
+ Sys::Syslog::syslog("warning","Web SMS send failed: %s",$WWW::SMS::Error);
+ my $void=$WWW::SMS::Error; # Prevent: Name "WWW::SMS::Error" used only once
+ }
}
return 1;
}
{
my($squeezed,$smscount,@lens)=@_;
- my $recalclen=0;
- for ($smscount=0;$recalclen<length $squeezed;$smscount++) {
- $recalclen+=$lens[$smscount];
- }
- for my $smsi (0..$smscount-1) {
- my $len=$lens[$smsi];
- $squeezed=~/^.{0,$len}/s;
- my $frag=$&;
- $squeezed=$';
- my $mail=Mail::Mailer->new("sendmail","-f","$SMSmailError");
- $mail->open({
- "To"=>$SMSmailRcpt,
- "From"=>$SMSmailError, # no longer displayed anyway
- "X-LaceMail-Version"=>$VERSION,
- "X-LaceMail-Contact"=>$SMScontact,
- });
- print $mail smsbuild($smsi,$smscount).$frag."\n";
- # FIXME: check errors
- $mail->close();
- }
- return 1;
+ return 0;
}
sub smssend
{
my($ignorenewmail,$smscount,%args)=@_;
- my %aliases=muttrc_aliases();
my $text=audit_sms(
"subject"=>unmime($Audit->subject()),
- "from"=>[ map({ $_=$_->address(); $_="\L$_"; $aliases{$_} || $_; } Mail::Address->parse(unmime($Audit->from()))) ],
+ "from"=>[ Mail::Address->parse(unmime($Audit->from())) ],
"body"=>substr(body_simple(),0,$MaxBodySMS*(1+0.25*$smscount)),
%args);
my $texthead="";
return "<???>" if !defined($_) || /^\s*$/s;
s/^\s*//s;
s/\s*$//s;
- return $_ if length($_)<64;
- return substr($_,0,64)."...";
+ return $_ if length($_)<128;
+ return substr($_,0,128)."...";
}
our $profile_eval_depth=0;
$profile=$store_profile if !$profile;
my %do=map({ (!/=/ ? ($_=>1) : ($`=>$')); } profile_eval($profile));
- Sys::Syslog::syslog("info","%s%s: %s: %s",
+ Sys::Syslog::syslog("info","%s%s%s: %s: %s",
+ (!$opt_dry ? "" : "--dry: "),
(!$store_ignore ? "" : "IGNORED[$store_ignore]: "),
map({ cut($_); } $folder,address_show(unmime($Audit->from())),unmime($Audit->subject())),
)
- if $do{"syslog"};
- $DoBell++ if $do{"bell"};
+ if $do{"syslog"} || $opt_dry;
$folder=~s/;.*$//s;
$folder="$Mail/".$' if $folder=~/^=/;
- write_message($folder) if !$store_ignore;
- smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
push @AuditStored,$folder if $do{"did"};
+ return if $store_ignore || $opt_dry;
+ $DoBell++ if $do{"bell"};
+ write_message($folder) or die;
+ smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
}
our $did_last=0;
{
my($folder)=@_;
+ return if $opt_dry;
local *F;
open F,">>$folder" or do { warn "Append \"$folder\": $!"; return 0; };
{
local $Audit=Mail::Audit->new(
"emergency"=>"$Mail/emergency",
"data"=>[map("$_\n",split("\n",$message))],
+ "log"=>"$HOME/.perlmail.log",
+ "loglevel"=>99,
);
local @AuditStored=();
- do { smssend $opt_smstest; return; } if $opt_smstest;
- write_message("$Mail/input");
+ do { smssend 0,$opt_smstest; return; } if $opt_smstest;
+ write_message("$Mail/input") or die;
audit();
warn 'Corrupted $_, repaired' if defined($save_)!=defined($_) || (defined($_) && $save_ ne $_);
}
open CHILD,'|'
.'('.'(razor-check 2>&1;echo >&3 $?)'
.'|sed "s/^/razor-check: /"'
- .'|logger -t "lacemail['.$$.']" -p mail.crit'
+ .'|logger -t "perlmail['.$$.']" -p mail.crit'
.') 3>&1'
.'|exit `cat`'
or return 0;
}
# NOTE: returns undef() if !wantarray and the first header is unrecognized
+# Returns also hosts
sub Received_for
{
my @r=();
my($for)=($hdr=~/\bfor\s+\<?(\S+)\>?\b/);
return $for if !wantarray();
push @r,$for if $for;
+ my($from,$fromaddr)=($hdr=~/\bfrom\s+(?:(\S+)\b.*?)??\[((?:\d{1,3}\.){3}\d{1,3})\]/);
+ $from=$fromaddr if !defined $from;
+ push @r,"$from:$fromaddr" if $from;
}
return @r;
}
-our %muttrc_pending=();
-sub muttrc
-{
-my($muttrc)=@_;
-
- $muttrc||="$HOME/.muttrc";
- $muttrc=~s/^\~/$HOME/;
- do { warn "Looping muttrc, ignoring: $muttrc"; return (); } if $muttrc_pending{$muttrc};
- local $muttrc_pending{$muttrc}=1;
- local *MUTTRC;
- open MUTTRC,$muttrc or do { warn "open \"$muttrc\": $!"; return (); };
- local $/="\n";
- local $_;
- my @r=();
- # far emulation mutt/init.c/mutt_parse_rc_line()
- while (<MUTTRC>) {
- s/^[\s;]*//s;
- s/[#;].*$//s;
- s/\s*$//s;
- next if !/^(\S+)\s*/s;
- if ($1 eq "source") {
- $_=$';
- do { warn "Wrong 'source' parameters at $muttrc:$.: $_"; next; } if !/^\S+$/;
- push @r,muttrc($_);
- next;
- }
- push @r,$_;
- }
- close MUTTRC or warn "close \"$muttrc\": $!";
- return wantarray() ? @r : join("",map("$_\n",@r));
-}
-
-my %mutteval_charmap=( # WARNING: Don't use "" or "0" here, see below for "|| warn"!
- '\\'=>"\\",
- 'r'=>"\r",
- 'n'=>"\n",
- 't'=>"\t",
- 'f'=>"\f",
- 'e'=>"\e",
- );
-# mutt/init.c/mutt_extract_token()
-sub mutteval
-{
- local $_=$_[0];
- return $_ if !s/^"//;
- do { warn "Missing trailing quote in: $_"; return $_; } if !s/"$//;
- s/\\(.)/$mutteval_charmap{$1} || warn "Undefined '\\$1' sequence in: $_";/ges;
- return $_;
-}
-
-sub muttrc_get
+# Extended Mail::Audit::MAPS
+# $domain,$full,[$timeout]
+sub dnsbl
{
-my(@headers)=@_;
+my($domain,$full,$timeout)=@_;
- my @r=map({ (ref $_ ? $_ : qr/^\s*set\s+\Q$_\E\s*=\s*(.*?)\s*$/si); } @headers);
- my %r=map(($_=>undef()),@r);
- for (muttrc()) {
- for my $ritem (@r) {
- /$ritem/si or next;
- $r{$ritem}=mutteval $1;
- }
- }
- for my $var (grep { !defined($r{$_}) } @r) {
- warn "Variable '$var' not found in muttrc";
- return undef();
+ $timeout||=30; # sec
+ $Mail::Audit::MAPS::host=$domain;
+ my @hosts=map({ s/^.*://; "[$_]"; } # strip DNS part
+ grep({ /^([^:@]*):/ # $1 is DNS name, $' is IP address
+ && !$alternates_host{$1} # leave only foreign hosts
+ && !$dnsbl_whitelist{$'}; } (Received_for()))
+ );
+ splice @hosts,1 if !$full && @hosts; # "&& @hosts" to prevent: WARN: splice() offset past end of array
+ {
+ package My::Audit::Faked;
+ sub received { return @{$_[0]->{"received"}}; }
}
- return wantarray() ? %r : $r{$r[0]};
+ my $self_faked={
+ "received"=>[@hosts],
+ };
+ bless $self_faked,"My::Audit::Faked";
+ return Mail::Audit::rblcheck($self_faked,$timeout);
}
sub muttrc_aliases
for my $addrobj (Mail::Address->parse($')) {
my $addr=$addrobj->address();
my $ref=\$r{"\L$addr"};
- $$ref=$key if !$$ref;
+ $$ref=$key if !$$ref; # use always the first occurence to prefer nicks
}
}
return %r;
my $alternatesre=qr/$alternates/si;
my $From=muttrc_get("from") or return;
my $Fromre=qr/^\Q$From\E$/si;
- warn "'From' \"$From\" not matches by 'alternates': $alternatesre"
+ my $Fromobj=parseone $From or return;
+ warn "'From' \"$From\" not matched by 'alternates': $alternatesre"
if $From!~/$alternates/si;
for my $for (reverse Received_for()) {
- return if $for=~/$From/si;
- next if $for!~/$alternatesre/si;
+ $for=~s/:.*$//; # strip IP address here
+ if ($Fromobj->user() ne "prog-mutt") {
+ next if lc($for) eq lc($From);
+ }
+ else {
+ my $forobj=parseone $for;
+ if ($forobj && $forobj->host()) {
+ # it is 'for' our primary address
+ next if lc($forobj->host()) eq lc($Fromobj->host()); # or 'return'? shouldn't matter
+ }
+ }
+ next if !$alternates_host{lc $for} && $for!~/$alternatesre/si;
store "$prefix\L$for",($profile || []);
return;
}
$text=~s/\b\Q$from\E\b/$to/gsi;
}
return if $text eq $orig;
- $Audit->put_header("X-LaceMail-header_remap-$header",$orig);
+ $Audit->put_header("X-PerlMail-header_remap-$header",$orig);
$Audit->replace_header($header,$text);
}
# MAIN
-my $basedir=File::Basename::dirname($0);
$Getopt::Long::ignorecase=0;
die "GetOptions error" if !Getopt::Long::GetOptions(
"inetd" ,sub { $opt_mode=\&inetd; },
"stdin" ,sub { $opt_mode=\&stdin; },
+ "dry" ,\$opt_dry,
"smstest:s",sub { $opt_mode=\&stdin; $opt_smstest=($_[1] || 1); },
"idle!" ,\$opt_idle,
"idletest" ,sub { syslogging_restore(); print((defined($_=useridle()) ? $_ : "<undef>")."\n"); exit 0; },
"muttrc" ,sub { syslogging_restore(); print scalar muttrc(); exit 0; },
- "d|basedir=s",\&basedir,
);
# "Excessive arguments" checked in &inetd
die "Missing mode" if !$opt_mode;
-my $filenameMyAudit="$basedir/My-Audit.pm";
-open AUDIT,$filenameMyAudit or die "open \"$filenameMyAudit\": $!";
-{
- local $/=undef();
- eval <AUDIT> or die "eval \"$filenameMyAudit\": $@";
- audit_init();
- }
-close AUDIT or warn "close \"$filenameMyAudit\": $!";
+PerlMail::Config::audit_init();
+%alternates_host=map((lc($_)=>1),@alternates_host);
+%dnsbl_whitelist=map(( $_ =>1),@dnsbl_whitelist);
&$opt_mode();
die "NOTREACHED";