4 # Copyright (C) 2002-2003 Jan Kratochvil <project-PerlMail@jankratochvil.net>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 use vars qw($VERSION);
22 $VERSION=do { my @r=(q$Revision$=~/\d+/g); sprintf "%d.".("%03d"x$#r),@r; };
29 Sys::Syslog::openlog("perlmail","pid","mail");
31 sub syslogging_on_save
33 push @syslogging_stack,$SIG{"__WARN__"},$SIG{"__DIE__" };
34 $SIG{"__WARN__"}=sub { Sys::Syslog::syslog("warning","WARN: %s",$_[0]); }; # disabled: print STDERR $_[0];
35 $SIG{"__DIE__" }=sub { Sys::Syslog::syslog("crit" ,"DIE: %s" ,$_[0]); };
38 sub syslogging_restore
40 $SIG{"__DIE__" }=pop @syslogging_stack;
41 $SIG{"__WARN__"}=pop @syslogging_stack;
48 use lib $ENV{"PERLMAIL_BASEDIR"} || File::Basename::dirname($0);
51 use lib "/home/lace/lib/perl5/site_perl/5.10.0";
52 use lib "/home/lace/lib64/perl5/site_perl/5.10.0/x86_64-linux-thread-multi";
58 use Mail::Audit qw(MAPS);
60 use Carp qw(cluck confess);
61 use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
62 require POSIX; # for ceil
65 require Mail::Address;
68 require HTML::Entities;
70 require Lingua::EN::Squeeze;
72 require HTTP::Cookies;
73 require HTTP::Request;
74 require LWP::UserAgent;
75 use URI::Escape 'uri_escape';
77 #require Authen::SASL; # Sanity check for &Net::SMTP::auth
80 use POSIX ":sys_wait_h";
83 our($Message,@AuditStored,$DoBell,$Dry);
84 my %alternates_host; # from @alternates_host
85 my %dnsbl_whitelist; # from @dnsbl_whitelist
87 # from RedHat "procmail-3.22-5"
88 # /i should be only $procmailFROM_DAEMON but how it can hurt to /i all?
89 our $procmailTO_ =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?'mio;
90 our $procmailTO =qr'^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To):(.*[^a-zA-Z])?'mio;
91 our $procmailFROM_DAEMON=qr'^(Mailing-List:|Precedence:.*(junk|bulk|list)|To: Multiple recipients of |(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )([^>]*[^(.%@a-z0-9])?(Post(ma?(st(e?r)?|n)|office)|(send)?Mail(er)?|daemon|m(mdf|ajordomo)|n?uucp|LIST(SERV|proc)|NETSERV|o(wner|ps)|r(e(quest|sponse)|oot)|b(ounce|bs\.smtp)|echo|mirror|s(erv(ices?|er)|mtp(error)?|ystem)|A(dmin(istrator)?|MMGR|utoanswer))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$))'mio;
92 $procmailFROM_MAILER=qr'^(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )[^>]*\b(Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|r(esponse|oot)|(bbs\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|MMGR))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$)'mio;
93 # perl-5.8.0 does not cope w/original FROM_MAILER on the third '?' character
94 # Thus we did '([^>]*[^(.%@a-z0-9])?' -> '[^>]*\b', I hope it is somehow similiar
95 # original FROM_MAILER =qr'^(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )([^>]*[^(.%@a-z0-9])?(Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|r(esponse|oot)|(bbs\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|MMGR))(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\(.*\).*)?)?$([^>]|$)'mio;
98 my $opt_smstest; # 1 or $smscount
107 syslogging_restore(); # This is more a debugging session
112 die "Invalid 'From ' line: $_" if $message eq "" && !/^From /;
113 if (!$opt_single && /^From / && $message) {
119 process $message if $message;
123 # FIXME: separate 'perlmail'-transfer together with perlmail-submit away
126 die "Excessive arguments" if @ARGV;
128 IO::Handle::autoflush STDOUT 1;
134 confess "Unexpected EOF: $!" if !defined $length;
135 confess "Missing EOL" if $length!~s/\n$//s;
136 exit 0 if $length eq "BYE";
137 confess "Unrecognized length: $length" if $length!~/^\d+$/;
140 $length==($_=read STDIN,$message,$length) or confess "Got $_ out of required $length bytes";
141 $length==length $message or confess "False read return ".length($message)." instead of $length";
143 # Do not: local *STDOUT; # FIXME: fd's inherited by spawned processes are not closed this way!
144 # local *STDERR; # FIXME: fd's inherited by spawned processes are not closed this way!
145 # as IPC::Open3 and IPC::Open2 will not redirect the output
146 # and send it to the original socket instead!
150 bell() or warn "Unable to BELL";
161 open BELL,">/dev/tty11" or return 0;
163 close BELL or return 0;
169 return 0 if ! -e "$HOME/away";
170 my %valid_users=map(($_=>1),@ValidUsers);
171 my($idlebest,$linebest);
172 for my $utmp (User::Utmp::getut(),{ "ut_line"=>"psaux" }) {
174 next if defined($_=$utmp->{"ut_type"}) && $_!=User::Utmp::USER_PROCESS();
175 next if defined($_=$utmp->{"ut_user"}) && !$valid_users{$_};
176 my $line="/dev/".$utmp->{"ut_line"};
177 my $atime=(stat $line)[8];
178 my $what="user \"".($utmp->{"ut_user"} || "<local>")."\", line \"$line\"";
179 warn "Unable to stat $what" and next if !$atime;
180 my $idle=time()-$atime;
181 warn "atime in future for $what" and next if $idle<0;
182 next if $idle>$IdleMax;
183 next if defined $idlebest && $idlebest<=$idle;
187 return !wantarray() ? $idlebest : ($idlebest,$linebest);
190 # return only the very (recursive) first part
193 return $Audit if !$Audit->is_mime();
196 $first=$_ while $_=$first->parts(0);
202 return 0 if !$Audit->is_mime();
203 return $Audit->is_multipart();
210 return $Audit->is_mime() ? $part->head()
211 : MIME::Head->new([ split "\n",$Audit->head()->as_string() ])
219 # be vary cautious here as most of $part methods will encode it!
220 return join "",@{$Audit->body()} if !$Audit->is_mime();
221 my $bodyhandle=$part->bodyhandle();
222 # If MIME is corrupted we don't get bodyhandle() for this part
223 # It may occur when "boundary" is specified by header but no such boundary is found in the body
224 return $bodyhandle->as_string() if $bodyhandle;
225 warn "MIME corrupted, adapting";
226 return $part->body_as_string();
233 return $Audit->is_mime() ? $part->effective_type() : mimehead($part)->mime_type();
238 my $first=body_first();
239 my $r=mimebody($first);
240 my $mime_type=mime_type($first);
241 if ($mime_type eq "text/html") {
242 # HTML::FormatText just does a useless text layouts
243 # PerlIO::via::StripHTML probably needs PerlIO input (?)
245 $r=HTML::Entities::decode($r);
246 # FIXME: detect charset from <meta> tag: "Content-type: text/html; charset=<???>"
248 elsif ($mime_type eq "application/pgp-encrypted"
249 && (my $filename=mimehead($first)->mime_attr("Content-Disposition.filename"))
251 # first part contains just "Version: 1" as of GnuPG v1.0.4 (GNU/Linux)
254 if ((my $charset=mimehead($first)->mime_attr("Content-Type.charset"))) {
255 my $cstocs=Cz::Cstocs->new($charset,"ascii");
256 $r=&$cstocs($r) if $cstocs; # charset may be unknown
265 return $Audit if !$part && !$Audit->is_mime();
267 # don't use '!$part->parts()' as even 0-parts-multiparts are still multiparts
268 return $part if $part->bodyhandle();
269 return map { (parts_linear($_)); } $part->parts();
274 my($smsi,$smscount)=@_;
276 return "$smsi/$smscount:" if $smscount>1;
282 my($ignorenewmail,$smscount,%args)=@_;
286 if (!$ignorenewmail) { # send by mail
287 $l-=length("Z emailu FIXME SMSmailError: ");
288 $l-=length(smsbuild($_,$smscount));
291 $l-=6; # 154 is the max length before split; why?
299 my($squeezed,$smscount,@lens)=@_;
301 $smscount=POSIX::ceil($smscount/5);
302 for my $smsi (0..$smscount-1) {
303 my $len=$lens[$smsi];
304 $squeezed=~/^.{0,$len}/s;
307 return 0 if 3!=@SMSwebRcpt;
309 open F,"$HOME/priv/WWW-SMS-$SMSwebRcpt_username.pwd" or return 0;
313 my $sms=WWW::SMS->new(@SMSwebRcpt,$frag,"username"=>$SMSwebRcpt_username,"passwd"=>$pwd);
314 for ($sms->gateways("sorted"=>"reliability")) {
315 last if $sms->send($_);
316 Sys::Syslog::syslog("warning","Web SMS send failed: %s",$WWW::SMS::Error);
317 my $void=$WWW::SMS::Error; # Prevent: Name "WWW::SMS::Error" used only once
325 my($squeezed,$smscount,@lens)=@_;
332 my($ignorenewmail,$smscount,%args)=@_;
334 my $text=PerlMail::Config::audit_sms(
335 "subject"=>unmime($Audit->subject()),
336 "from"=>[ Mail::Address->parse(unmime($Audit->from())) ],
337 "body"=>substr(body_simple(),0,$MaxBodySMS*(1+0.25*$smscount)),
340 ($texthead,$text)=@$text if ref $text;
341 do { print "$texthead\n$text\n"; return; } if $opt_smstest;
342 my @lens=smslens($ignorenewmail,$smscount,%args);
344 $maxlen+=$_ for (@lens);
346 for my $squeeze (@sms_squeezes) {
348 Lingua::EN::Squeeze::SqueezeControl($_) if defined ($_=$squeeze->{"SqueezeControl"});
349 $Lingua::EN::Squeeze::SQZ_OPTIMIZE_LEVEL or 1; # prevent: Name "$_" used only once: possible typo
350 $Lingua::EN::Squeeze::SQZ_OPTIMIZE_LEVEL=$_ if defined ($_=$squeeze->{"SQZ_OPTIMIZE_LEVEL"});
351 $squeezed=Lingua::EN::Squeeze::SqueezeText($text);
353 last if $maxlen>=length($texthead.$squeezed);
355 $squeezed=substr $texthead.$squeezed,0,$maxlen; # strip if we passed thru last for() above
357 for ($smscount=0;$recalclen<length $squeezed;$smscount++) {
358 $recalclen+=$lens[$smscount];
360 my $func=($ignorenewmail ? \&smssend_web : \&smssend_mail);
361 &$func($squeezed,$smscount,@lens);
366 my($ignorenewmail,@args)=@_;
368 return if !$opt_smstest && !$opt_idle && defined useridle();
370 return $_ if $_=smssend(1,@args); # web
371 return $_ if !$ignorenewmail && ($_=smssend(0,@args)); # mail
372 warn "Unable to SMSsend the mail";
379 return "<???>" if !defined($_) || /^\s*$/s;
382 return $_ if length($_)<128;
383 return substr($_,0,128)."...";
386 our $profile_eval_depth=0;
392 die "Nesting profile: $name" if 0x10<=(local $profile_eval_depth=$profile_eval_depth+1);
393 return @$name if ref $name;
394 if (!exists $audit_profile{$name}) {
395 cluck "Profile not found: $name";
398 my @this=@{$audit_profile{$name}};
399 return (profile_eval($'),@this[1..$#this]) if $this[0] && $this[0]=~/^=/;
407 return join(",",map({ $_->name() or $_->address(); } Mail::Address->parse($text)));
416 for (${$_}[1],"iso-8859-2") {
417 last if $_ && ($cstocs=Cz::Cstocs->new($_,"ascii"));
420 } MIME::Words::decode_mimewords($text));
423 # $folder: "$folder; comment"
424 # $profile as profile_eval($name)
427 my($folder,$profile,%args)=@_;
429 $profile=$store_profile if !$profile;
430 my %do=map({ (!/=/ ? ($_=>1) : ($`=>$')); } profile_eval($profile));
431 Sys::Syslog::syslog("info","%s%s%s: %s: %s",
432 (!$Dry ? "" : "--dry: "),
433 (!$store_ignore ? "" : "IGNORED[$store_ignore]: "),
434 map({ cut($_); } $folder,address_show(unmime($Audit->from())),unmime($Audit->subject())),
436 if $do{"syslog"} || $Dry;
438 $folder="$Mail/".$' if $folder=~/^=/;
439 push @AuditStored,$folder if $do{"did"};
440 return if $store_ignore || $Dry;
441 $DoBell++ if $do{"bell"};
442 write_message($folder) or die;
443 smssend_tryall $store_ignorenewmail,$do{"sms"},%args if $do{"sms"};
448 # no &$funcref=>did smth in this block
449 # &$funcref,@funcargs
452 my($funcref,@funcargs)=@_;
454 return @AuditStored!=$did_last if !$funcref;
455 local $did_last=@AuditStored;
456 &$funcref(@funcargs);
457 return @AuditStored!=$did_last;
465 open F,$filename or confess "open $filename: $!";
466 print F $Message or confess "write $filename: $!";
467 close F or confess "close $filename: $!";
471 # Never use Mail::Audit->store() as it will reformat MIME bodies and possibly corrupt OpenPGP!
476 return 1 if $Dry; # simulate OK
478 open F,">>$folder" or do { warn "Append \"$folder\": $!"; return 0; };
481 ($_=$Audit->_audit_get_lock(\*F,$folder)) and do { warn "Lock \"$folder\": $!"; last; };
482 seek F,0,IO::Handle::SEEK_END or do { warn "Seek-end \"$folder\": $!"; last; };
483 # FIXME: Check for '^From ' to not to rely on our network peer
484 print F $Message or do { warn "Write to \"$folder\": $!"; last; };
485 do { print F "\n"; warn "Missing trailing newline, fixed"; } if $Message!~/\n$/s;
486 close F or do { warn "Close \"$folder\""; last; };
489 warn "MAIL DROPPED for folder: $folder";
500 $message=~s/(\n)(From )/$1>$2/sg;
501 local $Message=$message;
502 # Cannot call 'local' for our-imported variable:
503 my $Audit_save=$Audit;
504 $Audit=Mail::Audit->new(
505 "emergency"=>"$Mail/emergency",
506 "data"=>[map("$_\n",split("\n",$message))],
507 "log"=>"$HOME/.perlmail.log",
510 local @AuditStored=();
511 do { smssend 0,$opt_smstest; return; } if $opt_smstest;
512 write_message("$Mail/input") or die;
513 PerlMail::Config::audit();
514 warn 'Corrupted $_, repaired' if defined($save_)!=defined($_) || (defined($_) && $save_ ne $_);
525 $err=$? if !defined $err;
526 return undef() if !WIFEXITED($?);
527 return undef() if WIFSIGNALED($?);
528 return undef() if WIFSTOPPED($?);
529 return 0 if !WEXITSTATUS($?);
530 return $isspam||1 if 1==WEXITSTATUS($?); # isspam
531 cluck "Possible FIXME or your system is broken (WEXITSTATUS==".WEXITSTATUS($?).")";
532 return 0; # simulate as not spam
535 # return: true (error-message or "1") if is spam
540 #$cmd||="nice spamassassin --exit-code 1 --mbox";
541 $cmd||="spamc -c -s 50000000";
542 # spamassassin has the specified exit code if IS spam, code 0 if NOT spam
543 # See &_spamchildcode for the code 1.
545 local $SIG{"PIPE"}=sub { warn "spamassassin gave me SIGPIPE: broken pipe"; };
546 # prevent Razor2's: Can't call method "log" on unblessed reference at Razor2/Client/Agent.pm line 212.
547 local $ENV{"HOME"}=$HOME;
548 # 2>/dev/null to prevent error messages to corrupt inetd() output of perlmail-accept(1)
549 open CHILD,"|$cmd &>/dev/null"
550 # Workaround: spamassassin-3.1.3-1.fc5
551 #.q{|awk '/^X-Spam-Flag: YES$/{if (!body) exit 1;}/^$/{body=1;}'}
553 #." >/dev/null 2>/dev/null"
555 print CHILD $Message;
557 return _spamchildcode;
560 # NOTE: returns undef() if !wantarray and the first header is unrecognized
561 # Returns always HOST:IP pair(s).
565 for my $hdr ($Audit->head->get("Received")) {
566 my($for)=($hdr=~/\bfor\s+\<?(\S+)\>?\b/);
567 return $for if !wantarray();
568 push @r,$for if $for;
569 my($from,$fromaddr)=($hdr=~/\bfrom\s+(?:(\S+)\b.*?)??\[((?:\d{1,3}\.){3}\d{1,3})\]/);
570 $from=$fromaddr if !defined $from;
571 push @r,"$from:$fromaddr" if $from;
576 # Extended Mail::Audit::MAPS
577 # $domain,$full,[$timeout]
578 # Returns false if valid, code if spam detected.
581 my($domain,$full,$timeout)=@_;
584 $Mail::Audit::MAPS::host=$domain;
585 for my $host (Received_for()) {
586 next if $host!~/^([^:@]*):/;
588 # $1 is DNS name, $ip is IP address
589 next if $alternates_host{$1}; # leave only foreign hosts
590 next if $dnsbl_whitelist{$ip};
594 # package My::Audit::Faked;
595 # sub received { return @{$_[0]->{"received"}}; }
597 # my $self_faked=Mail::Audit->new();
598 # $self_faked->{"received"}=["[$ip]"];
599 # bless $self_faked,"My::Audit::Faked";
600 # my $code=Mail::Audit::rblcheck($self_faked,$timeout);
601 my $code=$Audit->rblcheck($timeout);
604 # Some 0.0.0.0 etc. found for <linux-kernel@>, see: &Mail::Audit::MAPS::_checkit
605 # Do not: $code!='1 Invalid IP address '
607 return $code if $code ne '1 Invalid IP address ';
612 # Returns true if IS virus; the message will contain the virus name
617 $cmd||='nice clamscan --no-summary -';
618 # clamscan has exit code 1 if IS virus , code 0 if NOT virus
619 # Do not use IPC::Open2 as it would try to use our STDERR which is not valid by: local *STDERR;
621 local $SIG{"PIPE"}=sub { warn "clamscan '$cmd' gave me SIGPIPE: broken pipe"; };
622 my $pid=open3(\*WR,\*RD,\*ERR,$cmd.' 2>&1')
623 or do { cluck "IPC::Open3 $cmd: $!"; return 0; };
625 close WR or do { cluck "close WR of $cmd: $!"; return 0; };
626 my $status=do { local $/=undef(); <RD>; };
627 close RD or do { cluck "close RD of $cmd: $!"; return 0; };
628 # Do not: $status.=do { local $/=undef(); <ERR>; };
629 # close ERR or do { cluck "close ERR of $cmd: $!"; return 0; };
630 # (FIXME) as it causes: Use of uninitialized value in <HANDLE>
631 # waitpid fills $? for: &_spamchildcode
632 local $SIG{"ALRM"}=sub { warn "Timeout $clamscan_waitpid_timeout sec waiting for child $cmd"; };
633 alarm $clamscan_waitpid_timeout;
635 # as it would not be enough for clamscan(1) even after all close-s above.
636 my $pidcheck=waitpid($pid,0);
639 $pidcheck && $pidcheck==$pid
640 or do { cluck "waitpid for $cmd returned $pidcheck!=$pid"; return 0; };
641 $status=~s/^stdin: //mg;
642 # Prevent: LibClamAV Warning: PGP encoded attachment not scanned
643 $status=~s/^.*\bwarning:.*\n//img;
645 return $status if $status ne "OK" && $status;
646 return _spamchildcode $err,$status;
653 next if !(my $key=(/^alias\s+(\S+)\s+/)[0]);
654 for my $addrobj (Mail::Address->parse($')) {
655 my $addr=$addrobj->address();
656 my $ref=\$r{"\L$addr"};
657 $$ref=$key if !$$ref; # use always the first occurence to prefer nicks
663 # FIXME: host may get multiple recipients and thus not showing "for <...>"
664 # FIXME: muttrc_get("from") is too strict
665 sub store_muttrc_alternates
667 my($prefix,$profile)=@_;
669 my $alternates=muttrc_get("alternates") or return;
670 my $alternatesre=qr/$alternates/si;
671 my $From=muttrc_get("from") or return;
672 my $Fromre=qr/^\Q$From\E$/si;
673 my $Fromobj=parseone $From or return;
674 warn "'From' \"$From\" not matched by 'alternates': $alternatesre"
675 if $From!~/$alternates/si;
676 for my $for (reverse Received_for()) {
677 $for=~s/:.*$//; # strip IP address here
678 my $forobj=parseone $for;
679 if ($forobj && $forobj->host()) {
680 # it is 'for' our primary address
681 next if lc($forobj->host()) eq lc($Fromobj->host()); # or 'return'? shouldn't matter
683 next if !$alternates_host{lc $for} && $for!~/$alternatesre/si;
684 store "$prefix\L$for",($profile || []);
690 # $header: !ref => $Audit->get($header)
691 # $maybeaddress: qr/regex/i
692 # $maybeaddress: "string"
693 # $maybeaddress: "<Regexp:regex>" # hack :-(
694 # $maybeaddress: "<user@host>"
695 # $maybeaddress: "<user@>"
696 # $maybeaddress: "<@host>"
699 my($re,$justone,$header,$maybeaddress)=@_;
702 $header=join(",",&$header());
705 $header=$Audit->get($header);
707 return 0 if !$header;
708 return $header=~/$maybeaddress/i if "Regexp" eq ref $maybeaddress;
709 return $header=~/$re/i if !defined(my $want=($maybeaddress=~/^\<(.*)\>$/)[0]);
710 my @parsed=Mail::Address->parse($header);
711 warn "'mailto:' forbidden in pattern: $want" if $want=~/^\Qmailto:\E/;
712 return 0 if $justone && 1!=@parsed;
714 if ($want=~/^Regexp:/)
715 { $_->address()=~/$'/i; }
717 { $_->user() =~/^(?:\Qmailto:\E)?\Q$`\E/i; }
719 { $_->host() =~/^\Q$'\E/i; }
721 { $_->address()=~/^(?:\Qmailto:\E)?\Q$want\E/i; }
727 my($header,$substr)=@_;
729 return _headercore(qr/\Q$substr\E/i,0,$header,$substr);
734 my($header,$string)=@_;
736 cluck if !defined $string;
737 return _headercore(qr/\Q$string\E/i,1,$header,$string);
745 my $text=$Audit->get($header);
747 while (my($from,$to)=each(%$map)) {
748 $text=~s/\b\Q$from\E\b/$to/gsi;
750 return if $text eq $orig;
751 $Audit->put_header("X-PerlMail-header_remap-$header",$orig);
752 $Audit->replace_header($header,$text);
756 use Net::Cmd qw(CMD_OK CMD_MORE);
758 package My::Net::SMTP::LMTP;
760 our @ISA=qw(Net::SMTP);
762 use Net::Cmd qw(CMD_OK);
763 use Carp qw(confess cluck);
766 # as it would not set {'net_smtp_esmtp'}
767 sub _EHLO { shift->command("LHLO", @_)->response() == CMD_OK }
771 my($self,$func,@args)=@_;
773 do { return $_ if defined $_; } for $self->$func(@args);
782 my($admin_user,$admin_pwd,$user_from,$user_to)=@_;
784 my $lmtp=My::Net::SMTP::LMTP->clucked("new","localhost","Port"=>"lmtp",
787 bless $lmtp,"My::Net::SMTP::LMTP";
790 # $lmtp->auth(Authen::SASL->new(
791 # "mechanism"=>"PLAIN",
793 # "user"=>$admin_user,
794 # "pass"=>$admin_pwd,
795 # # Prevent: "authname"=>$admin_user
796 # # as it causes: DIE: Unknown callback: 'authname'. (user|auth|language|pass)
798 # FIXME: Authentication hack:
799 $lmtp->command("AUTH PLAIN")->response()==CMD_MORE
800 or do { cluck "auth announce"; return; };
801 $lmtp->clucked("command",encode_base64($user_from."\x00".$admin_user."\x00".$admin_pwd)) or return;
802 $lmtp->clucked("mail",$user_from) or return;
803 $lmtp->clucked("to",$user_to) or return;
804 $lmtp->clucked("data"); # Do not: or return;
805 # Prevent: 554 5.6.0 Message contains invalid header
806 (my $data=$Message)=~s/\AFrom .*\r?\n//;
807 $lmtp->clucked("datasend",$data) or return;
808 $lmtp->clucked("dataend") or return;
809 $lmtp->clucked("quit") or return;
815 $Getopt::Long::ignorecase=0;
816 die "GetOptions error" if !Getopt::Long::GetOptions(
817 "inetd" ,sub { $opt_mode=\&inetd; },
818 "stdin" ,sub { $opt_mode=\&stdin; },
819 "single!" ,\$opt_single,
821 "smstest:s",sub { $opt_mode=\&stdin; $opt_smstest=($_[1] || 1); },
823 "idletest" ,sub { syslogging_restore(); print((defined($_=useridle()) ? $_ : "<undef>")."\n"); exit 0; },
824 "muttrc" ,sub { syslogging_restore(); print scalar muttrc(); exit 0; },
826 # "Excessive arguments" checked in &inetd
827 die "Missing mode" if !$opt_mode;
829 %alternates_host=map((lc($_)=>1),@alternates_host);
830 %dnsbl_whitelist=map(( $_ =>1),@dnsbl_whitelist);