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