3 # Simple autoreply script. Command line arguments are :
4 # autoreply-file username alternate-file
6 # Read sendmail module config
7 $ENV{'PATH'} = "/bin:/usr/bin:/sbin:/usr/sbin";
8 $p = -l $0 ? readlink($0) : $0;
9 $p =~ /^(.*)\/[^\/]+$/;
11 %config = &read_config_file("$moddir/config");
13 # If this isn't the sendmail module, try it
14 if (!$config{'sendmail_path'} || !-x $config{'sendmail_path'}) {
15 $moddir =~ s/([^\/]+)$/sendmail/;
16 %config = &read_config_file("$moddir/config");
19 if (!$config{'sendmail_path'} || !-x $config{'sendmail_path'}) {
20 # Make some guesses about sendmail
21 if (-x "/usr/sbin/sendmail") {
22 %config = ( 'sendmail_path' => '/usr/sbin/sendmail' );
24 elsif (-x "/usr/local/sbin/sendmail") {
25 %config = ( 'sendmail_path' => '/usr/local/sbin/sendmail' );
27 elsif (-x "/opt/csw/lib/sendmail") {
28 %config = ( 'sendmail_path' => '/opt/csw/lib/sendmail' );
30 elsif (-x "/usr/lib/sendmail") {
31 %config = ( 'sendmail_path' => '/usr/lib/sendmail' );
34 die "Failed to find sendmail or config file";
38 # read headers and body
43 if (/^From\s+(\S+)/ && $lnum == 0) {
47 elsif (/^(\S+):\s+(.*)/) {
51 elsif (/^\s+(.*)/ && $lastheader) {
52 $header{$lastheader} .= $_;
60 if ($header{'x-webmin-autoreply'} ||
61 $header{'auto-submitted'} && lc($header{'auto-submitted'}) ne 'no') {
62 print STDERR "Cancelling autoreply to an autoreply\n";
65 if ($header{'x-mailing-list'} ||
67 $header{'precedence'} =~ /junk|bulk|list/i ||
68 $header{'to'} =~ /Multiple recipients of/i ||
69 $header{'from'} =~ /majordomo/i ||
70 $fromline =~ /majordomo/i) {
71 # Do nothing if post is from a mailing list
74 if ($header{'from'} =~ /postmaster|mailer-daemon/i ||
75 $fromline =~ /postmaster|mailer-daemon|<>/ ) {
76 # Do nothing if post is a bounce
80 # if spamassassin is installed, feed the email to it
81 $spam = &has_command("spamassassin");
83 $temp = "/tmp/autoreply.spam.$$";
85 open(SPAM, "| $spam >$temp 2>/dev/null");
92 if (/^X-Spam-Status:\s+Yes/i) {
101 print STDERR "Not autoreplying to spam\n";
106 # work out the correct to address
107 @to = ( &split_addresses($header{'to'}),
108 &split_addresses($header{'cc'}),
109 &split_addresses($header{'bcc'}) );
112 if ($t->[0] =~ /^([^\@\s]+)/ && $1 eq $ARGV[1] ||
113 $t->[0] eq $ARGV[1]) {
118 # build list of default reply headers
119 $rheader{'From'} = $to;
120 $rheader{'To'} = $header{'reply-to'} ? $header{'reply-to'}
122 $rheader{'Subject'} = "Autoreply to $header{'subject'}";
123 $rheader{'X-Webmin-Autoreply'} = 1;
124 $rheader{'X-Originally-To'} = $header{'to'};
125 chop($host = `hostname`);
126 $rheader{'Message-Id'} = "<".time().".".$$."\@".$host.">";
128 # read the autoreply file (or alternate)
129 if (open(AUTO, $ARGV[0]) ||
130 $ARGV[2] && open(AUTO, $ARGV[2])) {
132 s/\$SUBJECT/$header{'subject'}/g;
133 s/\$FROM/$header{'from'}/g;
135 s/\$DATE/$header{'date'}/g;
137 if (/^(\S+):\s*(.*)/ && !$doneheaders) {
138 if ($1 eq "No-Autoreply-Regexp") {
139 push(@no_regexp, $2);
141 elsif ($1 eq "Autoreply-File") {
157 $rbody = "Failed to open autoreply file $ARGV[0] : $!";
160 # Open the replies tracking DBM, if one was set
161 if ($rheader{'Reply-Tracking'}) {
162 $track_replies = dbmopen(%replies, $rheader{'Reply-Tracking'}, 0700);
164 if ($track_replies) {
165 # See if we have replied to this address before
166 $period = $rheader{'Reply-Period'} || 60*60;
167 ($from) = &split_addresses($header{'from'});
169 $lasttime = $replies{$from->[0]};
171 if ($now < $lasttime+$period) {
172 # Autoreplied already in this period .. just halt
175 $replies{$from->[0]} = $now;
178 delete($rheader{'Reply-Tracking'});
179 delete($rheader{'Reply-Period'});
181 # Check if we are within the requested time range
182 if ($rheader{'Autoreply-Start'} && time() < $rheader{'Autoreply-Start'} ||
183 $rheader{'Autoreply-End'} && time() > $rheader{'Autoreply-End'}) {
184 # Nope .. so do nothing
187 delete($rheader{'Autoreply-Start'});
188 delete($rheader{'Autoreply-End'});
190 # Check if there is a deny list, and if so don't send a reply
191 @fromsplit = &split_addresses($header{'from'});
193 $from = $fromsplit[0]->[0];
194 ($fromuser, $fromdom) = split(/\@/, $from);
195 foreach $n (split(/\s+/, $rheader{'No-Autoreply'})) {
196 if ($n =~ /^(\S+)\@(\S+)$/ && lc($from) eq lc($n) ||
197 $n =~ /^\*\@(\S+)$/ && lc($fromdom) eq lc($1) ||
198 $n =~ /^(\S+)\@\*$/ && lc($fromuser) eq lc($1) ||
199 $n =~ /^\*\@\*(\S+)$/ && lc($fromdom) =~ /$1$/i ||
200 $n =~ /^(\S+)\@\*(\S+)$/ && lc($fromuser) eq lc($1) &&
201 lc($fromdom) =~ /$2$/i) {
205 delete($rheader{'No-Autoreply'});
208 # Check if message matches one of the deny regexps
209 foreach $re (@no_regexp) {
210 if ($re =~ /\S/ && $rheaders =~ /$re/i) {
211 print STDERR "Skipping due to match on $re\n";
216 # Read attached files
217 foreach $f (@files) {
219 if (!open(FILE, $f)) {
220 print STDERR "Failed to open $f : $!\n";
226 $type = &guess_mime_type($f)."; name=\"$f\"";
227 $disp = "inline; filename=\"$f\"";
228 push(@attach, { 'headers' => [ [ 'Content-Type', $type ],
229 [ 'Content-Disposition', $disp ],
230 [ 'Content-Transfer-Encoding', 'base64' ]
235 # Work out the content type and encoding
236 $type = $rbody =~ /<html[^>]*>|<body[^>]*>/i ? "text/html" : "text/plain";
237 $cs = $rheader{'Charset'};
238 delete($rheader{'Charset'});
239 if ($rbody =~ /[\177-\377]/) {
241 $enc = "quoted-printable";
242 $encrbody = "ed_encode($rbody);
243 $type .= "; charset=".($cs || "iso-8859-1");
248 $type .= "; charset=$cs" if ($cs);
251 # run sendmail and feed it the reply
252 ($rfrom) = &split_addresses($rheader{'From'});
254 open(MAIL, "|$config{'sendmail_path'} -t -f$rfrom->[0]");
257 open(MAIL, "|$config{'sendmail_path'} -t -f$to");
259 foreach $h (keys %rheader) {
260 print MAIL "$h: $rheader{$h}\n";
263 # Create the message body
265 # Just text, so no encoding is needed
267 print MAIL "Content-Transfer-Encoding: $enc\n";
269 if (!$rheader{'Content-Type'}) {
270 print MAIL "Content-Type: $type\n";
273 print MAIL $encrbody;
276 # Need to send a multi-part MIME message
277 print MAIL "MIME-Version: 1.0\n";
278 $bound = "bound".time();
279 $ctype = "multipart/mixed";
280 print MAIL "Content-Type: $ctype; boundary=\"$bound\"\n";
282 $bodyattach = { 'headers' => [ [ 'Content-Type', $type ], ],
283 'data' => $encrbody };
285 push(@{$bodyattach->{'headers'}},
286 [ 'Content-Transfer-Encoding', $enc ]);
288 splice(@attach, 0, 0, $bodyattach);
291 print MAIL "This is a multi-part message in MIME format.","\n";
293 foreach $a (@attach) {
295 print MAIL "--",$bound,"\n";
297 foreach $h (@{$a->{'headers'}}) {
298 print MAIL $h->[0],": ",$h->[1],"\n";
300 if (lc($h->[0]) eq 'content-transfer-encoding');
305 if (lc($enc) eq 'base64') {
306 local $enc = &encode_base64($a->{'data'});
311 $a->{'data'} =~ s/\r//g;
312 $a->{'data'} =~ s/\n\.\n/\n\. \n/g;
313 print MAIL $a->{'data'};
314 if ($a->{'data'} !~ /\n$/) {
320 print MAIL "--",$bound,"--","\n";
325 # split_addresses(string)
326 # Splits a comma-separated list of addresses into [ email, real-name, original ]
330 local (@rv, $str = $_[0]);
332 if ($str =~ /^[\s,]*(([^<>\(\)\s]+)\s+\(([^\(\)]+)\))(.*)$/) {
333 # An address like foo@bar.com (Fooey Bar)
334 push(@rv, [ $2, $3, $1 ]);
337 elsif ($str =~ /^[\s,]*("([^"]+)"\s*<([^\s<>,]+)>)(.*)$/ ||
338 $str =~ /^[\s,]*(([^<>\@]+)\s+<([^\s<>,]+)>)(.*)$/ ||
339 $str =~ /^[\s,]*(([^<>\@]+)<([^\s<>,]+)>)(.*)$/ ||
340 $str =~ /^[\s,]*(([^<>\[\]]+)\s+\[mailto:([^\s\[\]]+)\])(.*)$/||
341 $str =~ /^[\s,]*(()<([^<>,]+)>)(.*)/ ||
342 $str =~ /^[\s,]*(()([^\s<>,]+))(.*)/) {
343 # Addresses like "Fooey Bar" <foo@bar.com>
344 # Fooey Bar <foo@bar.com>
345 # Fooey Bar<foo@bar.com>
346 # Fooey Bar [mailto:foo@bar.com]
350 push(@rv, [ $3, $2 eq "," ? "" : $2, $1 ]);
360 # encode_base64(string)
361 # Encodes a string into base64 format
365 pos($_[0]) = 0; # ensure start at the beginning
366 while ($_[0] =~ /(.{1,57})/gs) {
367 $res .= substr(pack('u57', $1), 1)."\n";
370 $res =~ tr|\` -_|AA-Za-z0-9+/|;
371 local $padding = (3 - length($_[0]) % 3) % 3;
372 $res =~ s/.{$padding}$/'=' x $padding/e if ($padding);
376 # guess_mime_type(filename)
380 return $file =~ /\.gif/i ? "image/gif" :
381 $file =~ /\.(jpeg|jpg)/i ? "image/jpeg" :
382 $file =~ /\.txt/i ? "text/plain" :
383 $file =~ /\.(htm|html)/i ? "text/html" :
384 $file =~ /\.doc/i ? "application/msword" :
385 $file =~ /\.xls/i ? "application/vnd.ms-excel" :
386 $file =~ /\.ppt/i ? "application/vnd.ms-powerpoint" :
387 $file =~ /\.(mpg|mpeg)/i ? "video/mpeg" :
388 $file =~ /\.avi/i ? "video/x-msvideo" :
389 $file =~ /\.(mp2|mp3)/i ? "audio/mpeg" :
390 $file =~ /\.wav/i ? "audio/x-wav" :
391 "application/octet-stream";
397 if (open(CONF, $_[0])) {
408 # quoted_encode(text)
409 # Encodes text to quoted-printable format
413 $t =~ s/([=\177-\377])/sprintf("=%2.2X",ord($1))/ge;
421 return -x $cmd ? $cmd : undef;
424 foreach my $d (split(":", $ENV{'PATH'}), "/usr/bin", "/usr/local/bin") {
425 return "$d/$cmd" if (-x "$d/$cmd");