diff --git a/pandora_server/conf/pandora_server.conf.new b/pandora_server/conf/pandora_server.conf.new index 33d34082c0..89b9cde24d 100644 --- a/pandora_server/conf/pandora_server.conf.new +++ b/pandora_server/conf/pandora_server.conf.new @@ -250,6 +250,10 @@ mta_address localhost #mta_from Pandora FMS +# 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 diff --git a/pandora_server/conf/pandora_server.conf.windows b/pandora_server/conf/pandora_server.conf.windows index a602f7abc4..19bc989008 100644 --- a/pandora_server/conf/pandora_server.conf.windows +++ b/pandora_server/conf/pandora_server.conf.windows @@ -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 diff --git a/pandora_server/lib/PandoraFMS/Config.pm b/pandora_server/lib/PandoraFMS/Config.pm index 0bd92497dc..886a04e23e 100644 --- a/pandora_server/lib/PandoraFMS/Config.pm +++ b/pandora_server/lib/PandoraFMS/Config.pm @@ -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); } diff --git a/pandora_server/lib/PandoraFMS/Sendmail.pm b/pandora_server/lib/PandoraFMS/Sendmail.pm index 394add6d1f..551d713dd3 100644 --- a/pandora_server/lib/PandoraFMS/Sendmail.pm +++ b/pandora_server/lib/PandoraFMS/Sendmail.pm @@ -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 { - $_ = ; - $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 diff --git a/pandora_server/lib/PandoraFMS/Tools.pm b/pandora_server/lib/PandoraFMS/Tools.pm index 36050e4275..5089581aad 100755 --- a/pandora_server/lib/PandoraFMS/Tools.pm +++ b/pandora_server/lib/PandoraFMS/Tools.pm @@ -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)); } - } + }; } ##########################################################################