diff --git a/lib/Log/Dispatch/Email.pm b/lib/Log/Dispatch/Email.pm index c6a690c..9d269b7 100644 --- a/lib/Log/Dispatch/Email.pm +++ b/lib/Log/Dispatch/Email.pm @@ -10,7 +10,7 @@ use Log::Dispatch::Output; use base qw( Log::Dispatch::Output ); use Devel::GlobalDestruction qw( in_global_destruction ); -use Params::Validate qw(validate SCALAR ARRAYREF BOOLEAN); +use Params::Validate qw(validate SCALAR ARRAYREF HASHREF BOOLEAN); Params::Validate::validation_options( allow_extra => 1 ); # need to untaint this value @@ -31,6 +31,10 @@ sub new { type => SCALAR, optional => 1 }, + send_args => { + type => SCALAR | ARRAYREF | HASHREF, + optional => 1 + }, buffered => { type => BOOLEAN, default => 1 @@ -43,8 +47,9 @@ sub new { $self->_basic_init(%p); $self->{subject} = $p{subject} || "$0: log email"; - $self->{to} = ref $p{to} ? $p{to} : [ $p{to} ]; - $self->{from} = $p{from}; + $self->{to} = ref $p{to} ? $p{to} : [ $p{to} ]; + $self->{from} = $p{from}; + $self->{send_args} = $p{send_args}; # Default to buffered for obvious reasons! $self->{buffered} = $p{buffered}; @@ -154,6 +159,11 @@ addresses. Required. A string containing an email address. This is optional and may not work with all mail sending methods. +=item * send_args ($, \@ or \%) + +This stores any options used to send email, such as smtp host, port, etc. +These are different for each subclass. + =item * buffered (0 or 1) This determines whether the object sends one email per message it is diff --git a/lib/Log/Dispatch/Email/MIMELite.pm b/lib/Log/Dispatch/Email/MIMELite.pm index bd4b426..d0855b0 100644 --- a/lib/Log/Dispatch/Email/MIMELite.pm +++ b/lib/Log/Dispatch/Email/MIMELite.pm @@ -16,7 +16,7 @@ sub send_email { my %p = @_; my %mail = ( - To => ( join ',', @{ $self->{to} } ), + To => ( join ',', @{ $self->{to} } ), Subject => $self->{subject}, Type => 'TEXT', Data => $p{message}, @@ -25,9 +25,11 @@ sub send_email { $mail{From} = $self->{from} if defined $self->{from}; local $?; - unless ( MIME::Lite->new(%mail)->send ) { - warn "Error sending mail with MIME::Lite"; - } + warn "Error sending mail with MIME::Lite" + unless do { + MIME::Lite->new(%mail)->send( @{ $self->{send_args} || [] } ); + }; + } 1; @@ -46,7 +48,8 @@ __END__ 'Email::MIMELite', min_level => 'emerg', to => [qw( foo@example.com bar@example.org )], - subject => 'Big error!' + subject => 'Big error!', + send_args => [ 'smtp', 'smtp.example.org', Port => 465, AuthUser => 'john', AuthPass => 'secret' ] ] ], ); @@ -58,4 +61,9 @@ __END__ This is a subclass of L that implements the send_email method using the L module. +=head1 CHANGING HOW MAIL IS SENT + +To change how mail is sent, set send_args to according to what +L<< MIME::Lite->send >> expects. + =cut diff --git a/lib/Log/Dispatch/Email/MailSend.pm b/lib/Log/Dispatch/Email/MailSend.pm index bb0e670..418c83a 100644 --- a/lib/Log/Dispatch/Email/MailSend.pm +++ b/lib/Log/Dispatch/Email/MailSend.pm @@ -25,7 +25,7 @@ sub send_email { local $?; eval { - my $fh = $msg->open + my $fh = $msg->open( @{ $self->{send_args} } ) or die "Cannot open handle to mail program"; $fh->print( $p{message} ) @@ -54,7 +54,8 @@ __END__ 'Email::MailSend', min_level => 'emerg', to => [qw( foo@example.com bar@example.org )], - subject => 'Big error!' + subject => 'Big error!', + send_args => [ 'smtp', Server => 'mail.example.org', Hello => 'foobar.com' ], ] ], ); @@ -68,6 +69,12 @@ method using the L module. =head1 CHANGING HOW MAIL IS SENT +There are two ways to change how mail is sent: + +=over 4 + +=item 1 + Since L is a subclass of L, you can change how mail is sent from this module by simply Cing L in your code before mail is sent. For example, to send mail via smtp, @@ -75,6 +82,13 @@ you could do: use Mail::Mailer 'smtp', Server => 'foo.example.com'; +=item 2 + +Set send_args to the same arguments as +the constructor of L expects. + +=back + For more details, see the L docs. =cut diff --git a/lib/Log/Dispatch/Email/MailSendmail.pm b/lib/Log/Dispatch/Email/MailSendmail.pm index 3214ebe..69520f2 100644 --- a/lib/Log/Dispatch/Email/MailSendmail.pm +++ b/lib/Log/Dispatch/Email/MailSendmail.pm @@ -24,6 +24,9 @@ sub send_email { From => $self->{from} || 'LogDispatch@foo.bar', ); + # merge options from %{send_args} + %mail = ( %mail, %{ $self->{send_args} } ) if defined $self->{send_args}; + local $?; unless ( Mail::Sendmail::sendmail(%mail) ) { warn "Error sending mail: $Mail::Sendmail::error"; @@ -46,7 +49,8 @@ __END__ 'Email::MailSendmail', min_level => 'emerg', to => [qw( foo@example.com bar@example.org )], - subject => 'Big error!' + subject => 'Big error!', + send_args => { smtp => '127.0.0.1', retries => 10, delay => 5, debug => 0, X-Custom-Header => 'epale' } ] ], ); @@ -58,4 +62,9 @@ __END__ This is a subclass of L that implements the send_email method using the L module. +=head1 CHANGING HOW MAIL IS SENT + +To change how mail is sent, set send_args to a hash reference just +like L<< %Mail::Sendmail::mailcfg >>. + =cut diff --git a/t/send_args-exim.conf b/t/send_args-exim.conf new file mode 100644 index 0000000..8693ef7 --- /dev/null +++ b/t/send_args-exim.conf @@ -0,0 +1,96 @@ +# This is a sample exim config file that I used together with send_args-test.pl +# to test send_args in Log::Dispatch::Mail modules. +# +# It listens on ports 25, 9025 and 9026, but requires authentication only for port 9026 +# +# It adds these headers: +# X-Exim-Source: source of the email (eg: sendmail invocation, tcp, etc) +# X-Exim-User: user that authenticated to send the email, or none +# X-Exim-Flag1: added by the router (at transport time) +# X-Exim-Flag2: added by the transport + +# this should be changed to some smtp server that will receive your emails: +SMTP_SMARTHOST = 172.16.8.51 + +# from which tcp ports we require authentication (25 and 9025 are missing) +# this is treated as a list +AUTH_PORTS = 9026 + +# user accounts, I used pipe to separate each user and password +AUTH_ACCOUNTS = user1|pass1 : alan|secreto : user3|pass3 + +acl_smtp_rcpt = acl_rcpt_to +acl_not_smtp = acl_sendmail + +daemon_smtp_ports = 25 : 9025 : 9026 + +ignore_bounce_errors_after = 2d +timeout_frozen_after = 30d + +# allow to change the From: +local_from_check = false + +# log to syslog +log_file_path = syslog +syslog_duplication = false +syslog_timestamp = false + +begin acl + +# locally generated +acl_sendmail: + accept + add_header = X-Exim-Source: command line + add_header = X-Exim-User: none (not required) + logwrite = Accepted from source: command line invocation + +# RCPT TO: +acl_rcpt_to: + accept + condition = ${if !forany{AUTH_PORTS}{match{$item}{$received_port}}{yes}{no}} + add_header = X-Exim-Source: SMTP $received_ip_address:$received_port + add_header = X-Exim-User: none (not required) + logwrite = Accepted from source: SMTP $received_ip_address:$received_port username: none (not required) + accept + authenticated = plain : login + add_header = X-Exim-Source: SMTP $received_ip_address:$received_port + add_header = X-Exim-User: $authenticated_id + logwrite = Accepted from source: SMTP $received_ip_address:$received_port username: $authenticated_id + deny + message = Authentication is required. Interface: $interface_address username: $authenticated_id + +begin routers +snd_smarthost: + driver = manualroute + route_list = !\N^$\N SMTP_SMARTHOST + transport = remote_smtp + errors_to = + headers_add = X-Exim-Flag1: hola + +begin transports +remote_smtp: + driver = smtp + headers_add = X-Exim-Flag2: hola + +begin retry + * * F,2h,15m; G,16h,1h,1.5; F,4d,6h + +begin rewrite + +begin authenticators + +plain: + driver = plaintext + public_name = PLAIN + server_advertise_condition = true + server_prompts = : + server_condition = ${if inlist{$auth2|$auth3}{AUTH_ACCOUNTS}} + server_set_id = $auth2 + +login: + driver = plaintext + public_name = LOGIN + server_advertise_condition = true + server_prompts = Username:: : Password:: + server_condition = ${if inlist{$auth1|$auth2}{AUTH_ACCOUNTS}} + server_set_id = $auth1 diff --git a/t/send_args-test.pl b/t/send_args-test.pl new file mode 100755 index 0000000..d7ef0e4 --- /dev/null +++ b/t/send_args-test.pl @@ -0,0 +1,387 @@ +#!/usr/bin/perl +# This script tests different ways of specifying send_args +# for each Log::Dispatch::Email module. +# +# This is not an automatic test, it is in fact very time consuming. +# +# See my sample exim.conf to set up an smtp server for these tests. +# +# Test procedure: +# 1. Configure your smtp server (see sample exim.conf) +# 2. Customize this script with your email address, and different send_args to test +# 3. Run this script +# 4. Check exim's log file +# 5. Check your received emails. +# Verify if send_args were used as expected: +# check X-Exim-Source header, other headers, envelope, etc. +# 6. Disable successful tests, fix send_args for failed tests, go back to step 3. +# + +use strict; +use warnings; +use Log::Dispatch; +use Log::Dispatch::Screen::Color; +use Log::Dispatch::Email::MIMELite; +use Log::Dispatch::Email::MailSend; +use Log::Dispatch::Email::MailSendmail; +use Log::Dispatch::Email::EmailSend; # <- I had to make some changes to this one, mailer_args was not being used + +# where to send all emails: +my $email = 'someone@test.com'; + +# for testing modules that don't support any options or authentication +# in this case we test by setting a custom X-Test header, or HELO +my $server1_host = '127.0.0.1'; + +# for testing modules that support specifying a custom port number +# but not authentication +my $server2_host = '127.0.0.1'; +my $server2_port = 9025; + +# for testing modules that support specyfing a custom port number +# but not authentication +my $server3_host = '127.0.0.1'; +my $server3_port = 9026; +my $server3_user = 'alan'; +my $server3_pass = 'secreto'; + +my $log = Log::Dispatch->new(); +$log->add( Log::Dispatch::Screen::Color->new( min_level => 'info', name => 'Screen-1' ) ); +my $i; + + +################# +# +# Log::Dispatch::Email::MIMELite +# +# for smtp we check that the options are really used by verifying the mail logs and mail headers of received emails, they should indicate that authentication worked. +# +# note: To see debug messages from MIME::Lite: +# global: $MIME::Lite::DEBUG = 1; +# in send_args: Debug => 1 + +my @MIMELite = ( + { # 0 works, used sendmail + # disabled => 1, + test => "no send_args", + }, + { # 1 works, used sendmail + # disabled => 1, + test => "send_args = [ ]", + send_args => [ ], + }, + { # 2 works? + # disabled => 1, + test => "send_args = [ 'testfile' ]", + send_args => [ 'testfile' ] + }, + { # 3 works, results in /tmp/MIMELite-test. + # disabled => 1, + test => "send_args = [ 'testfile', '/tmp/MIMELite-test.$$' ]", + send_args => [ 'testfile', "/tmp/MIMELite-test.$$" ] + }, + { # 4 works + # disabled => 1, + test => "send_args = [ 'sendmail' ]", + send_args => [ 'sendmail' ] + }, + { # 5 works + # disabled => 1, + test => "send_args = [ 'sendmail', '/usr/sbin/sendmail -ti' ]", + send_args => [ 'sendmail', '/usr/sbin/sendmail -ti' ] + }, + { # 6 works, used sendmail instead of smtp + # disabled => 1, + test => "send_args = [ 'smtp' ]", + send_args => [ 'smtp' ] + }, + { # 7 works + # disabled => 1, + test => "send_args = [ 'smtp', $server1_host ]", + send_args => [ 'smtp', $server1_host ] + }, + { # 8 works + # disabled => 1, + test => "send_args = [ 'smtp', $server3_host, Port => $server3_port, AuthUser => $server3_user, AuthPass => $server3_pass ]", + send_args => [ 'smtp', $server3_host, Port => $server3_port, AuthUser => $server3_user, AuthPass => $server3_pass ] + }, + { # 9 works + # disabled => 1, + test => "send_args = [ 'smtp', ['badhost1', 'badhost2', $server3_host, 'unused-host'], Port => $server3_port, AuthUser => $server3_user, AuthPass => $server3_pass", + send_args => [ 'smtp', ['badhost1', 'badhost2', $server3_host, 'unused-host'], Port => $server3_port, AuthUser => $server3_user, AuthPass => $server3_pass ] + }, + { # 10 fails! (but this works with smtp_simple! see test #16) + # disabled => 1, + test => "send_args = [ 'smtp', Host => ['badhost1', 'badhost2', $server3_host:$server3_port , 'unused-host'], Port => $server3_port, AuthUser => $server3_user, AuthPass => $server3_pass ]", + send_args => [ 'smtp', Host => ['badhost1', 'badhost2', "$server3_host:$server3_port", 'unused-host'], Port => $server3_port, AuthUser => $server3_user, AuthPass => $server3_pass ] + }, + + # The smtp_simple method uses Net::SMTP. + # Net::SMTP->new expects the host first, and args next when number of args is odd. The host can be an arrayref of hosts. + # Net::SMTP->new expects the host as a hash element if number of args is even. (note: MIME::Lite removes the first one before calling Net::SMTP->new) + # smtp_simple doesn't support authentication, so we set some headers instead to test if the args are used + { # 11 works (should use .libnetrc to find smtp server, in my test it was missing and used sendmail) + # disabled => 1, + test => "send_args = [ 'smtp_simple' ]", + send_args => [ 'smtp_simple' ] + }, + { # 12 works + # disabled => 1, + test => "send_args = [ 'smtp_simple', $server1_host ]", + send_args => [ 'smtp_simple', $server1_host ] + }, + { # 13 works + # disabled => 1, + test => "send_args = [ 'smtp_simple', $server2_host, Port => $server2_port ]", + send_args => [ 'smtp_simple', $server2_host, Port => $server2_port ] + }, + { # 14 works + # disabled => 1, + test => "send_args = [ 'smtp_simple', $server2_host:$server2_port ]", + send_args => [ 'smtp_simple', "$server2_host:$server2_port" ] + }, + { # 15 works + # disabled => 1, + test => "send_args = [ 'smtp_simple', ['server-invalid', $server2_host, 'server-unused'], Port => $server2_port ]", + send_args => [ 'smtp_simple', ['server-invalid', $server2_host, 'server-unused'], Port => $server2_port ] + }, + { # 16 works + # disabled => 1, + test => "send_args = [ 'smtp_simple', ['server-invalid', $server2_host:$server2_port , 'server-unused'], ]", + send_args => [ 'smtp_simple', ['server-invalid', "$server2_host:$server2_port", 'server-unused'], ] + }, + +); + +$i = -1; +foreach (@MIMELite) { + # next; # uncomment to skip MIME::Lite tests + $i++; + next if $_->{disabled}; + $_->{name} = sprintf('MIMELite-%02d with %s', $i, delete $_->{test}); + $_->{subject} = 'Log::Dispatch test: ' . $_->{name}; + $_->{to} = $email; + $_->{min_level} = 'info'; + $log->add(Log::Dispatch::Email::MIMELite->new($_)); +} + + +################# +# +# Log::Dispatch::Email::MailSend +# +# when using smtp: the options will be the same as the constructor for Net::SMTP +# when using smtps: the options will be the same as the constructor for Net::SMTP::SSL +# +# note: we can't prove that the options are working by using smtp with server2 because there is no way to provide auth credentials when using Log::Dispatch::Email::MailSend. Instead we use a custom Hello and later check if it was really used. +# +# note2: +# To see debug messages from Net::SMTP: +# add Debug => 1 to send_args + +my @MailSend = ( + { # 0 works, sent via sendmail + # disabled => 1, + test => "no send_args", + }, + { # 1 works, sent via sendmail + # disabled => 1, + test => "send_args = [ ]", + send_args => [ ], + }, + { # 2 works, sent via sendmail + # disabled => 1, + test => "send_args = [ 'sendmail' ]", + send_args => [ 'sendmail' ], + }, + { # 3 fails: I don't have qmail installed + # disabled => 1, + test => "send_args = [ 'qmail' ]", + send_args => [ 'qmail' ], + }, + { # 4 fails: no server specified, and doesn't use .libnetrc + # disabled => 1, + test => "send_args = [ 'smtp' ]", + send_args => [ 'smtp' ], + }, + { # 5 works + # disabled => 1, + test => "send_args = [ 'smtp', Server => $server1_host ]", + send_args => [ 'smtp', Server => $server1_host ], + }, + { # 6 works, hello sent correctly + # disabled => 1, + test => "send_args = [ 'smtp', Server => $server1_host, Hello => 'banana.net' ]", + send_args => [ 'smtp', Server => $server1_host, Hello => 'banana.net' ], + }, + { # 7 fails: I'm missing Net::SMTP::SSL and a working smtps server, repeat test after setting it up properly + # disabled => 1, + test => "send_args = [ 'smtps' ]", + send_args => [ 'smtps' ], + }, + { # 8 fails: I'm missing Net::SMTP::SSL and a working smtps server, repeat test after setting it up properly + # disabled => 1, + test => "send_args = [ 'smtps', Server => $server1_host, Hello => 'cambur.com' ]", + send_args => [ 'smtps', Server => $server1_host, Hello => 'cambur.com' ], + }, + { # 9 works: result in $PWD/mailer.testfile + # disabled => 1, + test => "send_args = [ 'testfile' ]", + send_args => [ 'testfile' ], + }, +); + +$i = -1; +foreach (@MailSend) { + # next; # uncomment to skip MailSend tests + $i++; + next if $_->{disabled}; + $_->{name} = sprintf('MailSend-%02d with %s', $i, delete $_->{test}); + $_->{subject} = 'Log::Dispatch test: ' . $_->{name}; + $_->{to} = $email; + $_->{min_level} = 'info'; + $log->add(Log::Dispatch::Email::MailSend->new($_)); +} + + +################# +# +# Log::Dispatch::Email::MailSendmail +# +# send_args is a hash reference like %Mail::Sendmail::mailcfg +# +# note1: to see debug messages from Mail::Sendmail add debug => x to send_args, where x is the desired level from 0 to 6. +# +# note2: to prove if the options are really used, we add a custom header: X-Test + +my @MailSendmail = ( + { # 0 works, sent to 127.0.0.1 on port 25 + # disabled => 1, + test => "no send_args", + }, + { # 1 fails: must use a hash reference (should I improve my patch to allow arrayrefs?) + # disabled => 1, + test => "send_args = [ ]", + send_args => [ ], + }, + { # 2 works, sent to 127.0.0.1 on port 25 + # disabled => 1, + test => "send_args = { }", + send_args => { }, + }, + { # 3 works + # disabled => 1, + test => "send_args = { smtp => $server1_host }", + send_args => { smtp => $server1_host } + }, + { # 4 works, X-Test was present + # disabled => 1, + test => "send_args = { smtp => $server1_host, 'X-Test' => 'matute' }", + send_args => { smtp => $server1_host, 'X-Test' => 'matute' } + }, + { # 5 works, X-Test was present + # disabled => 1, + test => "send_args = { smtp => [ 'badserver1', 'badserver2', $server1_host ], 'X-Test' => 'comejobo' }", + send_args => { smtp => [ 'badserver1', 'badserver2', $server1_host ], 'X-Test' => 'comejobo' } + }, +); + +$i=-1; +foreach (@MailSendmail) { + # next; # uncomment to skip Mail::Sendmail tests + $i++; + next if $_->{disabled}; + $_->{name} = sprintf('MailSendmail-%02d with %s', $i, delete $_->{test}); + $_->{subject} = 'Log::Dispatch test: ' . $_->{name}; + $_->{to} = $email; + $_->{min_level} = 'info'; + $log->add(Log::Dispatch::Email::MailSendmail->new($_)); +} + + +################ +# +# Log::Dispatch::Email::EmailSend +# + +# These tests also verify backward compatibility + +# note1: +# Email::Send uses Net::SMTP, but adds the ability to provide authentication +# +# note2: +# To see debug messages add Debug => 1 to send_args + +my @EmailSend = ( + { # 0 works, sent via sendmail + # disabled => 1, + test => "no send_args", + }, + { # 1 works, sent via sendmail + # disabled => 1, + test => "send_args = [ ]", + send_args => [ ], + }, + { # 2 works + # disabled => 1, + test => "send_args = [ 'SMTP', Host => $server1_host ]", + send_args => [ 'SMTP', Host => $server1_host ] + }, + { # 3 fails with Log::Dispatch::Email::EmailSend 0.03 and Email::Send 2.201 Reason: mailer_args are never used. + # works with my changes + # disabled => 1, + test => "send_args = [ 'SMTP', Host => $server3_host, Port => $server3_port, username => $server3_user, password => $server3_pass ]", + send_args => [ 'SMTP', Host => $server3_host, Port => $server3_port, username => $server3_user, password => $server3_pass ] + }, + { # 4 works + # disabled => 1, + test => "send_args = [ 'Sendmail' ]", + send_args => [ 'Sendmail' ] + }, + { # 5 fails with Log::Dispatch::Email::EmailSend 0.03 and Email::Send 2.201 Reason: mailer_args are never used. + # works with my changes + # disabled => 1, + test => "mailer = 'SMTP', mailer_args = [ $server3_host, Port => $server3_port, username => $server3_user, password => $server3_pass ]", + mailer => 'SMTP', + mailer_args => [ $server3_host, Port => $server3_port, username => $server3_user, password => $server3_pass ], + }, + { # 6 works, old api test + # disabled => 1, + test => "mailer = 'Sendmail'", + mailer => 'Sendmail', + }, + { # 7 works, old and new mixed up, ends up using smtp + # disabled => 1, + test => "mailer = 'Sendmail', send_args = [ 'SMTP', Host => $server3_host, Port => $server3_port, username => $server3_user, password => $server3_pass ]", + mailer => 'Sendmail', + send_args => [ 'SMTP', Host => $server3_host, Port => $server3_port, username => $server3_user, password => $server3_pass ], + }, + { # 8 old and new mixed up, should use the new one: sendmail + # works with my changes + # disabled => 1, + test => "mailer = 'SMTP', mailer_args = [ $server1_host ], send_args = [ 'Sendmail' ]", + mailer => 'SMTP', + mailer_args => [ $server1_host ], + send_args => [ 'Sendmail' ], + }, +); + +$i=-1; +foreach (@EmailSend) { + # next; # uncomment to skip Email::EmailSend tests + $i++; + next if $_->{disabled}; + $_->{name} = sprintf('EmailSend-%02d with %s', $i, delete $_->{test}); + $_->{subject} = 'Log::Dispatch test: ' . $_->{name}; + $_->{to} = $email; + $_->{min_level} = 'info'; + $log->add(Log::Dispatch::Email::EmailSend->new($_)); +} + +$log->info("This is a test message from PID: $$ at: " . localtime . "\n"); + +foreach my $d ( sort { $a->name cmp $b->name } $log->outputs ) { + print "Dispatching message via $d->{name}\n"; +} + +exit 0;