#!/usr/bin/perl # Original plugin by slerena@gmail.com, (c) 2012. based on Sendmail.pm library use strict; use vars qw( $VERSION %mailcfg $address_rx $debug $log $error $retry_delay $connect_retries $auth_support ); use Socket; use Time::Local; # for automatic time zone detection use Sys::Hostname; # for use of hostname in HELO sub get_param($) { my $param = shift; my $value = ""; $param = "-".$param; for(my $i=0; $i<$#ARGV; $i++) { if ($ARGV[$i] eq $param) { $value = $ARGV[$i+1]; last; } } return $value; } %mailcfg = ( # List of SMTP servers: 'smtp' => [ qw( localhost ) ], '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 Digest::HMAC_MD5 qw(hmac_md5 hmac_md5_hex); $auth_support = 'DIGEST-MD5 CRAM-MD5 PLAIN LOGIN'; # use MIME::QuotedPrint if available and configured in %mailcfg eval("use MIME::QuotedPrint"); $mailcfg{'mime'} &&= (!$@); # 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\x2F\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 = '((' . $user_rx . ')\@(' . $dom_rx . '|' . $ip_rx . '))'; ; # v. 0.61 sub _require_md5 { eval { require Digest::MD5; Digest::MD5->import(qw(md5 md5_hex)); }; $error .= $@ if $@; return ($@ ? undef : 1); } sub _require_base64 { eval { require MIME::Base64; MIME::Base64->import(qw(encode_base64 decode_base64)); }; $error .= $@ if $@; return ($@ ? undef : 1); } sub _hmac_md5 { my ($pass, $ckey) = @_; my $size = 64; $pass = md5($pass) if length($pass) > $size; my $ipad = $pass ^ (chr(0x36) x $size); my $opad = $pass ^ (chr(0x5c) x $size); return md5_hex($opad, md5($ipad, $ckey)); } sub _digest_md5 { my ($user, $pass, $challenge, $realm) = @_; my %ckey = map { /^([^=]+)="?(.+?)"?$/ } split(/,/, $challenge); $realm ||= $ckey{realm}; #($user =~ s/\@(.+)$//o) ? $1 : $server; my $nonce = $ckey{nonce}; my $cnonce = &make_cnonce; my $uri = join('/', 'smtp', hostname()||'localhost', $ckey{realm}); my $qop = 'auth'; my $nc = '00000001'; my($hv, $a1, $a2); $hv = md5("$user:$realm:$pass"); $a1 = md5_hex("$hv:$nonce:$cnonce"); $a2 = md5_hex("AUTHENTICATE:$uri"); $hv = md5_hex("$a1:$nonce:$nc:$cnonce:$qop:$a2"); return qq(username="$user",realm="$ckey{realm}",nonce="$nonce",nc=$nc,cnonce="$cnonce",digest-uri="$uri",response=$hv,qop=$qop); } sub make_cnonce { my $s = '' ; for(1..16) { $s .= chr(rand 256) } $s = encode_base64($s, ""); $s =~ s/\W/X/go; return substr($s, 0, 16); } 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); my $TZ = $mailcfg{'tz'}; if ( $TZ eq "" ) { # offset in hours my $offset = sprintf "%.1f", (timegm(localtime) - time) / 3600; my $minutes = sprintf "%02d", abs( $offset - int($offset) ) * 60; $TZ = sprintf("%+03d", int($offset)) . $minutes; } return join(" ", ($wdays[$wday] . ','), $mday, $months[$mon], $year+1900, sprintf("%02d:%02d:%02d", $hour, $min, $sec), $TZ ); } # end sub time_to_date sub sendmail { $error = ''; $log = "Mail::Sendmail v. $VERSION - " . scalar(localtime()) . "\n"; my $CRLF = "\015\012"; local $/ = $CRLF; local $\ = ''; # to protect us from outside settings local $_; my (%mail, $k, $smtp, $server, $port, $connected, $localhost, $fromaddr, $recip, @recipients, $to, $header, %esmtp, @wanted_methods, ); use vars qw($server_reply); # -------- a few internal subs ---------- sub fail { # things to do before returning a sendmail failure $error .= join(" ", @_) . "\n"; if ($server_reply) { $error .= "Server said: $server_reply\n"; print STDERR "Server said: $server_reply\n" if $^W; } close S; return 0; } sub socket_write { my $i; for $i (0..$#_) { # accept references, so we don't copy potentially big data my $data = ref($_[$i]) ? $_[$i] : \$_[$i]; if ($mailcfg{'debug'} > 5) { if (length($$data) < 500) { print ">", $$data; } else { print "> [...", length($$data), " bytes sent ...]\n"; } } print(S $$data) || return 0; } 1; } sub socket_read { $server_reply = ""; do { $_ = ; $server_reply .= $_; #chomp $_; print "<$_" if $mailcfg{'debug'} > 5; if (/^[45]/ or !$_) { chomp $server_reply; return; # return false } } while (/^[\d]+-/); chomp $server_reply; return $server_reply; } # -------- end of internal subs ---------- # all config keys to lowercase, to prevent typo errors foreach $k (keys %mailcfg) { if ($k =~ /[A-Z]/) { $mailcfg{lc($k)} = $mailcfg{$k}; } } # redo mail hash, arranging keys case etc... while (@_) { $k = shift @_; if (!$k and $^W) { warn "Received false mail hash key: \'$k\'. Did you forget to put it in quotes?\n"; } # arrange keys case $k = ucfirst lc($k); $k =~ s/\s*:\s*$//o; # kill colon (and possible spaces) at end, we add it later. # uppercase also after "-", so people don't complain that headers case is different # than in Outlook. $k =~ s/-(.)/"-" . uc($1)/ge; $mail{$k} = shift @_; if ($k !~ /^(Message|Body|Text)$/i) { # normalize possible line endings in headers $mail{$k} =~ s/\015\012?/\012/go; $mail{$k} =~ s/\012/$CRLF/go; } } $smtp = $mail{'Smtp'} || $mail{'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'} || $mailcfg{'port'} || 25; delete $mail{'Port'}; my $auth = $mail{'Auth'}; delete $mail{'Auth'}; { # don't warn for undefined values below local $^W = 0; $mail{'Message'} = join("", $mail{'Message'}, $mail{'Body'}, $mail{'Text'}); } # delete @mail{'Body', 'Text'}; delete $mail{'Body'}; delete $mail{'Text'}; # Extract 'From:' e-mail address to use as envelope sender $fromaddr = $mail{'Sender'} || $mail{'From'} || $mailcfg{'from'}; #delete $mail{'Sender'}; 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 $mail{'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'; $mail{'Message'} = encode_qp($mail{'Message'}); } else { $mail{'Content-Transfer-Encoding'} = '8bit'; if ($mail{'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 without encoding, hoping it will come across OK.\n" if $^W; } } } $mail{'Message'} =~ s/^\./\.\./gom; # handle . as first character $mail{'Message'} =~ s/\n/$CRLF/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 = 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; $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 } 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" ; } my($oldfh) = select(S); $| = 1; select($oldfh); socket_read() || return fail("Connection error from $smtp on port $port ($_)"); socket_write("EHLO $localhost$CRLF") || return fail("send EHLO error (lost connection?)"); my $ehlo = socket_read(); if ($ehlo) { # parse EHLO response map { s/^\d+[- ]//; my ($k, $v) = split /\s+/, $_, 2; $esmtp{$k} = $v || 1 if $k; } split(/\n/, $ehlo); } else { # try plain HELO instead socket_write("HELO $localhost$CRLF") || return fail("send HELO error (lost connection?)"); } if ($auth) { warn "AUTH requested\n" if ($mailcfg{debug} > 4); # reduce wanted methods to those supported my @methods = grep {$esmtp{'AUTH'}=~/(^|\s)$_(\s|$)/i} grep {$auth_support =~ /(^|\s)$_(\s|$)/i} grep /\S/, split(/\s+/, $auth->{method}); if (@methods) { # try to authenticate if (exists $auth->{pass}) { $auth->{password} = $auth->{pass}; } my $method = uc $methods[0]; _require_base64() || fail("Could not use MIME::Base64 module required for authentication"); if ($method eq "LOGIN") { print STDERR "Trying AUTH LOGIN\n" if ($mailcfg{debug} > 9); socket_write("AUTH LOGIN$CRLF") || return fail("send AUTH LOGIN failed (lost connection?)"); socket_read() || return fail("AUTH LOGIN failed: $server_reply"); socket_write(encode_base64($auth->{user},$CRLF)) || return fail("send LOGIN username failed (lost connection?)"); socket_read() || return fail("LOGIN username failed: $server_reply"); socket_write(encode_base64($auth->{password},$CRLF)) || return fail("send LOGIN password failed (lost connection?)"); socket_read() || return fail("LOGIN password failed: $server_reply"); } elsif ($method eq "PLAIN") { warn "Trying AUTH PLAIN\n" if ($mailcfg{debug} > 9); socket_write( "AUTH PLAIN " . encode_base64(join("\0", $auth->{user}, $auth->{user}, $auth->{password}), $CRLF) ) || return fail("send AUTH PLAIN failed (lost connection?)"); socket_read() || return fail("AUTH PLAIN failed: $server_reply"); } elsif ($method eq "CRAM-MD5") { _require_md5() || fail("Could not use Digest::MD5 module required for authentication"); warn "Trying AUTH CRAM-MD5\n" if ($mailcfg{debug} > 9); socket_write("AUTH CRAM-MD5$CRLF") || return fail("send CRAM-MD5 failed (lost connection?)"); my $challenge = socket_read() || return fail("AUTH CRAM-MD5 failed: $server_reply"); $challenge =~ s/^\d+\s+//; my $response = _hmac_md5($auth->{password}, decode_base64($challenge)); socket_write(encode_base64("$auth->{user} $response", $CRLF)) || return fail("AUTH CRAM-MD5 failed: $server_reply"); socket_read() || return fail("AUTH CRAM-MD5 failed: $server_reply"); } elsif ($method eq "DIGEST-MD5") { _require_md5() || fail("Could not use Digest::MD5 module required for authentication"); warn "Trying AUTH DIGEST-MD5\n" if ($mailcfg{debug} > 9); socket_write("AUTH DIGEST-MD5$CRLF") || return fail("send CRAM-MD5 failed (lost connection?)"); 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); my $response = _digest_md5($auth->{user}, $auth->{password}, decode_base64($challenge), $auth->{realm}); warn "\nRESPONSE=$response\n" if ($mailcfg{debug} > 10); socket_write(encode_base64($response, ""), $CRLF) || return fail("AUTH DIGEST-MD5 failed: $server_reply"); my $status = socket_read() || return fail("AUTH DIGEST-MD5 failed: $server_reply"); if ($status =~ /^3/) { socket_write($CRLF) || return fail("AUTH DIGEST-MD5 failed: $server_reply"); socket_read() || return fail("AUTH DIGEST-MD5 failed: $server_reply"); } } else { return fail("$method not supported (and wrongly advertised as supported by this silly module)\n"); } $log .= "AUTH $method succeeded as user $auth->{user}\n"; } else { $esmtp{'AUTH'} =~ s/(^\s+|\s+$)//g; # cleanup for printig it below if ($auth->{required}) { return fail("Required AUTH method '$auth->{method}' not supported. " ."(Server supports '$esmtp{'AUTH'}'. Module supports: '$auth_support')"); } else { warn "No common authentication method! Requested: '$auth->{method}'. Server supports '$esmtp{'AUTH'}'. Module supports: '$auth_support'. Skipping authentication\n"; } } } socket_write("MAIL FROM:<$fromaddr>$CRLF") || return fail("send MAIL FROM: error"); socket_read() || return fail("MAIL FROM: error ($_)"); my $to_ok = 0; foreach $to (@recipients) { socket_write("RCPT TO:<$to>$CRLF") || return fail("send RCPT TO: error"); if (socket_read()) { $log .= "To: $to\n"; $to_ok++; } else { $log .= "FAILED To: $to ($server_reply)"; $error .= "Bad recipient <$to>: $server_reply\n"; } } unless ($to_ok) { return fail("No valid recipient"); } # start data part socket_write("DATA$CRLF") || return fail("send DATA error"); socket_read() || return fail("DATA error ($_)"); # print headers foreach $header (keys %mail) { next if $header eq "Message"; $mail{$header} =~ s/\s+$//o; # kill possible trailing garbage socket_write("$header: $mail{$header}$CRLF") || return fail("send $header: error"); }; #- 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 (passed as a reference, in case it's big) socket_write($CRLF, \$mail{'Message'}, "$CRLF.$CRLF") || return fail("send message error"); socket_read() || return fail("message transmission error ($_)"); $log .= "\nResult: $_"; # finish socket_write("QUIT$CRLF") || return fail("send QUIT error"); socket_read(); close S; return 1; } # end sub sendmail # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Main code here # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ my $host = get_param("h"); my $destination = get_param("d"); my $from = get_param("f"); if (($host eq "") || ($destination eq "") || ($from eq "")){ print 'Pandora SMTP Remote plugin, (c) 2012 slerena@gmail.com'; print "\n\nThis plugin is used to send a mail to a SMTP server and check if works\n\nUsage:\n\n"; print " -h SMTP Server IP address\n"; print " -d Destination email\n"; print " -f Email of the sender\n"; print "\nOptional parameters \n\n"; print " -a Autentication system, could be LOGIN, PLAIN, CRAM-MD5 or DIGEST-MD\n"; print " -o SMTP Port (25 by default)\n"; print " -u user (only if MTA auth required)\n"; print " -p password (only if MTA auth required)\n"; print " -e debug - Show error (for testing in console only!)\n"; print "\n"; exit; } # Optional parameters my $user = get_param("u"); my $pass = get_param("p"); my $port = get_param("o"); if ($port eq ""){ $port = 25; } my $show_error = get_param("e"); my $auth = get_param("a"); my $subject = "Pandora FMS SMTP Test"; my $message = "This is a check for SMTP done with Pandora FMS"; my %mail = ( To => $destination, Message => $message, Subject => $subject, 'X-Mailer' => "Pandora FMS", Smtp => $host, Port => $port, From => $from, ); if ($auth ne ""){ $mail{auth} = {user=>$user, password=>$pass, method=>$auth, required=>1 }; } if (sendmail %mail) { print "1\n"; } else { print "0\n"; if ($show_error ne ""){ if (defined($Mail::Sendmail::error)){ print "ERROR Code: $Mail::Sendmail::error \n"; } else { print "Undefined error ¿?\n"; } } } exit;