You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

vacation.pl 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. #!/usr/bin/perl -w
  2. #
  3. # Virtual Vacation 4.0r1
  4. #
  5. # $Revision: 1857 $
  6. # Originally by Mischa Peters <mischa at high5 dot net>
  7. #
  8. # Copyright (c) 2002 - 2005 High5!
  9. # Licensed under GPL for more info check GPL-LICENSE.TXT
  10. #
  11. # Additions:
  12. # 2004/07/13 David Osborn <ossdev at daocon.com>
  13. # strict, processes domain level aliases, more
  14. # subroutines, send reply from original to address
  15. #
  16. # 2004/11/09 David Osborn <ossdev at daocon.com>
  17. # Added syslog support
  18. # Slightly better logging which includes messageid
  19. # Avoid infinite loops with domain aliases
  20. #
  21. # 2005-01-19 Troels Arvin <troels at arvin.dk>
  22. # PostgreSQL-version.
  23. # Normalized DB schema from one vacation table ("vacation")
  24. # to two ("vacation", "vacation_notification"). Uses
  25. # referential integrity CASCADE action to simplify cleanup
  26. # when a user is no longer on vacation.
  27. # Inserting variables into queries stricly by prepare()
  28. # to try to avoid SQL injection.
  29. # International characters are now handled well.
  30. #
  31. # 2005-01-21 Troels Arvin <troels at arvin.dk>
  32. # Uses the Email::Valid package to avoid sending notices
  33. # to obviously invalid addresses.
  34. #
  35. # 2007-08-15 David Goodwin <david at palepurple.co.uk>
  36. # Use the Perl Mail::Sendmail module for sending mail
  37. # Check for headers that start with blank lines (patch from forum)
  38. #
  39. # 2007-08-20 Martin Ambroz <amsys at trustica.cz>
  40. # Added initial Unicode support
  41. #
  42. # 2008-05-09 Fabio Bonelli <fabiobonelli at libero.it>
  43. # Properly handle failed queries to vacation_notification.
  44. # Fixed log reporting.
  45. #
  46. # 2008-07-29 Patch from Luxten to add repeat notification after timeout. See:
  47. # https://sourceforge.net/tracker/index.php?func=detail&aid=2031631&group_id=191583&atid=937966
  48. #
  49. # 2008-08-01 Luigi Iotti <luigi at iotti dot biz>
  50. # Use envelope sender/recipient instead of using
  51. # From: and To: header fields;
  52. # Support to good vacation behavior as in
  53. # http://www.irbs.net/internet/postfix/0707/0954.html
  54. # (needs to be tested);
  55. #
  56. # 2008-08-04 David Goodwin <david at palepurple dot co dot uk>
  57. # Use Log4Perl
  58. # Added better testing (and -t option)
  59. #
  60. # 2009-06-29 Stevan Bajic <stevan at bajic.ch>
  61. # Add Mail::Sender for SMTP auth + more flexibility
  62. #
  63. # 2009-07-07 Stevan Bajic <stevan at bajic.ch>
  64. # Add better alias lookups
  65. # Check for more heades from Anti-Virus/Anti-Spam solutions
  66. #
  67. # 2009-08-10 Sebastian <reg9009 at yahoo dot de>
  68. # Adjust SQL query for vacation timeframe. It is now possible to set from/until date for vacation message.
  69. #
  70. # 2012-04-1 Nikolaos Topp <info at ichier.de>
  71. # Add configuration parameter $smtp_client in order to get mails through
  72. # postfix helo-checks, using check_helo_access whitelist without permitting 'localhost' default style stuff
  73. #
  74. # 2012-04-19 Jan Kruis <jan at crossreference dot nl>
  75. # change SQL query for vacation into function.
  76. # Add sub get_interval()
  77. # Gives the user the option to set the interval time ( 0 = one reply, 1 = autoreply, > 1 = Delay reply )
  78. # See https://sourceforge.net/tracker/?func=detail&aid=3508083&group_id=191583&atid=937966
  79. #
  80. # 2012-06-18 Christoph Lechleitner <christoph.lechleitner@iteg.at>
  81. # Add capability to include the subject of the original mail in the subject of the vacation message.
  82. # A good vacation subject could be: 'Re: $SUBJECT'
  83. # Also corrected log entry about "Already informed ..." to show the $orig_from, not $email
  84. #
  85. # Requirements - the following perl modules are required:
  86. # DBD::Pg or DBD::mysql
  87. # Mail::Sender, Email::Valid MIME::Charset, Log::Log4perl, Log::Dispatch, MIME::EncWords and GetOpt::Std
  88. #
  89. # You may install these via CPAN, or through your package tool.
  90. # CPAN: 'perl -MCPAN -e shell', then 'install Module::Whatever'
  91. #
  92. # On Debian based systems :
  93. # libmail-sender-perl
  94. # libdbd-pg-perl
  95. # libemail-valid-perl
  96. # libmime-perl
  97. # liblog-log4perl-perl
  98. # liblog-dispatch-perl
  99. # libgetopt-argvfile-perl
  100. # libmime-charset-perl (currently in testing, see instructions below)
  101. # libmime-encwords-perl (currently in testing, see instructions below)
  102. #
  103. # Note: When you use this module, you may start seeing error messages
  104. # like "Cannot insert a duplicate key into unique index
  105. # vacation_notification_pkey" in your system logs. This is expected
  106. # behavior, and not an indication of trouble (see the "already_notified"
  107. # subroutine for an explanation).
  108. #
  109. # You must also have the Email::Valid and MIME-tools perl-packages
  110. # installed. They are available in some package collections, under the
  111. # names 'perl-Email-Valid' and 'perl-MIME-tools', respectively.
  112. # One such package collection (for Linux) is:
  113. # http://dag.wieers.com/home-made/apt/packages.php
  114. #
  115. use utf8;
  116. use DBI;
  117. use MIME::Base64 qw(encode_base64);
  118. use Encode qw(encode decode);
  119. use MIME::EncWords qw(:all);
  120. use Email::Valid;
  121. use strict;
  122. use Mail::Sender;
  123. use Getopt::Std;
  124. use Log::Log4perl qw(get_logger :levels);
  125. use File::Basename;
  126. # ========== begin configuration ==========
  127. # IMPORTANT: If you put passwords into this script, then remember
  128. # to restrict access to the script, so that only the vacation user
  129. # can read it.
  130. # db_type - uncomment one of these
  131. our $db_type = 'Pg';
  132. #our $db_type = 'mysql';
  133. # leave empty for connection via UNIX socket
  134. our $db_host = '';
  135. # connection details
  136. our $db_username = 'user';
  137. our $db_password = 'password';
  138. our $db_name = 'postfix';
  139. our $vacation_domain = 'autoreply.example.org';
  140. # smtp server used to send vacation e-mails
  141. our $smtp_server = 'localhost';
  142. our $smtp_server_port = 25;
  143. # this is the helo we [the vacation script] use on connection; you may need to change this to your hostname or something,
  144. # depending upon what smtp helo restrictions you have in place within Postfix.
  145. our $smtp_client = 'localhost';
  146. # SMTP authentication protocol used for sending.
  147. # Can be 'PLAIN', 'LOGIN', 'CRAM-MD5' or 'NTLM'
  148. # see "perldoc Mail::Sender" (search for "auth") for more options and details
  149. # Leave it blank if you don't use authentication
  150. our $smtp_auth = undef;
  151. # username used to login to the server
  152. our $smtp_authid = 'someuser';
  153. # password used to login to the server
  154. our $smtp_authpwd = 'somepass';
  155. # This specifies the mail 'from' name which is shown to recipients of vacation replies.
  156. # If you leave it empty, the vacation mail will contain:
  157. # From: <original@recipient.domain>
  158. # If you specify something here you'd instead see something like :
  159. # From: Some Friendly Name <original@recipient.domain>
  160. our $friendly_from = '';
  161. # use TLS for the SMTP connection?
  162. # while in general this would be a good idea, TLS with Mail::Sender 0.8.22 is buggy - https://rt.cpan.org/Public/Bug/Display.html?id=85438
  163. our $smtp_tls_allowed = 0;
  164. # Set to 1 to enable logging to syslog.
  165. our $syslog = 0;
  166. # path to logfile, when empty logging is suppressed
  167. # change to e.g. /dev/null if you want nothing logged.
  168. # if we can't write to this, and $log_to_file is 1 (below) the script will abort.
  169. our $logfile='/var/log/vacation.log';
  170. # 2 = debug + info, 1 = info only, 0 = error only
  171. our $log_level = 2;
  172. # Whether to log to file or not, 0 = do not write to a log file
  173. our $log_to_file = 0;
  174. # notification interval, in seconds
  175. # set to 0 to notify only once
  176. # e.g. 1 day ...
  177. #our $interval = 60*60*24;
  178. # disabled by default
  179. our $interval = 0;
  180. # Send vacation mails to do-not-reply email addresses.
  181. # By default vacation email addresses will be sent.
  182. # For now emails from bounce|do-not-reply|facebook|linkedin|list-|myspace|twitter won't
  183. # be answered when $custom_noreply_pattern is set to 1.
  184. # default = 0
  185. our $custom_noreply_pattern = 0;
  186. our $noreply_pattern = 'bounce|do-not-reply|facebook|linkedin|list-|myspace|twitter';
  187. # instead of changing this script, you can put your settings to /etc/mail/postfixadmin/vacation.conf
  188. # or /etc/postfixadmin/vacation.conf just use Perl syntax there to fill the variables listed above
  189. # (without the "our" keyword). Example:
  190. # $db_username = 'mail';
  191. if (-f '/etc/mail/postfixadmin/vacation.conf') {
  192. require '/etc/mail/postfixadmin/vacation.conf';
  193. } elsif (-f '/etc/postfixadmin/vacation.conf') {
  194. require '/etc/postfixadmin/vacation.conf';
  195. }
  196. # =========== end configuration ===========
  197. if($log_to_file == 1) {
  198. if (( ! -w $logfile ) && (! -w dirname($logfile))) {
  199. # Cannot log; no where to write to.
  200. die("Cannot create logfile : $logfile");
  201. }
  202. }
  203. my ($from, $to, $cc, $replyto , $subject, $messageid, $lastheader, $smtp_sender, $smtp_recipient, %opts, $test_mode, $logger);
  204. $subject='';
  205. $messageid='unknown';
  206. # Setup a logger...
  207. #
  208. getopts('f:t:', \%opts) or die "Usage: $0 [-t yes] -f sender -- recipient\n\t-t for testing only\n";
  209. $opts{f} and $smtp_sender = $opts{f} or die '-f sender not present on command line';
  210. $test_mode = 0;
  211. $opts{t} and $test_mode = 1;
  212. $smtp_recipient = shift or die 'recipient not given on command line';
  213. my $log_layout = Log::Log4perl::Layout::PatternLayout->new('%d %p> %F:%L %M - %m%n');
  214. if($test_mode == 1) {
  215. $logger = get_logger();
  216. # log to stdout
  217. my $appender = Log::Log4perl::Appender->new('Log::Dispatch::Screen');
  218. $appender->layout($log_layout);
  219. $logger->add_appender($appender);
  220. $logger->debug('Test mode enabled');
  221. } else {
  222. $logger = get_logger();
  223. if($log_to_file == 1) {
  224. # log to file
  225. my $appender = Log::Log4perl::Appender->new(
  226. 'Log::Dispatch::File',
  227. filename => $logfile,
  228. mode => 'append');
  229. $appender->layout($log_layout);
  230. $logger->add_appender($appender);
  231. }
  232. if($syslog == 1) {
  233. my $syslog_appender = Log::Log4perl::Appender->new(
  234. 'Log::Dispatch::Syslog',
  235. facility => 'mail',
  236. );
  237. $logger->add_appender($syslog_appender);
  238. }
  239. }
  240. # change to $DEBUG, $INFO or $ERROR depending on how much logging you want.
  241. $logger->level($ERROR);
  242. if($log_level == 1) {
  243. $logger->level($INFO);
  244. }
  245. if($log_level == 2) {
  246. $logger->level($DEBUG);
  247. }
  248. binmode (STDIN,':encoding(UTF-8)');
  249. my $dbh;
  250. if ($db_host) {
  251. $dbh = DBI->connect("DBI:$db_type:dbname=$db_name;host=$db_host","$db_username", "$db_password", { RaiseError => 1 });
  252. } else {
  253. $dbh = DBI->connect("DBI:$db_type:dbname=$db_name","$db_username", "$db_password", { RaiseError => 1 });
  254. }
  255. if (!$dbh) {
  256. $logger->error('Could not connect to database'); # eval { } etc better here?
  257. exit(0);
  258. }
  259. my $db_true; # MySQL and PgSQL use different values for TRUE, and unicode support...
  260. if ($db_type eq 'mysql') {
  261. $dbh->do('SET CHARACTER SET utf8;');
  262. $db_true = '1';
  263. } else { # Pg
  264. $dbh->do("SET CLIENT_ENCODING TO 'UTF8'");
  265. $db_true = 'True';
  266. }
  267. # used to detect infinite address lookup loops
  268. my $loopcount=0;
  269. #
  270. # Get interval_time for email user from the vacation table
  271. #
  272. sub get_interval {
  273. my ($to) = @_;
  274. my $query = qq{SELECT interval_time FROM vacation WHERE email=? };
  275. my $stm = $dbh->prepare($query) or panic_prepare($query);
  276. $stm->execute($to) or panic_execute($query," 'email='$to'");
  277. my $rv = $stm->rows;
  278. if ($rv == 1) {
  279. my @row = $stm->fetchrow_array;
  280. my $interval = $row[0] ;
  281. return $interval ;
  282. } else {
  283. return 0 ;
  284. }
  285. }
  286. sub already_notified {
  287. my ($to, $from) = @_;
  288. my $logger = get_logger();
  289. my $query;
  290. # delete old notifications
  291. if ($db_type eq 'Pg') {
  292. $query = qq{DELETE FROM vacation_notification USING vacation WHERE vacation.email = vacation_notification.on_vacation AND on_vacation = ? AND notified = ? AND notified_at < vacation.activefrom;};
  293. } else { # mysql
  294. $query = qq{DELETE vacation_notification.* FROM vacation_notification LEFT JOIN vacation ON vacation.email = vacation_notification.on_vacation WHERE on_vacation = ? AND notified = ? AND notified_at < vacation.activefrom};
  295. }
  296. my $stm = $dbh->prepare($query);
  297. if (!$stm) {
  298. $logger->error("Could not prepare query (trying to delete old vacation notifications) :'$query' to: $to, from:$from");
  299. return 1;
  300. }
  301. $stm->execute($to,$from);
  302. $query = qq{INSERT into vacation_notification (on_vacation,notified) values (?,?)};
  303. $stm = $dbh->prepare($query);
  304. if (!$stm) {
  305. $logger->error("Could not prepare query '$query' to: $to, from:$from");
  306. return 1;
  307. }
  308. $stm->{'PrintError'} = 0;
  309. $stm->{'RaiseError'} = 0;
  310. if (!$stm->execute($to,$from)) {
  311. my $e=$dbh->errstr;
  312. # Violation of a primary key constraint may happen here, and that's
  313. # fine. All other error conditions are not fine, however.
  314. if ($e !~ /(?:_pkey|^Duplicate entry)/) {
  315. $logger->error("Failed to insert into vacation_notification table (to:$to from:$from error:'$e' query:'$query')");
  316. # Let's play safe and notify anyway
  317. return 1;
  318. }
  319. $interval = get_interval($to);
  320. if ($interval) {
  321. if ($db_type eq 'Pg') {
  322. $query = qq{SELECT extract( epoch from (NOW()-notified_at))::int FROM vacation_notification WHERE on_vacation=? AND notified=?};
  323. } else { # mysql
  324. $query = qq{SELECT NOW()-notified_at FROM vacation_notification WHERE on_vacation=? AND notified=?};
  325. }
  326. $stm = $dbh->prepare($query) or panic_prepare($query);
  327. $stm->execute($to,$from) or panic_execute($query,"on_vacation='$to', notified='$from'");
  328. my @row = $stm->fetchrow_array;
  329. my $int = $row[0];
  330. if ($int > $interval) {
  331. $logger->info("[Interval elapsed, sending the message]: From: $from To:$to");
  332. $query = qq{UPDATE vacation_notification SET notified_at=NOW() WHERE on_vacation=? AND notified=?};
  333. $stm = $dbh->prepare($query);
  334. if (!$stm) {
  335. $logger->error("Could not prepare query '$query' (to: '$to', from: '$from')");
  336. return 0;
  337. }
  338. if (!$stm->execute($to,$from)) {
  339. $e=$dbh->errstr;
  340. $logger->error("Error from running query '$query' (to: '$to', from: '$from', error: '$e')");
  341. }
  342. return 0;
  343. } else {
  344. $logger->debug("Notification interval not elapsed; not sending vacation reply (to: '$to', from: '$from')");
  345. return 1;
  346. }
  347. } else {
  348. return 1;
  349. }
  350. }
  351. return 0;
  352. }
  353. #
  354. # Check to see if there is a vacation record against a specific email address.
  355. #
  356. sub check_for_vacation {
  357. my ($email_to_check) =@_;
  358. my $query = qq{SELECT email FROM vacation WHERE email=? and active=$db_true and activefrom <= NOW() and activeuntil >= NOW()};
  359. my $stm = $dbh->prepare($query) or panic_prepare($query);
  360. $stm->execute($email_to_check) or panic_execute($query,"email='$email_to_check'");
  361. my $rv = $stm->rows;
  362. return $rv;
  363. }
  364. # try and determine if email address has vacation turned on; we
  365. # have to do alias searching, and domain aliasing resolution for this.
  366. # If found, return ($num_matches, $real_email);
  367. sub find_real_address {
  368. my ($email) = @_;
  369. my $logger = get_logger();
  370. if (++$loopcount > 20) {
  371. $logger->error("find_real_address loop! (more than 20 attempts!) currently: $email");
  372. exit(1);
  373. }
  374. my $realemail = '';
  375. my $rv = check_for_vacation($email);
  376. # Recipient has vacation
  377. if ($rv == 1) {
  378. $realemail = $email;
  379. $logger->debug("Found '$email' has vacation active");
  380. } else {
  381. my $vemail = $email;
  382. $vemail =~ s/\@/#/g;
  383. $vemail = $vemail . "\@" . $vacation_domain;
  384. $logger->debug("Looking for alias records that '$email' resolves to with vacation turned on");
  385. my $query = qq{SELECT goto FROM alias WHERE address=? AND (goto LIKE ? OR goto LIKE ? OR goto LIKE ? OR goto = ?)};
  386. my $stm = $dbh->prepare($query) or panic_prepare($query);
  387. $stm->execute($email,"$vemail,%","%,$vemail","%,$vemail,%", "$vemail") or panic_execute($query,"address='$email'");
  388. $rv = $stm->rows;
  389. # Recipient is an alias, check if mailbox has vacation
  390. if ($rv == 1) {
  391. my @row = $stm->fetchrow_array;
  392. my $alias = $row[0];
  393. if ($alias =~ /,/) {
  394. for (split(/\s*,\s*/, lc($alias))) {
  395. my $singlealias = $_;
  396. $logger->debug("Found alias \'$singlealias\' for email \'$email\'. Looking if vacation is on for alias.");
  397. $rv = check_for_vacation($singlealias);
  398. # Alias has vacation
  399. if ($rv == 1) {
  400. $realemail = $singlealias;
  401. last;
  402. }
  403. }
  404. } else {
  405. $rv = check_for_vacation($alias);
  406. # Alias has vacation
  407. if ($rv == 1) {
  408. $realemail = $alias;
  409. }
  410. }
  411. # We have to look for alias domain (domain1 -> domain2)
  412. } else {
  413. my ($user, $domain) = split(/@/, $email);
  414. $logger->debug("Looking for alias domain for $domain / $email / $user");
  415. $query = qq{SELECT target_domain FROM alias_domain WHERE alias_domain=?};
  416. $stm = $dbh->prepare($query) or panic_prepare($query);
  417. $stm->execute($domain) or panic_execute($query,"alias_domain='$domain'");
  418. $rv = $stm->rows;
  419. # The domain has a alias domain level alias
  420. if ($rv == 1) {
  421. my @row = $stm->fetchrow_array;
  422. my $alias_domain_dest = $row[0];
  423. ($rv, $realemail) = find_real_address ("$user\@$alias_domain_dest");
  424. # We still have to look for domain level aliases...
  425. } else {
  426. my ($user, $domain) = split(/@/, $email);
  427. $logger->debug("Looking for domain level aliases for $domain / $email / $user");
  428. $query = qq{SELECT goto FROM alias WHERE address=?};
  429. $stm = $dbh->prepare($query) or panic_prepare($query);
  430. $stm->execute("\@$domain") or panic_execute($query,"address='\@$domain'");
  431. $rv = $stm->rows;
  432. # The recipient has a domain level alias
  433. if ($rv == 1) {
  434. my @row = $stm->fetchrow_array;
  435. my $wildcard_dest = $row[0];
  436. my ($wilduser, $wilddomain) = split(/@/, $wildcard_dest);
  437. # Check domain alias
  438. if ($wilduser) {
  439. ($rv, $realemail) = find_real_address ($wildcard_dest);
  440. } else {
  441. ($rv, $realemail) = find_real_address ("$user\@$wilddomain");
  442. }
  443. } else {
  444. $logger->debug("No domain level alias present for $domain / $email / $user");
  445. }
  446. }
  447. }
  448. }
  449. return ($rv, $realemail);
  450. }
  451. # sends the vacation mail to the original sender.
  452. #
  453. sub send_vacation_email {
  454. my ($email, $orig_from, $orig_to, $orig_messageid, $orig_subject, $test_mode) = @_;
  455. my $logger = get_logger();
  456. $logger->debug("Asked to send vacation reply to $email thanks to $orig_messageid");
  457. my $query = qq{SELECT subject,body FROM vacation WHERE email=?};
  458. my $stm = $dbh->prepare($query) or panic_prepare($query);
  459. $stm->execute($email) or panic_execute($query,"email='$email'");
  460. my $rv = $stm->rows;
  461. if ($rv == 1) {
  462. my @row = $stm->fetchrow_array;
  463. if (already_notified($email, $orig_from) == 1) {
  464. $logger->debug("Already notified $orig_from, or some error prevented us from doing so");
  465. return;
  466. }
  467. $logger->debug("Will send vacation response for $orig_messageid: FROM: $email (orig_to: $orig_to), TO: $orig_from; VACATION SUBJECT: $row[0] ; VACATION BODY: $row[1]");
  468. my $subject = $row[0];
  469. $orig_subject = decode("mime-header", $orig_subject);
  470. $subject =~ s/\$SUBJECT/$orig_subject/g;
  471. if ($subject ne $row[0]) {
  472. $logger->debug("Patched Subject of vacation message to: $subject");
  473. }
  474. my $body = $row[1];
  475. my $from = $email;
  476. my $to = $orig_from;
  477. my %smtp_connection;
  478. %smtp_connection = (
  479. 'smtp' => $smtp_server,
  480. 'port' => $smtp_server_port,
  481. 'auth' => $smtp_auth,
  482. 'authid' => $smtp_authid,
  483. 'authpwd' => $smtp_authpwd,
  484. 'tls_allowed' => $smtp_tls_allowed,
  485. 'smtp_client' => $smtp_client,
  486. 'skip_bad_recipients' => 'true',
  487. 'encoding' => 'Base64',
  488. 'ctype' => 'text/plain; charset=UTF-8',
  489. 'headers' => 'Precedence: junk',
  490. 'headers' => 'X-Loop: Postfix Admin Virtual Vacation',
  491. 'on_errors' => 'die', # raise exception on error
  492. );
  493. my %mail;
  494. %mail = (
  495. 'subject' => encode_mimewords($subject, 'Charset', 'UTF-8'),
  496. 'from' => $from,
  497. 'fake_from' => $friendly_from . " <$from>",
  498. 'to' => $to,
  499. 'msg' => encode_base64(encode("UTF-8", $body))
  500. );
  501. if($test_mode == 1) {
  502. $logger->info("** TEST MODE ** : Vacation response sent to $to from $from subject $subject (not) sent\n");
  503. $logger->info(%mail);
  504. return 0;
  505. }
  506. eval {
  507. $Mail::Sender::NO_X_MAILER = 1;
  508. my $sender = new Mail::Sender({%smtp_connection});
  509. $sender->Open({%mail});
  510. $sender->SendLineEnc($body);
  511. $sender->Close();
  512. $logger->debug("Vacation response sent to $to, from $from");
  513. };
  514. if ($@) {
  515. $logger->error("Failed to send vacation response: $@ / " . $Mail::Sender::Error);
  516. }
  517. }
  518. }
  519. # Convert a (list of) email address(es) from RFC 822 style addressing to
  520. # RFC 821 style addressing. e.g. convert:
  521. # "John Jones" <JJones@acme.com>, "Jane Doe/Sales/ACME" <JDoe@acme.com>
  522. # to:
  523. # jjones@acme.com, jdoe@acme.com
  524. sub strip_address {
  525. my ($arg) = @_;
  526. if(!$arg) {
  527. return '';
  528. }
  529. my @ok;
  530. $logger = get_logger();
  531. my @list;
  532. @list = $arg =~ m/([\w\.\-\+\'\=_\^\|\$\/\{\}~\?\*\\&\!`\%]+\@[\w\.\-]+\w+)/g;
  533. foreach(@list) {
  534. #$logger->debug("Checking: $_");
  535. my $temp = Email::Valid->address( -address => $_, -mxcheck => 0);
  536. if($temp) {
  537. push(@ok, $temp);
  538. } else {
  539. $logger->debug("Email not valid : $Email::Valid::Details");
  540. }
  541. }
  542. # remove duplicates
  543. my %seen = ();
  544. my @uniq;
  545. foreach my $item (@ok) {
  546. push(@uniq, $item) unless $seen{$item}++
  547. }
  548. my $result = lc(join(', ', @uniq));
  549. #$logger->debug("Result: $result");
  550. return $result;
  551. }
  552. sub panic_prepare {
  553. my ($arg) = @_;
  554. my $logger = get_logger();
  555. $logger->error("Could not prepare sql statement: '$arg'");
  556. exit(0);
  557. }
  558. sub panic_execute {
  559. my ($arg,$param) = @_;
  560. my $logger = get_logger();
  561. $logger->error("Could not execute sql statement - '$arg' with parameters '$param'");
  562. exit(0);
  563. }
  564. # Make sure the email wasn't sent by someone who could be a mailing list etc; if it was,
  565. # then we abort after appropriate logging.
  566. sub check_and_clean_from_address {
  567. my ($address) = @_;
  568. my $logger = get_logger();
  569. if($address =~ /^(noreply|postmaster|mailer\-daemon|listserv|majordomo|owner\-|request\-|bounces\-)/i ||
  570. $address =~ /\-(owner|request|bounces)\@/i ||
  571. ($custom_noreply_pattern == 1 && $address =~ /^.*($noreply_pattern).*/i) ) {
  572. $logger->debug("sender $address contains $1 - will not send vacation message");
  573. exit(0);
  574. }
  575. $address = strip_address($address);
  576. if($address eq '') {
  577. $logger->error("Address $address is not valid; exiting");
  578. exit(0);
  579. }
  580. #$logger->debug("Address cleaned up to $address");
  581. return $address;
  582. }
  583. ########################### main #################################
  584. # Take headers apart
  585. $cc = '';
  586. $replyto = '';
  587. $logger->debug("Script argument SMTP recipient is : '$smtp_recipient' and smtp_sender : '$smtp_sender'");
  588. while (<STDIN>) {
  589. last if (/^$/);
  590. if (/^\s+(.*)/ and $lastheader) { $$lastheader .= " $1"; next; }
  591. elsif (/^from:\s*(.*)\n$/i) { $from = $1; $lastheader = \$from; }
  592. elsif (/^to:\s*(.*)\n$/i) { $to = $1; $lastheader = \$to; }
  593. elsif (/^cc:\s*(.*)\n$/i) { $cc = $1; $lastheader = \$cc; }
  594. elsif (/^Reply\-to:\s*(.*)\s*\n$/i) { $replyto = $1; $lastheader = \$replyto; }
  595. elsif (/^subject:\s*(.*)\n$/i) { $subject = $1; $lastheader = \$subject; }
  596. elsif (/^message\-id:\s*(.*)\s*\n$/i) { $messageid = $1; $lastheader = \$messageid; }
  597. elsif (/^x\-spam\-(flag|status):\s+yes/i) { $logger->debug("x-spam-$1: yes found; exiting"); exit (0); }
  598. elsif (/^x\-facebook\-notify:/i) { $logger->debug('Mail from facebook, ignoring'); exit(0); }
  599. elsif (/^precedence:\s+(bulk|list|junk)/i) { $logger->debug("precedence: $1 found; exiting"); exit (0); }
  600. elsif (/^x\-loop:\s+postfix\ admin\ virtual\ vacation/i) { $logger->debug('x-loop: postfix admin virtual vacation found; exiting'); exit (0); }
  601. elsif (/^Auto\-Submitted:\s*no/i) { next; }
  602. elsif (/^Auto\-Submitted:/i) { $logger->debug('Auto-Submitted: something found; exiting'); exit (0); }
  603. elsif (/^List\-(Id|Post|Unsubscribe):/i) { $logger->debug("List-$1: found; exiting"); exit (0); }
  604. elsif (/^(x\-(barracuda\-)?spam\-status):\s+(yes)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); }
  605. elsif (/^(x\-dspam\-result):\s+(spam|bl[ao]cklisted)/i) { $logger->debug("$1: $2 found; exiting"); exit (0); }
  606. elsif (/^(x\-(anti|avas\-)?virus\-status):\s+(infected)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); }
  607. elsif (/^(x\-(avas\-spam|spamtest|crm114|razor|pyzor)\-status):\s+(spam)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); }
  608. elsif (/^(x\-osbf\-lua\-score):\s+[0-9\/\.\-\+]+\s+\[([-S])\]/i) { $logger->debug("$1: $2 found; exiting"); exit (0); }
  609. else {$lastheader = '' ; }
  610. }
  611. if($smtp_recipient =~ /\@$vacation_domain/) {
  612. # the regexp used here could probably be improved somewhat, for now hope that people won't use # as a valid mailbox character.
  613. my $tmp = $smtp_recipient;
  614. $tmp =~ s/\@$vacation_domain//;
  615. $tmp =~ s/#/\@/;
  616. $logger->debug("Converted autoreply mailbox back to normal style - from $smtp_recipient to $tmp");
  617. $smtp_recipient = $tmp;
  618. undef $tmp;
  619. }
  620. # If either From: or To: are not set, exit
  621. if(!$from || !$to || !$messageid || !$smtp_sender || !$smtp_recipient) {
  622. $logger->info("One of from=$from, to=$to, messageid=$messageid, smtp sender=$smtp_sender, smtp recipient=$smtp_recipient empty");
  623. exit(0);
  624. }
  625. $logger->debug("Email headers have to: '$to' and From: '$from'");
  626. $to = strip_address($to);
  627. $cc = strip_address($cc);
  628. $from = check_and_clean_from_address($from);
  629. if($replyto ne '') {
  630. # if reply-to is invalid, or looks like a mailing list, then we probably don't want to send a reply.
  631. $replyto = check_and_clean_from_address($replyto);
  632. }
  633. $smtp_sender = check_and_clean_from_address($smtp_sender);
  634. $smtp_recipient = check_and_clean_from_address($smtp_recipient);
  635. if ($smtp_sender eq $smtp_recipient) {
  636. $logger->debug("smtp sender $smtp_sender and recipient $smtp_recipient are the same; aborting");
  637. exit(0);
  638. }
  639. for (split(/,\s*/, lc($to)), split(/,\s*/, lc($cc))) {
  640. my $header_recipient = strip_address($_);
  641. if ($smtp_sender eq $header_recipient) {
  642. $logger->debug("sender header $smtp_sender contains recipient $header_recipient (mailing myself?)");
  643. exit(0);
  644. }
  645. }
  646. my ($rv, $email) = find_real_address($smtp_recipient);
  647. if ($rv == 1) {
  648. $logger->debug("Attempting to send vacation response for: $messageid to: $smtp_sender, $smtp_recipient, $email (test_mode = $test_mode)");
  649. send_vacation_email($email, $smtp_sender, $smtp_recipient, $messageid, $subject, $test_mode);
  650. } else {
  651. $logger->debug("SMTP recipient $smtp_recipient which resolves to $email does not have an active vacation (rv: $rv, email: $email)");
  652. }
  653. 0;
  654. #/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */