Add SSL/STARTTLS support to pandora_sendmail.

This commit is contained in:
Ramon Novoa 2019-09-10 15:53:47 +02:00
parent 4722d4a08b
commit 33d9ce9de8
5 changed files with 141 additions and 60 deletions

View File

@ -250,6 +250,10 @@ mta_address localhost
#mta_from Pandora FMS <pandora@mydomain.com>
# SMTP encryption protocol (none, ssl, starttls)
#mta_encryption none
# Set 1 if want eMail deliver alert in separate mail (default).
# Set 0 if want eMail deliver shared mail by all destination.
mail_in_separate 1

View File

@ -235,6 +235,10 @@ dataserver_threads 2
# probably you need to change it to avoid problems with your antispam
#mta_from pandora@sampledomain.com
# SMTP encryption protocol (none, ssl, starttls)
#mta_encryption none
# xprobe2: Optional package to detect OS types using advanced TCP/IP
# fingerprinting tecniques, much more accurates than stadard nmap.
# If not provided, nmap is used insted xprobe2

View File

@ -187,6 +187,33 @@ sub pandora_get_sharedconfig ($$) {
[$dbh]
);
$pa_config->{'rb_product_name'} = 'Pandora FMS' unless (defined ($pa_config->{'rb_product_name'}) && $pa_config->{'rb_product_name'} ne '');
# Mail transport agent configuration. Local configuration takes precedence.
if ($pa_config->{"mta_address"} eq '') {
$pa_config->{"mta_address"} = pandora_get_tconfig_token ($dbh, 'email_smtpServer', '');
$pa_config->{"mta_from"} = '"' . pandora_get_tconfig_token ($dbh, 'email_from_name', 'Pandora FMS') . '" <' .
pandora_get_tconfig_token ($dbh, 'email_from_dir', 'pandora@pandorafms.org') . '>';
$pa_config->{"mta_pass"} = pandora_get_tconfig_token ($dbh, 'email_password', '');
$pa_config->{"mta_port"} = pandora_get_tconfig_token ($dbh, 'email_smtpPort', '');
$pa_config->{"mta_user"} = pandora_get_tconfig_token ($dbh, 'email_username', '');
$pa_config->{"mta_encryption"} = pandora_get_tconfig_token ($dbh, 'email_encryption', '');
# Auto-negotiate the auth mechanism, since it cannot be set from the console.
# Do not include PLAIN, it generates the following error:
# 451 4.5.0 SMTP protocol violation, see RFC 2821
$pa_config->{"mta_auth"} = 'DIGEST-MD5 CRAM-MD5 LOGIN';
# Fix the format of mta_encryption.
if ($pa_config->{"mta_encryption"} eq 'tls') {
$pa_config->{"mta_encryption"} = 'starttls';
}
elsif ($pa_config->{"mta_encryption"} =~ m/^ssl/) {
$pa_config->{"mta_encryption"} = 'ssl';
}
else {
$pa_config->{"mta_encryption"} = 'none';
}
}
}
##########################################################################
@ -303,12 +330,13 @@ sub pandora_load_config {
$pa_config->{"dynamic_constant"} = 10; # 7.0
# Internal MTA for alerts, each server need its own config.
$pa_config->{"mta_address"} = '127.0.0.1'; # Introduced on 2.0
$pa_config->{"mta_port"} = '25'; # Introduced on 2.0
$pa_config->{"mta_address"} = ''; # Introduced on 2.0
$pa_config->{"mta_port"} = ''; # Introduced on 2.0
$pa_config->{"mta_user"} = ''; # Introduced on 2.0
$pa_config->{"mta_pass"} = ''; # Introduced on 2.0
$pa_config->{"mta_auth"} = 'none'; # Introduced on 2.0 (Support LOGIN PLAIN CRAM-MD5 DIGEST-MD)
$pa_config->{"mta_from"} = 'pandora@localhost'; # Introduced on 2.0
$pa_config->{"mta_encryption"} = 'none';
$pa_config->{"mail_in_separate"} = 1; # 1: eMail deliver alert mail in separate mails.
# 0: eMail deliver 1 mail with all destination.
@ -592,6 +620,9 @@ sub pandora_load_config {
elsif ($parametro =~ m/^mta_from\s(.*)/i) {
$pa_config->{'mta_from'}= clean_blank($1);
}
elsif ($parametro =~ m/^mta_encryption\s(.*)/i) {
$pa_config->{'mta_encryption'}= clean_blank($1);
}
elsif ($parametro =~ m/^mail_in_separate\s+([0-9]*)/i) {
$pa_config->{'mail_in_separate'}= clean_blank($1);
}

View File

@ -32,7 +32,9 @@ $VERSION = '0.79_16';
'tz' => '', # only to override automatic detection
'port' => 25, # change it if you always use a non-standard port
'debug' => 0 # prints stuff to STDERR
'debug' => 0, # prints stuff to STDERR
'encryption' => 'none', # no, ssl or starttls
'timeout' => 5, # timeout for socket reads/writes in seconds
);
# *******************************************************************
@ -54,7 +56,8 @@ use vars qw(
$auth_support
);
use Socket;
use IO::Socket::INET;
use IO::Select;
use Time::Local; # for automatic time zone detection
use Sys::Hostname; # for use of hostname in HELO
@ -62,6 +65,12 @@ use Sys::Hostname; # for use of hostname in HELO
$auth_support = 'DIGEST-MD5 CRAM-MD5 PLAIN LOGIN';
# IO::Socket object.
my $S;
# IO::Select object.
my $Sel;
# use MIME::QuotedPrint if available and configured in %mailcfg
eval("use MIME::QuotedPrint");
$mailcfg{'mime'} &&= (!$@);
@ -178,9 +187,9 @@ sub sendmail {
local $_;
my (%mail, $k,
$smtp, $server, $port, $connected, $localhost,
$smtp, $server, $port, $localhost,
$fromaddr, $recip, @recipients, $to, $header,
%esmtp, @wanted_methods,
%esmtp, @wanted_methods, $encryption
);
use vars qw($server_reply);
# -------- a few internal subs ----------
@ -191,7 +200,7 @@ sub sendmail {
$error .= "Server said: $server_reply\n";
print STDERR "Server said: $server_reply\n" if $^W;
}
close S;
close $S if defined($S);
return 0;
}
@ -200,7 +209,7 @@ sub sendmail {
for $i (0..$#_) {
# accept references, so we don't copy potentially big data
my $data = ref($_[$i]) ? $_[$i] : \$_[$i];
if ($mailcfg{'debug'} > 5) {
if ($mailcfg{'debug'} > 9) {
if (length($$data) < 500) {
print ">", $$data;
}
@ -208,23 +217,32 @@ sub sendmail {
print "> [...", length($$data), " bytes sent ...]\n";
}
}
print(S $$data) || return 0;
my @sockets = $Sel->can_write($mailcfg{'timeout'});
return 0 if (!@sockets);
syswrite($sockets[0], $$data) || return 0;
}
1;
}
sub socket_read {
my $buffer;
$server_reply = "";
do {
$_ = <S>;
$server_reply .= $_;
#chomp $_;
print "<$_" if $mailcfg{'debug'} > 5;
if (/^[45]/ or !$_) {
chomp $server_reply;
return; # return false
}
} while (/^[\d]+-/);
while (my @sockets = $Sel->can_read($mailcfg{'timeout'})) {
return if (!@sockets);
# 16kByte is the maximum size of an SSL frame and because sysread
# returns data from only a single SSL frame you can guarantee that
# there are no pending data.
sysread($sockets[0], $buffer, 65535) || return;
$server_reply .= $buffer;
last if ($buffer =~ m/\n$/);
}
print "<$server_reply" if $mailcfg{'debug'} > 9;
if ($server_reply =~ /^[45]/) {
chomp $server_reply;
return; # return false
}
chomp $server_reply;
return $server_reply;
}
@ -262,11 +280,13 @@ sub sendmail {
$smtp = $mail{'Smtp'} || $mail{'Server'};
unshift @{$mailcfg{'smtp'}}, $smtp if ($smtp and $mailcfg{'smtp'}->[0] ne $smtp);
$encryption = $mail{'Encryption'} || $mail{'Encryption'};
# 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'};
delete $mail{'Smtp'}; delete $mail{'Server'}; delete $mail{'Encryption'};
$mailcfg{'port'} = $mail{'Port'} || $mailcfg{'port'} || 25;
delete $mail{'Port'};
@ -343,48 +363,36 @@ sub sendmail {
$localhost = hostname() || '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, scalar(getprotobyname 'tcp') ) {
return fail("socket failed ($!)")
}
print "- trying $server\n" if $mailcfg{'debug'} > 1;
print "- trying $server\n" if $mailcfg{'debug'} > 9;
$server =~ s/\s+//go; # remove spaces just in case of a typo
# extract port if server name like "mail.domain.com:2525"
$port = ($server =~ s/:(\d+)$//o) ? $1 : $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
# load IO::Socket SSL if needed
if ($encryption ne 'none') {
eval "require IO::Socket::SSL" || return fail("IO::Socket::SSL is not available");
}
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 ($encryption ne 'ssl') {
$S = new IO::Socket::INET(PeerPort => $port, PeerAddr => $server, Proto => 'tcp');
}
if ( $connected ) {
print "- connected to $server\n" if $mailcfg{'debug'} > 3;
else {
$S = new IO::Socket::SSL(PeerPort => $port, PeerAddr => $server, Proto => 'tcp', SSL_verify => 0, Domain => AF_INET);
}
if ( $S ) {
print "- connected to $server\n" if $mailcfg{'debug'} > 9;
last;
}
else {
$error .= "connect to $server failed\n";
print "- connect to $server failed, next server...\n" if $mailcfg{'debug'} > 1;
print "- connect to $server failed, next server...\n" if $mailcfg{'debug'} > 9;
next; # next server
}
}
unless ( $connected ) {
unless ( $S ) {
return fail("connect to $smtp failed ($!) no (more) retries!")
};
@ -397,8 +405,9 @@ sub sendmail {
;
}
my($oldfh) = select(S); $| = 1; select($oldfh);
$Sel = new IO::Select() || return fail("IO::Select error");
$Sel->add($S);
socket_read()
|| return fail("Connection error from $smtp on port $port ($_)");
socket_write("EHLO $localhost$CRLF")
@ -418,8 +427,37 @@ sub sendmail {
|| return fail("send HELO error (lost connection?)");
}
if ($auth) {
warn "AUTH requested\n" if ($mailcfg{debug} > 4);
# STARTTLS
if ($encryption eq 'starttls') {
defined($esmtp{'STARTTLS'})
|| return fail('STARTTLS not supported');
socket_write("STARTTLS$CRLF") || return fail("send STARTTLS error");
socket_read()
|| return fail('STARTTLS error');
IO::Socket::SSL->start_SSL($S, SSL_hostname => $server)
|| return fail("start_SSL failed");
# The client SHOULD send an EHLO command as the
# first command after a successful TLS negotiation.
socket_write("EHLO $localhost$CRLF")
|| return fail("send EHLO error (lost connection?)");
my $ehlo = socket_read();
if ($ehlo) {
# The server MUST discard any knowledge
# obtained from the client.
%esmtp = ();
# parse EHLO response
map {
s/^\d+[- ]//;
my ($k, $v) = split /\s+/, $_, 2;
$esmtp{$k} = $v || 1 if $k;
} split(/\n/, $ehlo);
}
}
if (defined($auth) && $auth->{'user'} ne '') {
warn "AUTH requested\n" if ($mailcfg{debug} > 9);
# reduce wanted methods to those supported
my @methods = grep {$esmtp{'AUTH'}=~/(^|\s)$_(\s|$)/i}
grep {$auth_support =~ /(^|\s)$_(\s|$)/i}
@ -480,9 +518,9 @@ sub sendmail {
my $challenge = socket_read()
|| return fail("AUTH DIGEST-MD5 failed: $server_reply");
$challenge =~ s/^\d+\s+//; $challenge =~ s/[\r\n]+$//;
warn "\nCHALLENGE=", decode_base64($challenge), "\n" if ($mailcfg{debug} > 10);
warn "\nCHALLENGE=", decode_base64($challenge), "\n" if ($mailcfg{debug} > 9);
my $response = _digest_md5($auth->{user}, $auth->{password}, decode_base64($challenge), $auth->{realm});
warn "\nRESPONSE=$response\n" if ($mailcfg{debug} > 10);
warn "\nRESPONSE=$response\n" if ($mailcfg{debug} > 9);
socket_write(encode_base64($response, ""), $CRLF)
|| return fail("AUTH DIGEST-MD5 failed: $server_reply");
my $status = socket_read()
@ -562,7 +600,7 @@ sub sendmail {
socket_write("QUIT$CRLF")
|| return fail("send QUIT error");
socket_read();
close S;
close $S;
return 1;
} # end sub sendmail

View File

@ -518,7 +518,14 @@ sub pandora_sendmail {
Smtp => $pa_config->{"mta_address"},
Port => $pa_config->{"mta_port"},
From => $pa_config->{"mta_from"},
Encryption => $pa_config->{"mta_encryption"},
);
# Set the timeout.
$PandoraFMS::Sendmail::mailcfg{'timeout'} = $pa_config->{"tcp_timeout"};
# Enable debugging.
$PandoraFMS::Sendmail::mailcfg{'debug'} = $pa_config->{"verbosity"};
if (defined($content_type)) {
$mail{'Content-Type'} = $content_type;
@ -535,15 +542,12 @@ sub pandora_sendmail {
$mail{auth} = {user=>$pa_config->{"mta_user"}, password=>$pa_config->{"mta_pass"}, method=>$pa_config->{"mta_auth"}, required=>1 };
}
if (sendmail %mail) {
return;
}
else {
logger ($pa_config, "[ERROR] Sending email to $to_address with subject $subject", 1);
if (defined($Mail::Sendmail::error)){
logger ($pa_config, "ERROR Code: $Mail::Sendmail::error", 5);
eval {
if (!sendmail(%mail)) {
logger ($pa_config, "[ERROR] Sending email to $to_address with subject $subject", 1);
logger ($pa_config, "ERROR Code: $Mail::Sendmail::error", 5) if (defined($Mail::Sendmail::error));
}
}
};
}
##########################################################################