forked from github/Quit.mwForum
364 lines
11 KiB
Perl
364 lines
11 KiB
Perl
# Based on Mail::Sendmail by Milivoj Ivkovic <mi@alma.ch>
|
|
# This version has been slightly modified for mwForum.
|
|
# The original version can be obtained from CPAN.
|
|
|
|
package MwfSendmail;
|
|
$VERSION = "1.15.0";
|
|
|
|
# *************** Configuration you may want to change *******************
|
|
# You probably want to set your SMTP server here (unless you specify it in
|
|
# every script), and leave the rest as is. See pod documentation for details
|
|
|
|
%mailcfg = (
|
|
# List of SMTP servers:
|
|
'smtp' => [ ],
|
|
|
|
'from' => '', # default sender e-mail, used when no From header in mail
|
|
|
|
'mime' => 1, # use MIME encoding by default
|
|
|
|
'retries' => 1, # number of retries on smtp connect failure
|
|
'delay' => 1, # delay in seconds between retries
|
|
|
|
'tz' => '', # only to override automatic detection
|
|
'port' => 25, # change it if you always use a non-standard port
|
|
'debug' => 0 # prints stuff to STDERR
|
|
);
|
|
|
|
# *******************************************************************
|
|
|
|
use strict;
|
|
require Exporter;
|
|
use vars qw(
|
|
$VERSION
|
|
@ISA
|
|
@EXPORT
|
|
@EXPORT_OK
|
|
%mailcfg
|
|
$default_smtp_server
|
|
$default_smtp_port
|
|
$default_sender
|
|
$TZ
|
|
$use_MIME
|
|
$address_rx
|
|
$debug
|
|
$log
|
|
$error
|
|
$retry_delay
|
|
$connect_retries
|
|
);
|
|
|
|
use Socket;
|
|
use Time::Local; # for automatic time zone detection
|
|
|
|
# use MIME::QuotedPrint if available and configured in %mailcfg
|
|
eval("use MIME::QuotedPrint");
|
|
$mailcfg{mime} &&= (!$@);
|
|
|
|
@ISA = qw(Exporter);
|
|
@EXPORT = qw(&sendmail);
|
|
@EXPORT_OK = qw(
|
|
%mailcfg
|
|
time_to_date
|
|
$default_smtp_server
|
|
$default_smtp_port
|
|
$default_sender
|
|
$TZ
|
|
$address_rx
|
|
$debug
|
|
$log
|
|
$error
|
|
);
|
|
|
|
# regex for e-mail addresses where full=$1, user=$2, domain=$3
|
|
# see pod documentation about this regex
|
|
|
|
my $word_rx = '[\x21\x23-\x27\x2A-\x2B\x2D\w\x3D\x3F]+';
|
|
my $user_rx = $word_rx # valid chars
|
|
.'(?:\.' . $word_rx . ')*' # possibly more words preceded by a dot
|
|
;
|
|
my $dom_rx = '\w[-\w]+(?:\.\w[-\w]+)*'; # less valid chars in domain names
|
|
my $ip_rx = '\[\d{1,3}(?:\.\d{1,3}){3}\]';
|
|
|
|
$address_rx = '\b((' . $user_rx . ')\@(' . $dom_rx . '\b|' . $ip_rx . '))';
|
|
; # v. 0.4
|
|
|
|
sub time_to_date {
|
|
# convert a time() value to a date-time string according to RFC 822
|
|
|
|
my $time = $_[0] || time(); # default to now if no argument
|
|
|
|
my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
|
|
my @wdays = qw(Sun Mon Tue Wed Thu Fri Sat);
|
|
|
|
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
|
|
= localtime($time);
|
|
|
|
$TZ ||= $mailcfg{tz};
|
|
|
|
if ( $TZ eq "" ) {
|
|
# offset in hours
|
|
my $offset = sprintf "%.1f", (timegm(localtime) - time) / 3600;
|
|
my $minutes = sprintf "%02d", ( $offset - int($offset) ) * 60;
|
|
$TZ = sprintf("%+03d", int($offset)) . $minutes;
|
|
}
|
|
return join(" ",
|
|
($wdays[$wday] . ','),
|
|
$mday,
|
|
$months[$mon],
|
|
$year+1900,
|
|
sprintf("%02d", $hour) . ":" . sprintf("%02d", $min),
|
|
$TZ
|
|
);
|
|
} # end sub time_to_date
|
|
|
|
sub sendmail {
|
|
# original sendmail 1.21 by Christian Mallwitz.
|
|
# Modified and 'modulized' by mi@alma.ch
|
|
|
|
$error = '';
|
|
$log = "MwfSendmail v. $VERSION - " . scalar(localtime()) . "\n";
|
|
|
|
local $_;
|
|
local $/ = "\015\012";
|
|
|
|
my (%mail, $k,
|
|
$smtp, $server, $port, $connected, $localhost,
|
|
$message, $fromaddr, $recip, @recipients, $to, $header,
|
|
);
|
|
|
|
sub fail {
|
|
# things to do before returning a sendmail failure
|
|
#print STDERR @_ if $^W;
|
|
$error .= join(" ", @_) . "\n";
|
|
close S;
|
|
return 0;
|
|
}
|
|
|
|
# all config keys to lowercase, to prevent typo errors
|
|
foreach $k (keys %mailcfg) {
|
|
if ($k =~ /[A-Z]/) {
|
|
$mailcfg{lc($k)} = $mailcfg{$k};
|
|
}
|
|
}
|
|
|
|
# redo hash, arranging keys case etc...
|
|
while (@_) {
|
|
# arrange keys case
|
|
$k = ucfirst lc(shift @_);
|
|
|
|
if (!$k and $^W) {
|
|
warn "Received false mail hash key: \'$k\'. Did you forget to put it in quotes?\n";
|
|
}
|
|
|
|
$k =~ s/\s*:\s*$//o; # kill colon (and possible spaces) at end, we add it later.
|
|
$mail{$k} = shift @_;
|
|
}
|
|
|
|
$smtp = $mail{Smtp} || $mail{Server} || $default_smtp_server;
|
|
unshift @{$mailcfg{smtp}}, $smtp if ($smtp and $mailcfg{smtp}->[0] ne $smtp);
|
|
|
|
# delete non-header keys, so we don't send them later as mail headers
|
|
# I like this syntax, but it doesn't seem to work with AS port 5.003_07:
|
|
# delete @mail{'Smtp', 'Server'};
|
|
# so instead:
|
|
delete $mail{Smtp}; delete $mail{Server};
|
|
|
|
$mailcfg{port} = $mail{Port} || $default_smtp_port || $mailcfg{port} || 25;
|
|
delete $mail{Port};
|
|
|
|
# for backward compatibility only
|
|
$mailcfg{retries} = $connect_retries if defined($connect_retries);
|
|
$mailcfg{delay} = $retry_delay if defined($retry_delay);
|
|
|
|
{ # don't warn for undefined values below
|
|
local $^W = 0;
|
|
$message = join("", $mail{Message}, $mail{Body}, $mail{Text});
|
|
}
|
|
|
|
# delete @mail{'Message', 'Body', 'Text'};
|
|
delete $mail{Message}; delete $mail{Body}; delete $mail{Text};
|
|
|
|
# Extract 'From:' e-mail address
|
|
|
|
$fromaddr = $mail{From} || $default_sender || $mailcfg{from};
|
|
unless ($fromaddr =~ /$address_rx/) {
|
|
return fail("Bad or missing From address: \'$fromaddr\'");
|
|
}
|
|
$fromaddr = $1;
|
|
|
|
# add Date header if needed
|
|
$mail{Date} ||= time_to_date() ;
|
|
$log .= "Date: $mail{Date}\n";
|
|
|
|
# cleanup message, and encode if needed
|
|
$message =~ s/^\./\.\./gom; # handle . as first character
|
|
$message =~ s/\r\n/\n/go; # normalize line endings, step 1 of 2 (next step after MIME encoding)
|
|
|
|
$mail{'MIME-version'} ||= '1.0';
|
|
$mail{'Content-type'} ||= "text/plain; charset=\"iso-8859-1\"";
|
|
|
|
unless ( $mail{'Content-transfer-encoding'}
|
|
|| $mail{'Content-type'} =~ /multipart/io )
|
|
{
|
|
if ($mailcfg{mime}) {
|
|
$mail{'Content-transfer-encoding'} = 'quoted-printable';
|
|
$message = encode_qp($message);
|
|
}
|
|
else {
|
|
$mail{'Content-transfer-encoding'} = '8bit';
|
|
if ($message =~ /[\x80-\xFF]/o) {
|
|
$error .= "MIME::QuotedPrint not present!\nSending 8bit characters, hoping it will come across OK.\n";
|
|
warn "MIME::QuotedPrint not present!\n",
|
|
"Sending 8bit characters, hoping it will come across OK.\n"
|
|
if $^W;
|
|
}
|
|
}
|
|
}
|
|
|
|
$message =~ s/\n/\015\012/go; # normalize line endings, step 2.
|
|
|
|
# Get recipients
|
|
{ # don't warn for undefined values below
|
|
local $^W = 0;
|
|
$recip = join(", ", $mail{To}, $mail{Cc}, $mail{Bcc});
|
|
}
|
|
|
|
delete $mail{Bcc};
|
|
|
|
@recipients = ();
|
|
while ($recip =~ /$address_rx/go) {
|
|
push @recipients, $1;
|
|
}
|
|
unless (@recipients) {
|
|
return fail("No recipient!")
|
|
}
|
|
|
|
# get local hostname for polite HELO
|
|
$localhost = (gethostbyname('localhost'))[0] || 'localhost';
|
|
|
|
foreach $server ( @{$mailcfg{smtp}} ) {
|
|
# open socket needs to be inside this foreach loop on Linux,
|
|
# otherwise all servers fail if 1st one fails !??! why?
|
|
unless ( socket S, AF_INET, SOCK_STREAM, (getprotobyname 'tcp')[2] ) {
|
|
return fail("socket failed ($!)")
|
|
}
|
|
|
|
#print "- trying $server\n" if $mailcfg{debug} > 1;
|
|
|
|
$server =~ s/\s+//go; # remove spaces just in case of a typo
|
|
# extract port if server name like "mail.domain.com:2525"
|
|
($server =~ s/:(.+)$//o) ? $port = $1 : $port = $mailcfg{port};
|
|
$smtp = $server; # save $server for use outside foreach loop
|
|
|
|
my $smtpaddr = inet_aton $server;
|
|
unless ($smtpaddr) {
|
|
$error .= "$server not found\n";
|
|
next; # next server
|
|
}
|
|
|
|
my $retried = 0; # reset retries for each server
|
|
while ( ( not $connected = connect S, pack_sockaddr_in($port, $smtpaddr) )
|
|
and ( $retried < $mailcfg{retries} )
|
|
) {
|
|
$retried++;
|
|
$error .= "connect to $server failed ($!)\n";
|
|
#print "- connect to $server failed ($!)\n" if $mailcfg{debug} > 1;
|
|
#print "retrying in $mailcfg{delay} seconds...\n" if $mailcfg{debug} > 1;
|
|
sleep $mailcfg{delay};
|
|
}
|
|
|
|
if ( $connected ) {
|
|
#print "- connected to $server\n" if $mailcfg{debug} > 3;
|
|
last;
|
|
}
|
|
else {
|
|
$error .= "connect to $server failed\n";
|
|
#print "- connect to $server failed, next server...\n" if $mailcfg{debug} > 1;
|
|
next; # next server
|
|
}
|
|
}
|
|
|
|
unless ( $connected ) {
|
|
return fail("connect to $smtp failed ($!) no (more) retries!")
|
|
};
|
|
|
|
{
|
|
local $^W = 0; # don't warn on undefined variables
|
|
# Add info to log variable
|
|
$log .= "Server: $smtp Port: $port\n"
|
|
. "From: $fromaddr\n"
|
|
. "Subject: $mail{Subject}\n"
|
|
. "To: ";
|
|
}
|
|
|
|
my($oldfh) = select(S); $| = 1; select($oldfh);
|
|
|
|
chomp($_ = <S>);
|
|
if (/^[45]/ or !$_) {
|
|
return fail("Connection error from $smtp on port $port ($_)")
|
|
}
|
|
|
|
print S "HELO $localhost\015\012";
|
|
chomp($_ = <S>);
|
|
if (/^[45]/ or !$_) {
|
|
return fail("HELO error ($_)")
|
|
}
|
|
|
|
print S "mail from: <$fromaddr>\015\012";
|
|
chomp($_ = <S>);
|
|
if (/^[45]/ or !$_) {
|
|
return fail("mail From: error ($_)")
|
|
}
|
|
|
|
foreach $to (@recipients) {
|
|
#if ($debug) { print STDERR "sending to: <$to>\n"; }
|
|
print S "rcpt to: <$to>\015\012";
|
|
chomp($_ = <S>);
|
|
if (/^[45]/ or !$_) {
|
|
$log .= "!Failed: $to\n ";
|
|
return fail("Error sending to <$to> ($_)\n");
|
|
}
|
|
else {
|
|
$log .= "$to\n ";
|
|
}
|
|
}
|
|
|
|
# start data part
|
|
print S "data\015\012";
|
|
chomp($_ = <S>);
|
|
if (/^[45]/ or !$_) {
|
|
return fail("Cannot send data ($_)");
|
|
}
|
|
|
|
# print headers
|
|
foreach $header (keys %mail) {
|
|
$mail{$header} =~ s/\s+$//o; # kill possible trailing garbage
|
|
print S "$header: ", $mail{$header}, "\015\012";
|
|
};
|
|
|
|
#- test diconnecting from network here, to see what happens
|
|
#- print STDERR "DISCONNECT NOW!\n";
|
|
#- sleep 4;
|
|
#- print STDERR "trying to continue, expecting an error... \n";
|
|
|
|
# send message body
|
|
print S "\015\012",
|
|
$message,
|
|
"\015\012.\015\012";
|
|
|
|
chomp($_ = <S>);
|
|
if (/^[45]/ or !$_) {
|
|
return fail("message transmission failed ($_)");
|
|
}
|
|
|
|
# finish
|
|
print S "quit\015\012";
|
|
$_ = <S>;
|
|
close S;
|
|
|
|
return 1;
|
|
} # end sub sendmail
|
|
|
|
1;
|