Advertisement
Guest User

Untitled

a guest
Jun 16th, 2017
318
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.74 KB | None | 0 0
  1. use DBI;
  2. use MIME::Base64;
  3. use MIME::EncWords qw(:all);
  4. use Email::Valid;
  5. use strict;
  6. use Mail::Sender;
  7. use Getopt::Std;
  8. use Log::Log4perl qw(get_logger :levels);
  9. use File::Basename;
  10.  
  11. # ========== begin configuration ==========
  12.  
  13. # IMPORTANT: If you put passwords into this script, then remember
  14. # to restrict access to the script, so that only the vacation user
  15. # can read it.
  16.  
  17. # db_type - uncomment one of these
  18. our $db_type = 'mysql';
  19. #our $db_type = 'mysql';
  20.  
  21. # leave empty for connection via UNIX socket
  22. our $db_host = '127.0.0.1';
  23.  
  24. # connection details
  25. our $db_username = 'postfix';
  26. our $db_password = 'P@ssw0rd';
  27. our $db_name = 'postfix';
  28.  
  29. our $vacation_domain = 'autoreply.mail-kad.redirectme.net';
  30.  
  31. # smtp server used to send vacation e-mails
  32. our $smtp_server = 'localhost';
  33. our $smtp_server_port = 25;
  34.  
  35. # SMTP authentication protocol used for sending.
  36. # Can be 'PLAIN', 'LOGIN', 'CRAM-MD5' or 'NTLM'
  37. # Leave it blank if you don't use authentification
  38. our $smtp_auth = undef;
  39. # username used to login to the server
  40.  
  41. our $smtp_authid = 'sara@mail-kad.redirectme.net';
  42. # password used to login to the server
  43. our $smtp_authpwd = 'P@ssw0rd';
  44.  
  45. # Set to 1 to enable logging to syslog.
  46. our $syslog = 0;
  47.  
  48. # path to logfile, when empty logging is supressed
  49. # change to e.g. /dev/null if you want nothing logged.
  50. # if we can't write to this, and $log_to_file is 1 (below) the script will abort.
  51. our $logfile='/var/log/vacation.log';
  52. # 2 = debug + info, 1 = info only, 0 = error only
  53. our $log_level = 2;
  54. # Whether to log to file or not, 0 = do not write to a log file
  55. our $log_to_file = 2;
  56.  
  57. # notification interval, in seconds
  58. # set to 0 to notify only once
  59. # e.g. 1 day ...
  60. #my $interval = 60*60*24;
  61. # disabled by default
  62. our $interval = 100;
  63.  
  64. # instead of changing this script, you can put your settings to /etc/mail/postfixadmin/vacation.conf
  65. # or /etc/postfixadmin/vacation.conf just use Perl syntax there to fill the variables listed above
  66. # (without the "our" keyword). Example:
  67. # $db_username = 'mail';
  68. if (-f "/etc/mail/postfixadmin/vacation.conf") {
  69. require "/etc/mail/postfixadmin/vacation.conf";
  70. } elsif (-f "/etc/postfixadmin/vacation.conf") {
  71. require "/etc/postfixadmin/vacation.conf";
  72. }
  73.  
  74. # =========== end configuration ===========
  75.  
  76. if($log_to_file == 1) {
  77. if (( ! -w $logfile ) && (! -w dirname($logfile))) {
  78. # Cannot log; no where to write to.
  79. die("Cannot create logfile : $logfile");
  80. }
  81. }
  82.  
  83. my ($from, $to, $cc, $replyto , $subject, $messageid, $lastheader, $smtp_sender, $smtp_recipient, %opts, $spam, $test_mode, $logger);
  84.  
  85. $subject='';
  86.  
  87. $messageid='unknown';
  88.  
  89. # Setup a logger...
  90. #
  91. getopts('f:t:', \%opts) or die "Usage: $0 [-t yes] -f sender -- recipient\n\t-t for testing only\n";
  92. $opts{f} and $smtp_sender = $opts{f} or die "-f sender not present on command line";
  93. $test_mode = 0;
  94. $opts{t} and $test_mode = 1;
  95. $smtp_recipient = shift or die "recipient not given on command line";
  96.  
  97. my $log_layout = Log::Log4perl::Layout::PatternLayout->new("%d %p> %F:%L %M - %m%n");
  98.  
  99. if($test_mode == 1) {
  100. $logger = get_logger();
  101. # log to stdout
  102. my $appender = Log::Log4perl::Appender->new('Log::Dispatch::Screen');
  103. $appender->layout($log_layout);
  104. $logger->add_appender($appender);
  105. $logger->debug("Test mode enabled");
  106. } else {
  107. $logger = get_logger();
  108. if($log_to_file == 1) {
  109. # log to file
  110. my $appender = Log::Log4perl::Appender->new(
  111. 'Log::Dispatch::File',
  112. filename => $logfile,
  113. mode => 'append');
  114.  
  115. $appender->layout($log_layout);
  116. $logger->add_appender($appender);
  117. }
  118.  
  119. if($syslog == 1) {
  120. my $syslog_appender = Log::Log4perl::Appender->new(
  121. 'Log::Dispatch::Syslog',
  122. Facility => 'mail',
  123. );
  124. $logger->add_appender($syslog_appender);
  125. }
  126. }
  127.  
  128. # change to $DEBUG, $INFO or $ERROR depending on how much logging you want.
  129. $logger->level($ERROR);
  130. if($log_level == 1) {
  131. $logger->level($INFO);
  132.  
  133. }
  134. if($log_level == 2) {
  135. $logger->level($DEBUG);
  136. }
  137.  
  138. binmode (STDIN,':utf8');
  139.  
  140. my $dbh;
  141. if ($db_host) {
  142. $dbh = DBI->connect("DBI:$db_type:dbname=$db_name;host=$db_host","$db_username", "$db_password", { RaiseError => 1 });
  143. } else {
  144. $dbh = DBI->connect("DBI:$db_type:dbname=$db_name","$db_username", "$db_password", { RaiseError => 1 });
  145. }
  146.  
  147. if (!$dbh) {
  148. $logger->error("Could not connect to database"); # eval { } etc better here?
  149. exit(0);
  150. }
  151.  
  152. my $db_true; # MySQL and PgSQL use different values for TRUE, and unicode support...
  153. if ($db_type eq "mysql") {
  154. $dbh->do("SET CHARACTER SET utf8;");
  155. $db_true = '1';
  156. } else { # Pg
  157. $dbh->do("SET CLIENT_ENCODING TO 'UTF8'");
  158. $db_true = 'True';
  159. }
  160.  
  161. # used to detect infinite address lookup loops
  162. my $loopcount=0;
  163.  
  164. sub already_notified {
  165. my ($to, $from) = @_;
  166. my $logger = get_logger();
  167. my $query = qq{INSERT into vacation_notification (on_vacation,notified) values (?,?)};
  168. my $stm = $dbh->prepare($query);
  169. if (!$stm) {
  170. $logger->error("Could not prepare query '$query' to: $to, from:$from");
  171. return 1;
  172. }
  173. $stm->{'PrintError'} = 0;
  174. $stm->{'RaiseError'} = 0;
  175. if (!$stm->execute($to,$from)) {
  176. my $e=$dbh->errstr;
  177.  
  178. :
  179. # Violation of a primay key constraint may happen here, and that's
  180. # fine. All other error conditions are not fine, however.
  181. if ($e !~ /(?:_pkey|^Duplicate entry)/) {
  182. $logger->error("Failed to insert into vacation_notification table (to:$to from:$from error:'$e' query:'$query')");
  183. # Let's play safe and notify anyway
  184. return 1;
  185. }
  186. if ($interval) {
  187. $query = qq{SELECT NOW()-notified_at FROM vacation_notification WHERE on_vacation=? AND notified=?};
  188. $stm = $dbh->prepare($query) or panic_prepare($query);
  189. $stm->execute($to,$from) or panic_execute($query,"on_vacation='$to', notified='$from'");
  190. my @row = $stm->fetchrow_array;
  191. my $int = $row[0];
  192. if ($int > $interval) {
  193. $logger->info("[Interval elapsed, sending the message]: From: $from To:$to");
  194. $query = qq{UPDATE vacation_notification SET notified_at=NOW() WHERE on_vacation=? AND notified=?};
  195. $stm = $dbh->prepare($query);
  196. if (!$stm) {
  197. $logger->error("Could not prepare query '$query' (to: '$to', from: '$from')");
  198. return 0;
  199. }
  200. if (!$stm->execute($to,$from)) {
  201. $e=$dbh->errstr;
  202. $logger->error("Error from running query '$query' (to: '$to', from: '$from', error: '$e')");
  203. }
  204. return 0;
  205. } else {
  206. $logger->debug("Notification interval not elapsed; not sending vacation reply (to: '$to', from: '$from')");
  207. return 1;
  208. }
  209. } else {
  210. return 1;
  211. }
  212. }
  213. return 0;
  214. }
  215.  
  216. # try and determine if email address has vacation turned on; we
  217. # have to do alias searching, and domain aliasing resolution for this.
  218. # If found, return ($num_matches, $real_email);
  219. sub find_real_address {
  220. my ($email) = @_;
  221. my $logger = get_logger();
  222. if (++$loopcount > 20) {
  223. $logger->error("find_real_address loop! (more than 20 attempts!) currently: $email");
  224. :
  225.  
  226. exit(1);
  227. }
  228. my $realemail = '';
  229. my $query = qq{SELECT email FROM vacation WHERE email=? AND active=$db_true};
  230. my $stm = $dbh->prepare($query) or panic_prepare($query);
  231. $stm->execute($email) or panic_execute($query,"email='$email'");
  232. my $rv = $stm->rows;
  233.  
  234. # Recipient has vacation
  235. if ($rv == 1) {
  236. $realemail = $email;
  237. $logger->debug("Found '\$email'\ has vacation active");
  238. } else {
  239. my $vemail = $email;
  240. $vemail =~ s/\@/#/g;
  241. $vemail = $vemail . "\@" . $vacation_domain;
  242. $logger->debug("Looking for alias records that \'$email\' resolves to with vacation turned on");
  243. $query = qq{SELECT goto FROM alias WHERE address=? AND (goto LIKE ? OR goto LIKE ? OR goto LIKE ? OR goto = ?)};
  244. $stm = $dbh->prepare($query) or panic_prepare($query);
  245. $stm->execute($email,"$vemail,%","%,$vemail","%,$vemail,%", "$vemail") or panic_execute($query,"address='$email'");
  246. $rv = $stm->rows;
  247.  
  248. # Recipient is an alias, check if mailbox has vacation
  249. if ($rv == 1) {
  250. my @row = $stm->fetchrow_array;
  251. my $alias = $row[0];
  252. if ($alias =~ /,/) {
  253. for (split(/\s*,\s*/, lc($alias))) {
  254. my $singlealias = $_;
  255. $logger->debug("Found alias \'$singlealias\' for email \'$email\'. Looking if vacation is on for alias.");
  256. $query = qq{SELECT email FROM vacation WHERE email=? AND active=$db_true};
  257. $stm = $dbh->prepare($query) or panic_prepare($query);
  258. $stm->execute($singlealias) or panic_execute($query,"email='$singlealias'");
  259. $rv = $stm->rows;
  260. # Alias has vacation
  261. if ($rv == 1) {
  262. $realemail = $singlealias;
  263. last;
  264. }
  265. }
  266. } else {
  267. $query = qq{SELECT email FROM vacation WHERE email=? AND active=$db_true};
  268. $stm = $dbh->prepare($query) or panic_prepare($query);
  269. $stm->execute($alias) or panic_prepare($query,"email='$alias'");
  270. $rv = $stm->rows;
  271. # Alias has vacation
  272. if ($rv == 1) {
  273. $realemail = $alias;
  274. }
  275. }
  276.  
  277. # We have to look for alias domain (domain1 -> domain2)
  278. } else {
  279. my ($user, $domain) = split(/@/, $email);
  280. $logger->debug("Looking for alias domain for $domain / $email / $user");
  281. $query = qq{SELECT target_domain FROM alias_domain WHERE alias_domain=?};
  282. $stm = $dbh->prepare($query) or panic_prepare($query);
  283. $stm->execute($domain) or panic_execute($query,"alias_domain='$domain'");
  284. $rv = $stm->rows;
  285.  
  286. # The domain has a alias domain level alias
  287. if ($rv == 1) {
  288. my @row = $stm->fetchrow_array;
  289. my $alias_domain_dest = $row[0];
  290. ($rv, $realemail) = find_real_address ("$user\@$alias_domain_dest");
  291.  
  292. # We still have to look for domain level aliases...
  293. } else {
  294. my ($user, $domain) = split(/@/, $email);
  295. $logger->debug("Looking for domain level aliases for $domain / $email / $user");
  296. $query = qq{SELECT goto FROM alias WHERE address=?};
  297. $stm = $dbh->prepare($query) or panic_prepare($query);
  298. $stm->execute("\@$domain") or panic_execute($query,"address='\@$domain'");
  299. $rv = $stm->rows;
  300.  
  301. # The receipient has a domain level alias
  302. if ($rv == 1) {
  303. my @row = $stm->fetchrow_array;
  304. my $wildcard_dest = $row[0];
  305. my ($wilduser, $wilddomain) = split(/@/, $wildcard_dest);
  306.  
  307. # Check domain alias
  308. if ($wilduser) {
  309. ($rv, $realemail) = find_real_address ($wildcard_dest);
  310. } else {
  311. ($rv, $realemail) = find_real_address ("$user\@$wilddomain");
  312. }
  313. } else {
  314. $logger->debug("No domain level alias present for $domain / $email / $user");
  315. }
  316. }
  317. }
  318. }
  319. return ($rv, $realemail);
  320. }
  321.  
  322. # sends the vacation mail to the original sender.
  323. #
  324. sub send_vacation_email {
  325. my ($email, $orig_from, $orig_to, $orig_messageid, $test_mode) = @_;
  326. my $logger = get_logger();
  327. $logger->debug("Asked to send vacation reply to $email thanks to $orig_messageid");
  328. my $query = qq{SELECT subject,body FROM vacation WHERE email=?};
  329. my $stm = $dbh->prepare($query) or panic_prepare($query);
  330. $stm->execute($email) or panic_execute($query,"email='$email'");
  331. my $rv = $stm->rows;
  332. if ($rv == 1) {
  333. my @row = $stm->fetchrow_array;
  334. if (already_notified($email, $orig_from) == 1) {
  335. $logger->debug("Already notified $email, or some error prevented us from doing so");
  336. return;
  337. }
  338.  
  339. $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]");
  340. my $subject = $row[0];
  341. my $body = $row[1];
  342. my $from = $email;
  343. my $to = $orig_from;
  344. my %smtp_connection;
  345. %smtp_connection = (
  346. 'smtp' => $smtp_server,
  347. 'port' => $smtp_server_port,
  348. 'auth' => $smtp_auth,
  349. 'authid' => $smtp_authid,
  350. 'authpwd' => $smtp_authpwd,
  351. 'skip_bad_recipients' => 'true',
  352. 'encoding' => 'Base64',
  353. 'ctype' => 'text/plain; charset=UTF-8',
  354. 'headers' => 'Precedence: junk',
  355. 'headers' => 'X-Loop: Postfix Admin Virtual Vacation',
  356. );
  357. my %mail;
  358. # I believe Mail::Sender qp encodes the subject, so we no longer need to.
  359. %mail = (
  360. 'subject' => $subject,
  361. 'from' => $from,
  362. 'to' => $to,
  363. 'msg' => encode_base64($body)
  364. );
  365. if($test_mode == 1) {
  366. $logger->info("** TEST MODE ** : Vacation response sent to $to from $from subject $subject (not) sent\n");
  367. $logger->info(%mail);
  368. return 0;
  369. }
  370. $Mail::Sender::NO_X_MAILER = 1;
  371. my $sender = new Mail::Sender({%smtp_connection});
  372. $sender->Open({%mail});
  373. $sender->SendLineEnc($body);
  374. $sender->Close() or $logger->error("Failed to send vacation response: " . $sender->{'error_msg'});
  375. $logger->debug("Vacation response sent to $to, from $from");
  376. }
  377. }
  378.  
  379. # Convert a (list of) email address(es) from RFC 822 style addressing to
  380. # RFC 821 style addressing. e.g. convert:
  381. # "John Jones" <JJones@acme.com>, "Jane Doe/Sales/ACME" <JDoe@acme.com>
  382. # to:
  383. # jjones@acme.com, jdoe@acme.com
  384. sub strip_address {
  385. my ($arg) = @_;
  386. if(!$arg) {
  387. return '';
  388. }
  389. my @ok;
  390. $logger = get_logger();
  391. my @list;
  392. @list = $arg =~ m/([\w\.\-\+\'\=_\^\|\$\/\{\}~\?\*\\&\!`\%]+\@[\w\.\-]+\w+)/g;
  393. foreach(@list) {
  394. #$logger->debug("Checking: $_");
  395. my $temp = Email::Valid->address( -address => $_, -mxcheck => 0);
  396. if($temp) {
  397. push(@ok, $temp);
  398. } else {
  399. $logger->debug("Email not valid : $Email::Valid::Details");
  400. }
  401. }
  402. # remove duplicates
  403. my %seen = ();
  404. my @uniq;
  405. my $item;
  406. foreach $item (@ok) {
  407. push(@uniq, $item) unless $seen{$item}++
  408. }
  409.  
  410. my $result = lc(join(", ", @uniq));
  411. #$logger->debug("Result: $result");
  412. return $result;
  413. }
  414.  
  415. sub panic_prepare {
  416. my ($arg) = @_;
  417. my $logger = get_logger();
  418. $logger->error("Could not prepare sql statement: '$arg'");
  419. exit(0);
  420. }
  421.  
  422. sub panic_execute {
  423. my ($arg,$param) = @_;
  424. my $logger = get_logger();
  425. $logger->error("Could not execute sql statement - '$arg' with parameters '$param'");
  426. exit(0);
  427. }
  428.  
  429. # Make sure the email wasn't sent by someone who could be a mailing list etc; if it was,
  430. # then we abort after appropriate logging.
  431. sub check_and_clean_from_address {
  432. my ($address) = @_;
  433. my $logger = get_logger();
  434.  
  435. if($address =~ /^(noreply|postmaster|mailer\-daemon|listserv|majordomo|owner\-|request\-|bounces\-)/i ||
  436. $address =~ /\-(owner|request|bounces)\@/i ) {
  437. $logger->debug("sender $address contains $1 - will not send vacation message");
  438. exit(0);
  439. }
  440. $address = strip_address($address);
  441. if($address eq "") {
  442. $logger->error("Address $address is not valid; exiting");
  443. exit(0);
  444. }
  445. #$logger->debug("Address cleaned up to $address");
  446. return $address;
  447. }
  448. ########################### main #################################
  449.  
  450. # Take headers apart
  451. $cc = '';
  452. $replyto = '';
  453.  
  454. $logger->debug("Script argument SMTP recipient is : '$smtp_recipient' and smtp_sender : '$smtp_sender'");
  455. while (<STDIN>) {
  456. last if (/^$/);
  457. if (/^\s+(.*)/ and $lastheader) { $$lastheader .= " $1"; next; }
  458. elsif (/^from:\s*(.*)\n$/i) { $from = $1; $lastheader = \$from; }
  459. elsif (/^to:\s*(.*)\n$/i) { $to = $1; $lastheader = \$to; }
  460. elsif (/^cc:\s*(.*)\n$/i) { $cc = $1; $lastheader = \$cc; }
  461. elsif (/^Reply\-to:\s*(.*)\s*\n$/i) { $replyto = $1; $lastheader = \$replyto; }
  462. elsif (/^subject:\s*(.*)\n$/i) { $subject = $1; $lastheader = \$subject; }
  463. elsif (/^message\-id:\s*(.*)\s*\n$/i) { $messageid = $1; $lastheader = \$messageid; }
  464. elsif (/^x\-spam\-(flag|status):\s+yes/i) { $logger->debug("x-spam-$1: yes found; exiting"); exit (0); }
  465. elsif (/^x\-facebook\-notify:/i) { $logger->debug('Mail from facebook, ignoring'); exit(0); }
  466. elsif (/^precedence:\s+(bulk|list|junk)/i) { $logger->debug("precedence: $1 found; exiting"); exit (0); }
  467. elsif (/^x\-loop:\s+postfix\ admin\ virtual\ vacation/i) { $logger->debug("x-loop: postfix admin virtual vacation found; exiting"); exit (0); }
  468. elsif (/^Auto\-Submitted:\s*no/i) { next; }
  469. elsif (/^Auto\-Submitted:/i) { $logger->debug("Auto-Submitted: something found; exiting"); exit (0); }
  470. elsif (/^List\-(Id|Post):/i) { $logger->debug("List-$1: found; exiting"); exit (0); }
  471. elsif (/^(x\-(barracuda\-)?spam\-status):\s+(yes)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); }
  472. elsif (/^(x\-dspam\-result):\s+(spam|bl[ao]cklisted)/i) { $logger->debug("$1: $2 found; exiting"); exit (0); }
  473. elsif (/^(x\-(anti|avas\-)?virus\-status):\s+(infected)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); }
  474. elsif (/^(x\-(avas\-spam|spamtest|crm114|razor|pyzor)\-status):\s+(spam)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); }
  475. elsif (/^(x\-osbf\-lua\-score):\s+[0-9\/\.\-\+]+\s+\[([-S])\]/i) { $logger->debug("$1: $2 found; exiting"); exit (0); }
  476. else {$lastheader = "" ; }
  477. }
  478.  
  479. if($smtp_recipient =~ /\@$vacation_domain/) {
  480. # the regexp used here could probably be improved somewhat, for now hope that people won't use # as a valid mailbox character.
  481. my $tmp = $smtp_recipient;
  482. $tmp =~ s/\@$vacation_domain//;
  483. $tmp =~ s/#/\@/;
  484. $logger->debug("Converted autoreply mailbox back to normal style - from $smtp_recipient to $tmp");
  485. $smtp_recipient = $tmp;
  486. undef $tmp;
  487. }
  488.  
  489. # If either From: or To: are not set, exit
  490. if(!$from || !$to || !$messageid || !$smtp_sender || !$smtp_recipient) {
  491. $logger->info("One of from=$from, to=$to, messageid=$messageid, smtp sender=$smtp_sender, smtp recipient=$smtp_recipient empty");
  492. exit(0);
  493. }
  494. $logger->debug("Email headers have to: '$to' and From: '$from'");
  495. $to = strip_address($to);
  496. undef $tmp;
  497. }
  498.  
  499. # If either From: or To: are not set, exit
  500. if(!$from || !$to || !$messageid || !$smtp_sender || !$smtp_recipient) {
  501. $logger->info("One of from=$from, to=$to, messageid=$messageid, smtp sender=$smtp_sender, smtp recipient=$smtp_recipient empty");
  502. exit(0);
  503. }
  504. $logger->debug("Email headers have to: '$to' and From: '$from'");
  505. $to = strip_address($to);
  506. $cc = strip_address($cc);
  507. $from = check_and_clean_from_address($from);
  508. if($replyto ne "") {
  509. # if reply-to is invalid, or looks like a mailing list, then we probably don't want to send a reply.
  510. $replyto = check_and_clean_from_address($replyto);
  511. }
  512. $smtp_sender = check_and_clean_from_address($smtp_sender);
  513. $smtp_recipient = check_and_clean_from_address($smtp_recipient);
  514.  
  515. if ($smtp_sender eq $smtp_recipient) {
  516. $logger->debug("smtp sender $smtp_sender and recipient $smtp_recipient are the same; aborting");
  517. exit(0);
  518. }
  519.  
  520. for (split(/,\s*/, lc($to)), split(/,\s*/, lc($cc))) {
  521. my $header_recipient = strip_address($_);
  522. if ($smtp_sender eq $header_recipient) {
  523. $logger->debug("sender header $smtp_sender contains recipient $header_recipient (mailing myself?)");
  524. exit(0);
  525. }
  526. }
  527.  
  528. my ($rv, $email) = find_real_address($smtp_recipient);
  529. if ($rv == 1) {
  530. $logger->debug("Attempting to send vacation response for: $messageid to: $smtp_sender, $smtp_recipient, $email (test_mode = $test_mode)");
  531. send_vacation_email($email, $smtp_sender, $smtp_recipient, $messageid, $test_mode);
  532. } else {
  533. $logger->debug("SMTP recipient $smtp_recipient which resolves to $email does not have an active vacation (rv: $rv, email: $email)");
  534. }
  535.  
  536. 0;
  537.  
  538. #/* vim: set expandtab softtabstop=3 tabstop=3 shiftwidth=3: */
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement