+&lmtp_deliver for cyrus-imapd side delivery.
[PerlMail.git] / perlmail-accept
index dad2bd3..75dfa4a 100755 (executable)
@@ -69,6 +69,8 @@ require HTTP::Request;
 require LWP::UserAgent;
 use URI::Escape 'uri_escape';
 require WWW::SMS;
+require Authen::SASL;  # Sanity check for &Net::SMTP::auth
+use MIME::Base64;
 
 
 our($Message,@AuditStored,$DoBell);
@@ -89,6 +91,7 @@ my $opt_mode;
 my $opt_smstest;       # 1 or $smscount
 my $opt_idle;
 my $opt_dry;
+my $opt_single;
 
 
 sub process;
@@ -101,7 +104,7 @@ sub stdin
        local $_;
        while (<>) {
                die "Invalid 'From ' line: $_" if $message eq "" && !/^From /;
-               if (/^From / && $message) {
+               if (!$opt_single && /^From / && $message) {
                        process $message;
                        $message="";
                        }
@@ -130,8 +133,8 @@ sub inetd
                $length==($_=read STDIN,$message,$length) or confess "Got $_ out of required $length bytes";
                $length==length $message or confess "False read return ".length($message)." instead of $length";
                {
-                       local *STDOUT;
-                       local *STDERR;
+                       local *STDOUT;  # FIXME: fd's inherited by spawned processes are not closed this way!
+                       local *STDERR;  # FIXME: fd's inherited by spawned processes are not closed this way!
                        local $DoBell=0;
                        process $message;
                        if ($DoBell) {
@@ -487,32 +490,26 @@ my($message)=@_;
 # utility functions:
 
 # return: true (error-message or "1") if is spam
-sub razor2
+sub spamassassin
 {
-       # razor-check has exit code 1 if NOT spam, code 0 if IS spam
+my($cmd)=@_;
+
+       $cmd||="$HOME/bin/spamassassin --exit-code";
+       # spamassassin has exit code 1 if IS spam, code 0 if NOT spam
        local *CHILD;
-       local $SIG{"PIPE"}=sub { warn "razor2 gave me SIGPIPE: broken pipe"; };
+       local $SIG{"PIPE"}=sub { warn "spamassassin gave me SIGPIPE: broken pipe"; };
        # prevent Razor2's: Can't call method "log" on unblessed reference at Razor2/Client/Agent.pm line 212.
        local $ENV{"HOME"}=$HOME;
-       open CHILD,'|'
-                                       .'('.'(razor-check 2>&1;echo >&3 $?)'
-                                                       .'|sed "s/^/razor-check: /"'
-                                                       .'|logger -t "perlmail['.$$.']" -p mail.crit'
-                                                       .') 3>&1'
-                                       .'|exit `cat`'
+       # 2>/dev/null to prevent error messages to corrupt inetd() output of perlmail-accept(1)
+       open CHILD,"|$cmd --mbox >/dev/null 2>/dev/null"
                        or return 0;
        print CHILD $Message;
-       my $return;
-       {
-               local $/=undef();
-               $return=<CHILD> || 1;
-               }
-       close CHILD;
+       my $return=close CHILD;
        return undef() if !WIFEXITED($?);
        return undef() if  WIFSIGNALED($?);
        return undef() if  WIFSTOPPED($?);
-       return undef() if WEXITSTATUS($?);
-       return $return; # is-spam
+       return 1       if  WEXITSTATUS($?);     # is-spam
+       return 0;       # not-spam
 }
 
 # NOTE: returns undef() if !wantarray and the first header is unrecognized
@@ -661,6 +658,63 @@ my($header,$map)=@_;
        $Audit->replace_header($header,$text);
 }
 
+# LMTP engine:
+use Net::Cmd qw(CMD_OK CMD_MORE);
+{
+       package My::Net::SMTP::LMTP;
+       require Net::SMTP;
+       our @ISA=qw(Net::SMTP);
+       use Net::SMTP;
+       use Net::Cmd qw(CMD_OK);
+       use Carp qw(confess cluck);
+
+       # Do not: sub _HELO
+       # as it would not set {'net_smtp_esmtp'}
+       sub _EHLO { shift->command("LHLO", @_)->response()  == CMD_OK }
+
+       sub clucked
+       {
+       my($self,$func,@args)=@_;
+
+               do { return $_ if defined $_; } for $self->$func(@args);
+               cluck $func;
+               return;
+       }
+}
+
+
+sub lmtp_deliver
+{
+my($admin_user,$admin_pwd,$user_from,$user_to)=@_;
+
+       my $lmtp=My::Net::SMTP::LMTP->clucked("new","localhost","Port"=>"lmtp",
+#                      "Debug"=>1,
+                       ) or return;
+       bless $lmtp,"My::Net::SMTP::LMTP";
+# Prevent:
+# due to:
+#      $lmtp->auth(Authen::SASL->new(
+#                      "mechanism"=>"PLAIN",
+#                      "callback"=>{
+#                                      "user"=>$admin_user,
+#                                      "pass"=>$admin_pwd,
+#                                      # Prevent: "authname"=>$admin_user
+#                                      # as it causes: DIE: Unknown callback: 'authname'. (user|auth|language|pass)
+#                                      }));
+       # FIXME: Authentication hack:
+       $lmtp->command("AUTH PLAIN")->response()==CMD_MORE
+                       or do { cluck "auth announce"; return; };
+       $lmtp->clucked("command",encode_base64($user_from."\x00".$admin_user."\x00".$admin_pwd)) or return;
+       $lmtp->clucked("mail",$user_from) or return;
+       $lmtp->clucked("to",$user_to) or return;
+       $lmtp->clucked("data"); # Do not: or return;
+       # Prevent: 554 5.6.0 Message contains invalid header
+       (my $data=$Message)=~s/\AFrom .*\r?\n//;
+       $lmtp->clucked("datasend",$data) or return;
+       $lmtp->clucked("dataend") or return;
+       $lmtp->clucked("quit") or return;
+}
+
 
 # MAIN
 
@@ -668,6 +722,7 @@ $Getopt::Long::ignorecase=0;
 die "GetOptions error" if !Getopt::Long::GetOptions(
                  "inetd"    ,sub { $opt_mode=\&inetd; },
                  "stdin"    ,sub { $opt_mode=\&stdin; },
+                 "single!"  ,\$opt_single,
                  "dry"      ,\$opt_dry,
                  "smstest:s",sub { $opt_mode=\&stdin; $opt_smstest=($_[1] || 1); },
                  "idle!"    ,\$opt_idle,