1#!/usr/bin/perl -T 2 3#------------------------------------------------------------------------------ 4# This is amavisd-submit, a simple demonstrational program, taking an email 5# message on stdin and submiting it to amavisd daemon. It is functionally 6# much like the old amavis.c helper program, except that it talks a new 7# AM.PDP protocol with the amavisd daemon. See README.protocol for the 8# description of AM.PDP protocol. 9# 10# Author: Mark Martinec <Mark.Martinec@ijs.si> 11# 12# Copyright (c) 2004,2010-2014, Mark Martinec 13# All rights reserved. 14# 15# Redistribution and use in source and binary forms, with or without 16# modification, are permitted provided that the following conditions 17# are met: 18# 1. Redistributions of source code must retain the above copyright notice, 19# this list of conditions and the following disclaimer. 20# 2. Redistributions in binary form must reproduce the above copyright notice, 21# this list of conditions and the following disclaimer in the documentation 22# and/or other materials provided with the distribution. 23# 24# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 28# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 29# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 31# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 32# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34# POSSIBILITY OF SUCH DAMAGE. 35# 36# The views and conclusions contained in the software and documentation are 37# those of the authors and should not be interpreted as representing official 38# policies, either expressed or implied, of the Jozef Stefan Institute. 39 40# (the above license is the 2-clause BSD license, also known as 41# a "Simplified BSD License", and pertains to this program only) 42# 43# Patches and problem reports are welcome. 44# The latest version of this program is available at: 45# http://www.ijs.si/software/amavisd/ 46#------------------------------------------------------------------------------ 47 48# Usage: 49# amavisd-submit sender recip1 recip2 recip3 ... <email.msg 50# (should run under the same GID as amavisd, to make files accessible to it) 51# 52# To be placed in amavisd.conf: 53# $interface_policy{'SOCK'} = 'AM.PDP'; 54# $policy_bank{'AM.PDP'} = { protocol=>'AM.PDP' }; 55# $unix_socketname = '/var/amavis/amavisd.sock'; 56 57use warnings; 58use warnings FATAL => 'utf8'; 59no warnings 'uninitialized'; 60use strict; 61use re 'taint'; 62use IO::Socket; 63use IO::File; 64use File::Temp (); 65use Time::HiRes (); 66 67BEGIN { 68 use vars qw($VERSION); $VERSION = 2.101; 69 use vars qw($log_level $socketname $tempbase $io_socket_module_name); 70 71 72### USER CONFIGURABLE: 73 74 $log_level = 0; 75 $tempbase = '/var/amavis/tmp'; # where to create a temp directory with a msg 76 77 $socketname = '/var/amavis/amavisd.sock'; 78# $socketname = '127.0.0.1:9998'; 79# $socketname = '[::1]:9998'; 80 81### END OF USER CONFIGURABLE 82 83 84 # load a suitable sockets module 85 if ($socketname =~ m{^/}) { 86 require IO::Socket::UNIX; 87 $io_socket_module_name = 'IO::Socket::UNIX'; 88 } elsif (eval { require IO::Socket::IP }) { 89 # prefer using module IO::Socket::IP if available, 90 $io_socket_module_name = 'IO::Socket::IP'; 91 } elsif (eval { require IO::Socket::INET6 }) { 92 # otherwise fall back to IO::Socket::INET6 93 $io_socket_module_name = 'IO::Socket::INET6'; 94 } elsif (eval { require IO::Socket::INET }) { 95 $io_socket_module_name = 'IO::Socket::INET'; 96 } 97 $io_socket_module_name or die "No suitable socket module available"; 98} 99 100sub sanitize_str { 101 my($str, $keep_eol) = @_; 102 my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t', 103 "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\'); 104 if ($keep_eol) { 105 $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ? 106 exists($map{$1}) ? $map{$1} : 107 sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; 108 } else { 109 $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ? 110 exists($map{$1}) ? $map{$1} : 111 sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; 112 } 113 $str; 114} 115 116sub ll($) { 117 my($level) = @_; 118 $level <= $log_level; 119} 120 121sub do_log($$;@) { 122 my($level, $errmsg, @args) = @_; 123 $errmsg = sprintf($errmsg,@args) if @args; 124 print STDERR sanitize_str($errmsg),"\n" if $level <= $log_level; 125} 126 127sub proto_decode($) { 128 my($str) = @_; 129 $str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg; 130 $str; 131} 132 133sub proto_encode($@) { 134 my($attribute_name,@strings) = @_; local($1); 135 $attribute_name =~ # encode all but alfanumerics, '_' and '-' 136 s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg; 137 for (@strings) { # encode % and nonprintables 138 s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg; 139 } 140 $attribute_name . '=' . join(' ',@strings); 141} 142 143sub ask_amavisd($$) { 144 my($sock,$query_ref) = @_; 145 my(@encoded_query) = 146 map { /^([^=]+)=(.*)\z/s; proto_encode($1,$2) } @$query_ref; 147 do_log(2, "> %s", $_) for @encoded_query; 148 $sock->print( map($_."\015\012", (@encoded_query,'')) ) 149 or die "Can't write response to socket: $!"; 150 $sock->flush or die "Can't flush on socket: $!"; 151 my(%attr); 152 local($/) = "\015\012"; # set line terminator to CRLF 153 # must not use \r and \n, which may not be \015 and \012 on certain platforms 154 do_log(2, "waiting for response"); 155 while(<$sock>) { 156 last if /^\015\012\z/; # end of response 157 if (/^ ([^=\000\012]*?) (=|:[ \t]*) ([^\012]*?) \015\012 \z/xsi) { 158 my $attr_name = proto_decode($1); 159 my $attr_val = proto_decode($3); 160 if (!exists $attr{$attr_name}) { $attr{$attr_name} = [] } 161 push(@{$attr{$attr_name}}, $attr_val); 162 } 163 } 164 if (!defined($_) && $! != 0) { die "read from socket failed: $!" } 165 \%attr; 166} 167 168sub usage(;$) { 169 my($msg) = @_; 170 print STDERR $msg,"\n\n" if $msg ne ''; 171 my $prog = $0; $prog =~ s{^.*/(?=[^/]+\z)}{}; 172 print STDERR "$prog version $VERSION\n"; 173 die "Usage: \$ $prog sender recip1 recip2 ... < email.msg\n"; 174} 175 176# Main program starts here 177 178 $SIG{INT} = sub { die "\n" }; # do the END code block when interrupted 179 $SIG{TERM} = sub { die "\n" }; # do the END code block when killed 180 umask(0027); # set our preferred umask 181 182 @ARGV >= 1 or usage("Not enough arguments"); 183 184 my($sock, %sock_args); 185 if ($io_socket_module_name eq 'IO::Socket::UNIX') { 186 %sock_args = (Type => &SOCK_STREAM, Peer => $socketname); 187 } else { 188 %sock_args = (Type => &SOCK_STREAM, PeerAddr => $socketname); 189 } 190 do_log(2, "Connecting to %s using a module %s", 191 $socketname, $io_socket_module_name); 192 $sock = $io_socket_module_name->new(%sock_args) 193 or die "Can't connect to a $io_socket_module_name socket $socketname: $!\n"; 194 195 my $tempdir = File::Temp::tempdir('amavis-XXXXXXXXXX', DIR => $tempbase); 196 defined $tempdir && $tempdir ne '' 197 or die "Can't create a temporary directory: $!"; 198 chmod(0750, $tempdir) 199 or die "Can't change protection on directory $tempdir: $!"; 200 my $fname = "$tempdir/email.txt"; 201 202 # copy message from stdin to a file email.txt in the temporary directory 203 204 my $fh = IO::File->new; 205 $fh->open($fname, O_CREAT|O_EXCL|O_RDWR, 0640) 206 or die "Can't create file $fname: $!"; 207 my($nbytes,$buff); 208 while (($nbytes=read(STDIN,$buff,32768)) > 0) { 209 $fh->print($buff) or die "Error writing to $fname: $!"; 210 } 211 defined $nbytes or die "Error reading mail file: $!"; 212 $fh->close or die "Error closing $fname: $!"; 213 close(STDIN) or die "Error closing STDIN: $!"; 214 215 my(@query) = ( 216 'request=AM.PDP', 217 "mail_file=$fname", 218 "tempdir=$tempdir", 219 'tempdir_removed_by=server', 220 map("sender=<$_>", shift(@ARGV)), 221 map("recipient=<$_>", @ARGV), 222# 'delivery_care_of=server', 223# 'protocol_name=ESMTP', 224# 'helo_name=b.example.com', 225# 'client_address=::1', 226 ); 227 my $attr_ref = ask_amavisd($sock,\@query); 228 if (ll(2)) { 229 for my $attr_name (keys %$attr_ref) { 230 for my $attr_val (@{$attr_ref->{$attr_name}}) { 231 do_log(2, "< %s=%s", $attr_name,$attr_val); 232 } 233 } 234 } 235 my($setreply,$exit_code); 236 $setreply = $attr_ref->{'setreply'}->[0] if $attr_ref->{'setreply'}; 237 $exit_code = $attr_ref->{'exit_code'}->[0] if $attr_ref->{'exit_code'}; 238 if (defined $setreply && $setreply =~ /^2\d\d/) { # all ok 239 do_log(1, "%s", $setreply); 240 } elsif (!defined($setreply)) { 241 do_log(0, "Error, missing 'setreply' attribute"); 242 } else { 243 do_log(0, "%s", $setreply); 244 } 245 # may do another request here if needed ... 246 $sock->close or die "Error closing socket: $!"; 247 $exit_code = 0 if $exit_code==99; # same thing in this case, both is ok 248 exit 0+$exit_code; 249 250END { 251 # remove a temporary file and directory if necessary 252 if (defined $fname && -f $fname) { 253 unlink $fname or warn "Error deleting file $fname: $!"; 254 } 255 if (defined $tempdir && -d $tempdir) { 256 rmdir $tempdir or warn "Error deleting temporary directory $tempdir: $!"; 257 } 258} 259