PageRenderTime 48ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/qmailadmin/autoreply.pl

http://github.com/webmin/webmin
Perl | 476 lines | 403 code | 24 blank | 49 comment | 80 complexity | 42e1c02c34e86053440759e0a510d717 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-3.0, CC-BY-SA-3.0
  1. #!/usr/local/bin/perl
  2. # autoreply.pl
  3. # Simple autoreply script. Command line arguments are :
  4. # autoreply-file username alternate-file
  5. # Read sendmail module config
  6. $ENV{'PATH'} = "/bin:/usr/bin:/sbin:/usr/sbin";
  7. $p = -l $0 ? readlink($0) : $0;
  8. $p =~ /^(.*)\/[^\/]+$/;
  9. $moddir = $1;
  10. %config = &read_config_file("$moddir/config");
  11. # If this isn't the sendmail module, try it
  12. if (!$config{'sendmail_path'} || !-x $config{'sendmail_path'}) {
  13. $moddir =~ s/([^\/]+)$/sendmail/;
  14. %config = &read_config_file("$moddir/config");
  15. }
  16. if (!$config{'sendmail_path'} || !-x $config{'sendmail_path'}) {
  17. # Make some guesses about sendmail
  18. if (-x "/usr/sbin/sendmail") {
  19. %config = ( 'sendmail_path' => '/usr/sbin/sendmail' );
  20. }
  21. elsif (-x "/usr/local/sbin/sendmail") {
  22. %config = ( 'sendmail_path' => '/usr/local/sbin/sendmail' );
  23. }
  24. elsif (-x "/opt/csw/lib/sendmail") {
  25. %config = ( 'sendmail_path' => '/opt/csw/lib/sendmail' );
  26. }
  27. elsif (-x "/usr/lib/sendmail") {
  28. %config = ( 'sendmail_path' => '/usr/lib/sendmail' );
  29. }
  30. else {
  31. die "Failed to find sendmail or config file";
  32. }
  33. }
  34. # read headers and body
  35. $lnum = 0;
  36. while(<STDIN>) {
  37. $headers .= $_;
  38. s/\r|\n//g;
  39. if (/^From\s+(\S+)/ && $lnum == 0) {
  40. # Magic From line
  41. $fromline = $1;
  42. }
  43. elsif (/^(\S+):\s+(.*)/) {
  44. $header{lc($1)} = $2;
  45. $lastheader = lc($1);
  46. }
  47. elsif (/^\s+(.*)/ && $lastheader) {
  48. $header{$lastheader} .= $_;
  49. }
  50. elsif (!$_) { last; }
  51. $lnum++;
  52. }
  53. while(<STDIN>) {
  54. $body .= $_;
  55. }
  56. if ($header{'x-webmin-autoreply'} ||
  57. $header{'auto-submitted'} && lc($header{'auto-submitted'}) ne 'no') {
  58. print STDERR "Cancelling autoreply to an autoreply\n";
  59. exit 0;
  60. }
  61. if ($header{'x-spam-flag'} =~ /^Yes/i || $header{'x-spam-status'} =~ /^Yes/i) {
  62. print STDERR "Cancelling autoreply to message already marked as spam\n";
  63. exit 0;
  64. }
  65. if ($header{'x-mailing-list'} ||
  66. $header{'list-id'} ||
  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
  72. print STDERR "Cancelling autoreply to message from mailing list\n";
  73. exit 0;
  74. }
  75. if ($header{'from'} =~ /postmaster|mailer-daemon/i ||
  76. $fromline =~ /postmaster|mailer-daemon|<>/ ) {
  77. # Do nothing if post is a bounce
  78. print STDERR "Cancelling autoreply to bounce message\n";
  79. exit 0;
  80. }
  81. # work out the correct to address
  82. @to = ( &split_addresses($header{'to'}),
  83. &split_addresses($header{'cc'}),
  84. &split_addresses($header{'bcc'}) );
  85. $to = $to[0]->[0];
  86. foreach $t (@to) {
  87. if ($t->[0] =~ /^([^\@\s]+)/ && $1 eq $ARGV[1] ||
  88. $t->[0] eq $ARGV[1]) {
  89. $to = $t->[0];
  90. }
  91. }
  92. # build list of default reply headers
  93. $rheader{'From'} = $to;
  94. $rheader{'To'} = $header{'reply-to'} ? $header{'reply-to'}
  95. : $header{'from'};
  96. $rheader{'Subject'} = "Autoreply to $header{'subject'}";
  97. $rheader{'X-Webmin-Autoreply'} = 1;
  98. $rheader{'X-Originally-To'} = $header{'to'};
  99. chop($host = `hostname`);
  100. $rheader{'Message-Id'} = "<".time().".".$$."\@".$host.">";
  101. # read the autoreply file (or alternate)
  102. if (open(AUTO, "<".$ARGV[0]) ||
  103. $ARGV[2] && open(AUTO, "<".$ARGV[2])) {
  104. while(<AUTO>) {
  105. s/\$SUBJECT/$header{'subject'}/g;
  106. s/\$FROM/$header{'from'}/g;
  107. s/\$TO/$to/g;
  108. s/\$DATE/$header{'date'}/g;
  109. s/\$BODY/$body/g;
  110. if (/^(\S+):\s*(.*)/ && !$doneheaders) {
  111. if ($1 eq "No-Autoreply-Regexp") {
  112. push(@no_regexp, $2);
  113. }
  114. elsif ($1 eq "Must-Autoreply-Regexp") {
  115. push(@must_regexp, $2);
  116. }
  117. elsif ($1 eq "Autoreply-File") {
  118. push(@files, $2);
  119. }
  120. else {
  121. $rheader{$1} = $2;
  122. $rheaders .= $_;
  123. }
  124. }
  125. else {
  126. $rbody .= $_;
  127. $doneheaders = 1;
  128. }
  129. }
  130. close(AUTO);
  131. }
  132. else {
  133. $rbody = "Failed to open autoreply file $ARGV[0] : $!";
  134. }
  135. if ($header{'x-original-to'} && $rheader{'No-Forward-Reply'}) {
  136. # Don't autoreply to a forwarded email
  137. ($ot) = &split_addresses($header{'x-original-to'});
  138. if ($ot->[0] =~ /^([^\@\s]+)/ && $1 ne $ARGV[1] &&
  139. $ot->[0] ne $ARGV[1]) {
  140. print STDERR "Cancelling autoreply to forwarded message\n";
  141. exit 0;
  142. }
  143. }
  144. # Open the replies tracking DBM, if one was set
  145. my $rtfile = $rheader{'Reply-Tracking'};
  146. if ($rtfile) {
  147. $track_replies = dbmopen(%replies, $rtfile, 0700);
  148. eval { $replies{"test\@example.com"} = 1; };
  149. if ($@) {
  150. # DBM is corrupt! Clear it
  151. dbmclose(%replies);
  152. unlink($rtfile.".dir");
  153. unlink($rtfile.".pag");
  154. unlink($rtfile.".db");
  155. $track_replies = dbmopen(%replies, $rtfile, 0700);
  156. }
  157. }
  158. if ($track_replies) {
  159. # See if we have replied to this address before
  160. $period = $rheader{'Reply-Period'} || 60*60;
  161. ($from) = &split_addresses($header{'from'});
  162. if ($from) {
  163. $lasttime = $replies{$from->[0]};
  164. $now = time();
  165. if ($now < $lasttime+$period) {
  166. # Autoreplied already in this period .. just halt
  167. print STDERR "Already autoreplied at $lasttime which ",
  168. "is less than $period ago\n";
  169. exit 0;
  170. }
  171. $replies{$from->[0]} = $now;
  172. }
  173. }
  174. delete($rheader{'Reply-Tracking'});
  175. delete($rheader{'Reply-Period'});
  176. # Check if we are within the requested time range
  177. if ($rheader{'Autoreply-Start'} && time() < $rheader{'Autoreply-Start'} ||
  178. $rheader{'Autoreply-End'} && time() > $rheader{'Autoreply-End'}) {
  179. # Nope .. so do nothing
  180. print STDERR "Outside of autoreply window of ",
  181. "$rheader{'Autoreply-Start'}-$rheader{'Autoreply-End'}\n";
  182. exit 0;
  183. }
  184. delete($rheader{'Autoreply-Start'});
  185. delete($rheader{'Autoreply-End'});
  186. # Check if there is a deny list, and if so don't send a reply
  187. @fromsplit = &split_addresses($header{'from'});
  188. if (@fromsplit) {
  189. $from = $fromsplit[0]->[0];
  190. ($fromuser, $fromdom) = split(/\@/, $from);
  191. foreach $n (split(/\s+/, $rheader{'No-Autoreply'})) {
  192. if ($n =~ /^(\S+)\@(\S+)$/ && lc($from) eq lc($n) ||
  193. $n =~ /^\*\@(\S+)$/ && lc($fromdom) eq lc($1) ||
  194. $n =~ /^(\S+)\@\*$/ && lc($fromuser) eq lc($1) ||
  195. $n =~ /^\*\@\*(\S+)$/ && lc($fromdom) =~ /$1$/i ||
  196. $n =~ /^(\S+)\@\*(\S+)$/ && lc($fromuser) eq lc($1) &&
  197. lc($fromdom) =~ /$2$/i) {
  198. exit(0);
  199. }
  200. }
  201. delete($rheader{'No-Autoreply'});
  202. }
  203. # Check if message matches one of the deny regexps, or doesn't match a
  204. # required regexp
  205. foreach $re (@no_regexp) {
  206. if ($re =~ /\S/ && $headers =~ /$re/i) {
  207. print STDERR "Skipping due to match on $re\n";
  208. exit(0);
  209. }
  210. }
  211. if (@must_regexp) {
  212. my $found = 0;
  213. foreach $re (@must_regexp) {
  214. if ($headers =~ /$re/i) {
  215. $found++;
  216. }
  217. }
  218. if (!$found) {
  219. print STDERR "Skipping due to no match on ",
  220. join(" ", @must_regexp),"\n";
  221. exit(0);
  222. }
  223. }
  224. # if spamassassin is installed, feed the email to it
  225. $spam = &has_command("spamassassin");
  226. if ($spam) {
  227. $temp = "/tmp/autoreply.spam.$$";
  228. unlink($temp);
  229. open(SPAM, "| $spam >$temp 2>/dev/null");
  230. print SPAM $headers;
  231. print SPAM $body;
  232. close(SPAM);
  233. $isspam = undef;
  234. open(SPAMOUT, "<".$temp);
  235. while(<SPAMOUT>) {
  236. if (/^X-Spam-Status:\s+Yes/i) {
  237. $isspam = 1;
  238. last;
  239. }
  240. last if (!/\S/);
  241. }
  242. close(SPAMOUT);
  243. unlink($temp);
  244. if ($isspam) {
  245. print STDERR "Not autoreplying to spam\n";
  246. exit 0;
  247. }
  248. }
  249. # Read attached files
  250. foreach $f (@files) {
  251. local $/ = undef;
  252. if (!open(FILE, "<".$f)) {
  253. print STDERR "Failed to open $f : $!\n";
  254. exit(1);
  255. }
  256. $data = <FILE>;
  257. close(FILE);
  258. $f =~ s/^.*\///;
  259. $type = &guess_mime_type($f)."; name=\"$f\"";
  260. $disp = "inline; filename=\"$f\"";
  261. push(@attach, { 'headers' => [ [ 'Content-Type', $type ],
  262. [ 'Content-Disposition', $disp ],
  263. [ 'Content-Transfer-Encoding', 'base64' ]
  264. ],
  265. 'data' => $data });
  266. }
  267. # Work out the content type and encoding
  268. $type = $rbody =~ /<html[^>]*>|<body[^>]*>/i ? "text/html" : "text/plain";
  269. $cs = $rheader{'Charset'};
  270. delete($rheader{'Charset'});
  271. if ($rbody =~ /[\177-\377]/) {
  272. # High-ascii
  273. $enc = "quoted-printable";
  274. $encrbody = &quoted_encode($rbody);
  275. $type .= "; charset=".($cs || "UTF-8");
  276. }
  277. else {
  278. $enc = undef;
  279. $encrbody = $rbody;
  280. $type .= "; charset=$cs" if ($cs);
  281. }
  282. # run sendmail and feed it the reply
  283. ($rfrom) = &split_addresses($rheader{'From'});
  284. if ($rfrom->[0]) {
  285. open(MAIL, "|$config{'sendmail_path'} -t -f".quotemeta($rfrom->[0]));
  286. }
  287. else {
  288. open(MAIL, "|$config{'sendmail_path'} -t -f".quotemeta($to));
  289. }
  290. foreach $h (keys %rheader) {
  291. print MAIL "$h: $rheader{$h}\n";
  292. }
  293. # Create the message body
  294. if (!@attach) {
  295. # Just text, so no encoding is needed
  296. if ($enc) {
  297. print MAIL "Content-Transfer-Encoding: $enc\n";
  298. }
  299. if (!$rheader{'Content-Type'}) {
  300. print MAIL "Content-Type: $type\n";
  301. }
  302. print MAIL "\n";
  303. print MAIL $encrbody;
  304. }
  305. else {
  306. # Need to send a multi-part MIME message
  307. print MAIL "MIME-Version: 1.0\n";
  308. $bound = "bound".time();
  309. $ctype = "multipart/mixed";
  310. print MAIL "Content-Type: $ctype; boundary=\"$bound\"\n";
  311. print MAIL "\n";
  312. $bodyattach = { 'headers' => [ [ 'Content-Type', $type ], ],
  313. 'data' => $encrbody };
  314. if ($enc) {
  315. push(@{$bodyattach->{'headers'}},
  316. [ 'Content-Transfer-Encoding', $enc ]);
  317. }
  318. splice(@attach, 0, 0, $bodyattach);
  319. # Send attachments
  320. print MAIL "This is a multi-part message in MIME format.","\n";
  321. $lnum++;
  322. foreach $a (@attach) {
  323. print MAIL "\n";
  324. print MAIL "--",$bound,"\n";
  325. local $enc;
  326. foreach $h (@{$a->{'headers'}}) {
  327. print MAIL $h->[0],": ",$h->[1],"\n";
  328. $enc = $h->[1]
  329. if (lc($h->[0]) eq 'content-transfer-encoding');
  330. $lnum++;
  331. }
  332. print MAIL "\n";
  333. $lnum++;
  334. if (lc($enc) eq 'base64') {
  335. local $enc = &encode_base64($a->{'data'});
  336. $enc =~ s/\r//g;
  337. print MAIL $enc;
  338. }
  339. else {
  340. $a->{'data'} =~ s/\r//g;
  341. $a->{'data'} =~ s/\n\.\n/\n\. \n/g;
  342. print MAIL $a->{'data'};
  343. if ($a->{'data'} !~ /\n$/) {
  344. print MAIL "\n";
  345. }
  346. }
  347. }
  348. print MAIL "\n";
  349. print MAIL "--",$bound,"--","\n";
  350. print MAIL "\n";
  351. }
  352. close(MAIL);
  353. # split_addresses(string)
  354. # Splits a comma-separated list of addresses into [ email, real-name, original ]
  355. # triplets
  356. sub split_addresses
  357. {
  358. local (@rv, $str = $_[0]);
  359. while(1) {
  360. if ($str =~ /^[\s,]*(([^<>\(\)"\s]+)\s+\(([^\(\)]+)\))(.*)$/) {
  361. # An address like foo@bar.com (Fooey Bar)
  362. push(@rv, [ $2, $3, $1 ]);
  363. $str = $4;
  364. }
  365. elsif ($str =~ /^[\s,]*("([^"]+)"\s*<([^\s<>,]+)>)(.*)$/ ||
  366. $str =~ /^[\s,]*(([^<>\@]+)\s+<([^\s<>,]+)>)(.*)$/ ||
  367. $str =~ /^[\s,]*(([^<>\@]+)<([^\s<>,]+)>)(.*)$/ ||
  368. $str =~ /^[\s,]*(([^<>\[\]]+)\s+\[mailto:([^\s\[\]]+)\])(.*)$/||
  369. $str =~ /^[\s,]*(()<([^<>,]+)>)(.*)/ ||
  370. $str =~ /^[\s,]*(()([^\s<>,]+))(.*)/) {
  371. # Addresses like "Fooey Bar" <foo@bar.com>
  372. # Fooey Bar <foo@bar.com>
  373. # Fooey Bar<foo@bar.com>
  374. # Fooey Bar [mailto:foo@bar.com]
  375. # <foo@bar.com>
  376. # <group name>
  377. # foo@bar.com
  378. push(@rv, [ $3, $2 eq "," ? "" : $2, $1 ]);
  379. $str = $4;
  380. }
  381. else {
  382. last;
  383. }
  384. }
  385. return @rv;
  386. }
  387. # encode_base64(string)
  388. # Encodes a string into base64 format
  389. sub encode_base64
  390. {
  391. local $res;
  392. pos($_[0]) = 0; # ensure start at the beginning
  393. while ($_[0] =~ /(.{1,57})/gs) {
  394. $res .= substr(pack('u57', $1), 1)."\n";
  395. chop($res);
  396. }
  397. $res =~ tr|\` -_|AA-Za-z0-9+/|;
  398. local $padding = (3 - length($_[0]) % 3) % 3;
  399. $res =~ s/.{$padding}$/'=' x $padding/e if ($padding);
  400. return $res;
  401. }
  402. # guess_mime_type(filename)
  403. sub guess_mime_type
  404. {
  405. local ($file) = @_;
  406. return $file =~ /\.gif/i ? "image/gif" :
  407. $file =~ /\.(jpeg|jpg)/i ? "image/jpeg" :
  408. $file =~ /\.txt/i ? "text/plain" :
  409. $file =~ /\.(htm|html)/i ? "text/html" :
  410. $file =~ /\.doc/i ? "application/msword" :
  411. $file =~ /\.xls/i ? "application/vnd.ms-excel" :
  412. $file =~ /\.ppt/i ? "application/vnd.ms-powerpoint" :
  413. $file =~ /\.(mpg|mpeg)/i ? "video/mpeg" :
  414. $file =~ /\.avi/i ? "video/x-msvideo" :
  415. $file =~ /\.(mp2|mp3)/i ? "audio/mpeg" :
  416. $file =~ /\.wav/i ? "audio/x-wav" :
  417. "application/octet-stream";
  418. }
  419. sub read_config_file
  420. {
  421. local %config;
  422. if (open(CONF, "<".$_[0])) {
  423. while(<CONF>) {
  424. if (/^(\S+)=(.*)/) {
  425. $config{$1} = $2;
  426. }
  427. }
  428. close(CONF);
  429. }
  430. return %config;
  431. }
  432. # quoted_encode(text)
  433. # Encodes text to quoted-printable format
  434. sub quoted_encode
  435. {
  436. local $t = $_[0];
  437. $t =~ s/([=\177-\377])/sprintf("=%2.2X",ord($1))/ge;
  438. return $t;
  439. }
  440. sub has_command
  441. {
  442. local ($cmd) = @_;
  443. if ($cmd =~ /^\//) {
  444. return -x $cmd ? $cmd : undef;
  445. }
  446. else {
  447. foreach my $d (split(":", $ENV{'PATH'}), "/usr/bin", "/usr/local/bin") {
  448. return "$d/$cmd" if (-x "$d/$cmd");
  449. }
  450. return undef;
  451. }
  452. }