File Coverage

lib/Mail/DMARC/Report/Sender.pm
Criterion Covered Total %
statement 173 272 63.6
branch 42 106 39.6
condition 9 20 45.0
subroutine 23 24 95.8
pod 0 10 0.0
total 247 432 57.1


line stmt bran cond sub pod time code
1             package Mail::DMARC::Report::Sender;
2              
3 1     1   10815 use strict;
  1         2  
  1         31  
4 1     1   5 use warnings;
  1         2  
  1         45  
5              
6 1     1   8 use Data::Dumper;
  1         1  
  1         64  
7 1     1   7 use Carp;
  1         2  
  1         111  
8 1     1   8 use Encode;
  1         2  
  1         91  
9 1     1   777 use Getopt::Long;
  1         10383  
  1         6  
10 1     1   957 use Sys::Syslog qw(:standard :macros);
  1         8508  
  1         306  
11 1     1   13 use Mail::DMARC::Report;
  1         2  
  1         30  
12 1     1   6 use Email::Sender;
  1         2  
  1         31  
13 1     1   576 use Email::Sender::Simple qw{ sendmail };
  1         13503  
  1         6  
14 1     1   958 use Email::Sender::Transport::SMTP;
  1         74834  
  1         48  
15 1     1   688 use Email::Sender::Transport::SMTP::Persistent;
  1         2048  
  1         32  
16 1     1   471 use Module::Load;
  1         1147  
  1         6  
17              
18             sub new {
19 4     4 0 74 my $class = shift;
20 4         68 my $self = {
21             send_delay => 5,
22             batch_size => 1,
23             alarm_at => 120,
24             syslog => 0,
25             smarthost => undef,
26             transports_method => undef,
27             transports_object => undef,
28             dkim_key => undef,
29             verbose => 0,
30             };
31 4         16 return bless $self, $class;
32             };
33              
34             sub set_transports_object {
35 5     5 0 19 my ( $self,$transports_object ) = @_;
36 5         13 $self->{transports_object} = $transports_object;
37 5         8 return;
38             }
39              
40             sub set_transports_method {
41 3     3 0 2645 my ( $self,$transports_method ) = @_;
42 3         48 $self->{transports_method} = $transports_method;
43 3         8 return;
44             # Transports method is a sub which returns
45             # a list of transports for the given args.
46             }
47              
48             # Return a list of transports to try in order.
49             sub get_transports_for {
50 4     4 0 10 my ( $self, $args ) = @_;
51             # Have we passed a custom transports generation class?
52 4 100       27 if ( $self->{transports_method} ) {
53 3         6 my @transports = &{$self->{transports_method}}( $args );
  3         13  
54 3         28 return @transports;
55             }
56 1 50       13 if ( $self->{transports_object} ) {
57 1         7 my @transports = $self->{transports_object}->get_transports_for( $args );
58 1         3 return @transports;
59             }
60              
61 0         0 my $report = $args->{report};
62              
63 0         0 my $transport_can_maybetls = $Email::Sender::VERSION > 2.0;
64              
65             # Do we have a smart host?
66 0 0       0 if ( $report->config->{smtp}{smarthost} ) {
67 0 0       0 return ($self->{smarthost}) if $self->{smarthost};
68             my $transport_data = {
69             host => $report->config->{smtp}->{smarthost},
70 0         0 ssl => 'starttls',
71             port => 587,
72             helo => $report->sendit->smtp->get_helo_hostname,
73             timeout => 32,
74             };
75 0 0       0 $transport_data->{sasl_username} = $report->config->{smtp}->{smartuser} if $report->config->{smtp}->{smartuser};
76 0 0       0 $transport_data->{sasl_password} = $report->config->{smtp}->{smartpass} if $report->config->{smtp}->{smartpass};
77 0         0 my $transport = Email::Sender::Transport::SMTP::Persistent->new($transport_data);
78 0         0 $self->{smarthost} = $transport;
79 0         0 return ($self->{smarthost});
80             }
81              
82 0         0 my @smtp_hosts = $report->sendit->smtp->get_smtp_hosts($args->{to});
83              
84 0         0 my $log_data = $args->{log_data};
85 0         0 my @transports;
86 0         0 $log_data->{smtp_host} = join( ',', @smtp_hosts );
87              
88 0 0       0 if ( Email::Sender::Transport::SMTP->can('hosts') ) {
89 0 0       0 if ( $transport_can_maybetls ) {
90 0         0 push @transports, Email::Sender::Transport::SMTP->new({
91             hosts => \@smtp_hosts,
92             ssl => 'maybestarttls',
93             port => 25,
94             helo => $report->sendit->smtp->get_helo_hostname,
95             timeout => 32,
96             });
97             }
98             else {
99 0         0 push @transports, Email::Sender::Transport::SMTP->new({
100             hosts => \@smtp_hosts,
101             ssl => 'starttls',
102             port => 25,
103             helo => $report->sendit->smtp->get_helo_hostname,
104             timeout => 32,
105             });
106 0         0 push @transports, Email::Sender::Transport::SMTP->new({
107             hosts => \@smtp_hosts,
108             ssl => 0,
109             port => 25,
110             helo => $report->sendit->smtp->get_helo_hostname,
111             timeout => 32,
112             });
113             }
114             }
115             else {
116             # We can't pass hosts to the transport, so pass a list of transports
117             # for each possible host.
118              
119 0 0       0 if ( $transport_can_maybetls ) {
120 0         0 foreach my $host ( @smtp_hosts ) {
121 0         0 push @transports, Email::Sender::Transport::SMTP->new({
122             host => $host,
123             ssl => 'maybestarttls',
124             port => 25,
125             helo => $report->sendit->smtp->get_helo_hostname,
126             timeout => 32,
127             });
128             }
129             }
130             else {
131 0         0 foreach my $host ( @smtp_hosts ) {
132 0         0 push @transports, Email::Sender::Transport::SMTP->new({
133             host => $host,
134             ssl => 'starttls',
135             port => 25,
136             helo => $report->sendit->smtp->get_helo_hostname,
137             timeout => 32,
138             });
139             }
140 0         0 foreach my $host ( @smtp_hosts ) {
141 0         0 push @transports, Email::Sender::Transport::SMTP->new({
142             host => $host,
143             ssl => 0,
144             port => 25,
145             helo => $report->sendit->smtp->get_helo_hostname,
146             timeout => 32,
147             });
148             }
149             }
150             }
151              
152 0         0 return @transports;
153             }
154              
155             sub get_dkim_key {
156 4     4 0 11 my ( $self ) = @_;
157 4         21 my $report = $self->{report};
158 4 50       16 return $self->{dkim_key} if $self->{dkim_key};
159 4 50       15 if ( $report->config->{report_sign}->{keyfile} ) {
160 0         0 eval {
161 0         0 require Mail::DKIM::PrivateKey;
162 0         0 require Mail::DKIM::Signer;
163 0         0 require Mail::DKIM::TextWrap;
164             };
165 0 0       0 if ( UNIVERSAL::can( 'Mail::DKIM::Signer', "new" ) ) {
166 0         0 my $file = $report->config->{report_sign}->{keyfile};
167 0         0 $self->{dkim_key} = Mail::DKIM::PrivateKey->load(
168             'File' => $file,
169             );
170 0 0       0 if ( ! $self->{dkim_key} ) {
171 0         0 die "Could not load DKIM key $file";
172             }
173             }
174             else {
175 0         0 die 'DKIM signing requested but Mail::DKIM could not be loaded. Please check that Mail::DKIM is installed.';
176             }
177 0         0 $self->log_output( 'DKIM signing key loaded' );
178 0         0 return $self->{dkim_key};
179             }
180             }
181              
182              
183             sub run {
184 4     4 0 27 my ( $self ) = @_;
185              
186             GetOptions (
187             'verbose+' => \$self->{verbose},
188             'delay=i' => \$self->{send_delay},
189             'batch=i' => \$self->{batch_size},
190             'timeout=i' => \$self->{alarm_at},
191             'syslog+' => \$self->{syslog},
192 4         39 );
193              
194 4 50       1817 openlog( 'dmarc_send_reports', 'pid', LOG_MAIL ) if $self->{syslog};
195 4         23 $self->log_output( 'dmarc_send_reports starting up' );
196              
197 4         21 $|++;
198 4         49 my $report = Mail::DMARC::Report->new();
199 4         14 $self->{report} = $report;
200 4 50       34 $report->verbose($self->{verbose}) if defined $self->{verbose};
201             # If we have defined a custom transports generation class then
202             # load and instantiate it here.
203 4 50       18 if ( $report->config->{smtp}->{transports} ) {
204 4         3559 load $report->config->{smtp}->{transports};
205 4         413 my $package = $report->config->{smtp}->{transports};
206 4         45 my $transports_object = $package->new();
207 4         17 $self->set_transports_object( $transports_object );
208             }
209              
210 4     0   115 local $SIG{'ALRM'} = sub{ die "timeout\n" };
  0         0  
211              
212 4         15 my $batch_do = 1;
213              
214             # 1. get reports, one at a time
215             REPORT:
216 4         32 while ( my $aggregate = $report->store->next_todo() ) {
217 4         8 eval {
218 4         19 $self->send_report( $aggregate, $report );
219             };
220 4 50       24 if ( my $error = $@ ) {
221 0         0 $self->log_output( 'error sending report: ' . $error );
222             }
223              
224 4 50       199 if ( $batch_do++ > $self->{batch_size} ) {
225 0         0 $batch_do = 1;
226 0 0       0 if ( $self->{send_delay} > 0 ) {
227 0 0       0 print "sleeping ".$self->{send_delay} if $self->{verbose};
228 0 0       0 foreach ( 1 .. $self->{send_delay} ) { print '.' if $self->{verbose}; sleep 1; };
  0         0  
  0         0  
229 0 0       0 print "done.\n" if $self->{verbose};
230             }
231             }
232              
233             }
234              
235 4         30 alarm(0);
236              
237 4         29 $self->log_output( 'dmarc_send_reports done' );
238 4 50       15 closelog() if $self->{syslog};
239              
240 4         128 return;
241             }
242              
243             # PODNAME: dmarc_send_reports
244             # ABSTRACT: send aggregate reports
245              
246             sub send_report {
247              
248 4     4 0 11 my ( $self, $aggregate, $report ) = @_;
249              
250 4         46 alarm($self->{alarm_at});
251              
252 4         25 $self->log_output({
253             'id' => $aggregate->metadata->report_id,
254             'domain' => $aggregate->policy_published->domain,
255             'rua' => $aggregate->policy_published->rua,
256             });
257              
258             # Generate the list of report receivers
259 4         11 my $report_receivers = eval{ $report->uri->parse( $aggregate->policy_published->rua ) };
  4         18  
260 4 50       16 if ( my $error = $@ ) {
261 0         0 $self->log_output({
262             'id' => $aggregate->metadata->report_id,
263             'error' => 'No valid ruas found - deleting report - ' . $error,
264             });
265 0         0 $report->store->delete_report($aggregate->metadata->report_id);
266 0         0 alarm(0);
267 0         0 return;
268             }
269              
270             # Check we have some receivers
271 4 50       14 if ( scalar @$report_receivers == 0 ) {
272 0         0 $self->log_output({
273             'id' => $aggregate->metadata->report_id,
274             'error' => 'No valid ruas found - deleting report',
275             });
276 0         0 $report->store->delete_report($aggregate->metadata->report_id);
277 0         0 alarm(0);
278 0         0 return;
279             }
280              
281             # Generate the XML data and associated metadata
282 4         22 my $xml = $aggregate->as_xml();
283 4         24 my $xml_compressed = $report->compress(\$xml);
284 4         44 my $xml_compressed_bytes = length Encode::encode_utf8($xml_compressed);
285              
286 4         69 my $sent = 0;
287 4         7 my $cc_sent = 0;
288 4         9 my @too_big;
289             URI:
290 4         13 foreach my $receiver (@$report_receivers) {
291 4         9 my $method = $receiver->{uri};
292 4         12 my $max = $receiver->{max_bytes};
293              
294 4 50 33     32 if ( $max && $xml_compressed_bytes > $max ) {
    50          
    0          
    0          
295 0         0 $self->log_output({
296             'id' => $aggregate->metadata->report_id,
297             'info' => "skipping $method: report size ($xml_compressed_bytes) larger than $max",
298             });
299 0         0 push @too_big, $method;
300 0         0 next URI;
301             }
302             elsif ( 'mailto:' eq substr( $method, 0, 7 ) ) {
303 4         35 my ($to) = ( split /:/, $method )[-1];
304 4         39 my $cc = $report->config->{smtp}{cc};
305 4 50 33     28 if ( $cc && $cc ne 'set.this@for.a.while.example.com' && ! $cc_sent ) {
      33        
306 0         0 $self->email({ to => $cc, compressed => $xml_compressed, aggregate => \$aggregate });
307 0         0 $cc_sent = 1;
308             };
309 4 100       39 $self->email({ to => $to, compressed => $xml_compressed, aggregate => \$aggregate }) and $sent++;
310             }
311             # http(s) sending not yet enabled in module, skip this send and
312             # increment sent to avoid looping
313             elsif ( 'http:' eq substr( $method, 0, 5 ) ) {
314             #$report->sendit->http->post( $method, \$aggregate, $shrunk );
315 0         0 $sent++;
316             }
317             elsif ( 'https:' eq substr( $method, 0, 6 ) ) {
318             #$report->sendit->http->post( $method, \$aggregate, $shrunk );
319 0         0 $sent++;
320             }
321             }
322              
323 4 100       16 if ( $sent ) {
324 3         25 $report->store->delete_report($aggregate->metadata->report_id);
325             }
326             else {
327 1         10 $self->send_too_big_email(\@too_big, $xml_compressed_bytes, $aggregate);
328             };
329              
330 4         51 alarm(0);
331 4         47 return;
332             }
333              
334             sub send_too_big_email {
335 1     1 0 6 my ($self, $too_big, $bytes, $aggregate) = @_;
336 1         4 my $report = $self->{report};
337              
338             BIGURI:
339 1         5 foreach my $uri (@$too_big) {
340 0 0       0 next BIGURI if 'mailto:' ne substr( $uri, 0, 7 );
341 0         0 my ($to) = ( split /:/, $uri )[-1];
342 0         0 my $body = $report->sendit->too_big_report(
343             { uri => $uri,
344             report_bytes => $bytes,
345             report_id => $aggregate->metadata->report_id,
346             report_domain=> $aggregate->policy_published->domain,
347             }
348             );
349 0         0 my $mime_object = $report->sendit->smtp->assemble_too_big_message_object($aggregate, $to, $body);
350 0         0 $self->email({ to => $to, mime => $mime_object });
351             };
352 1         4 return;
353             };
354              
355             sub email {
356 4     4 0 12 my ($self, $args) = @_;
357              
358 4         12 my $to = $args->{to};
359 4 50       12 if ( !$to ) {
360 0         0 $self->log_output({ 'error' => 'No recipient for email' });
361 0         0 croak 'No recipient for email';
362             }
363 4   50     16 my $mime = $args->{mime} // undef;
364 4   50     12 my $compressed = $args->{compressed} // undef;
365 4   50     13 my $agg_ref = $args->{aggregate} // undef;
366 4         8 my $report = $self->{report};
367              
368 4         4 my $rid;
369 4 50       20 $rid = $$agg_ref->metadata->report_id if $agg_ref;
370              
371 4         14 my $log_data = {
372             deliver_to => $to,
373             };
374              
375 4         6 my $body;
376 4 50       11 if ( $rid ) {
    0          
377 4         17 my $mime_object = $report->sendit->smtp->assemble_message_object($agg_ref, $to, $compressed);
378 4         15 $body = $mime_object->as_string;
379 4         411 $log_data->{id} = $rid;
380 4         20 $log_data->{to_domain} = $$agg_ref->policy_published->domain;
381             }
382             elsif ( $mime ) {
383 0         0 $body = $mime->as_string;
384             }
385             else {
386 0         0 croak 'No email content';
387             }
388              
389 4         19 my $dkim_key = $self->get_dkim_key();
390 4 50       14 if ( $dkim_key ) {
391 0         0 my $dkim_algorithm = $report->config->{report_sign}{algorithm};
392 0         0 my $dkim_method = $report->config->{report_sign}{method};
393 0         0 my $dkim_domain = $report->config->{report_sign}{domain};
394 0         0 my $dkim_selector = $report->config->{report_sign}{selector};
395 0         0 eval {
396 0         0 my $dkim = Mail::DKIM::Signer->new(
397             Algorithm => $dkim_algorithm,
398             Method => $dkim_method,
399             Domain => $dkim_domain,
400             Selector => $dkim_selector,
401             Key => $dkim_key,
402             );
403 0         0 $body =~ s/\015?\012/\015\012/g;
404 0         0 $dkim->PRINT( $body );
405 0         0 $dkim->CLOSE;
406 0         0 my $signature = $dkim->signature;
407 0         0 $body = $signature->as_string . "\015\012" . $body;
408 0         0 $log_data->{dkim} = 1;
409             };
410 0 0       0 if ( my $error = $@ ) {
411 0 0       0 print "DKIM Signing error\n\t$error\n" if $self->{verbose};
412 0         0 $log_data->{error} = 'DKIM Signing error';
413 0         0 $log_data->{error_detail} = $error;
414 0         0 $self->log_output($log_data);
415 0         0 return;
416             }
417             }
418              
419 4         40 my @transports = $self->get_transports_for({
420             report => $report,
421             log_data => $log_data,
422             to => $to,
423             });
424 4         10 my $success;
425 4         15 while ( my $transport = shift @transports ) {
426 5         8 my $done = 0;
427 5         10 eval {
428             $success = sendmail(
429             $body,
430             {
431             from => $report->config->{organization}{email},
432 5         54 to => $to,
433             transport => $transport,
434             }
435             );
436 3 50       15866 if ( $success ) {
437 3         9 $log_data->{success} = $success->{message};
438 3         6 $done = 1;
439             }
440             };
441 5 100       23417 if ( my $error = $@ ) {
442 2 100       7635 next if scalar @transports;
443 1         3 my $code;
444             my $message;
445 1 50       5 if (ref $error eq 'Email::Sender::Failure') {
446 1         5 $code = $error->code;
447 1         3 $message = $error->message;
448             }
449             else {
450 0         0 $code = 'error';
451 0         0 $message = $error;
452 0         0 chomp $message;
453             }
454 1 50       4 $code = join( ', ', $log_data->{send_error_code}, $code ) if exists $log_data->{send_error_code};
455 1 50       4 $message = join( ', ', $log_data->{send_error}, $message ) if exists $log_data->{send_error};
456 1         3 $log_data->{send_error} = $message;
457 1         3 $log_data->{send_error_code} = $code;
458 1 50 33     5 if ( $error->code && $error->code =~ /^5/ ) {
459             # Perma error
460 0         0 $log_data->{deleted} = 1;
461 0         0 $report->store->delete_report($rid);
462 0         0 $success = 0;
463 0         0 last;
464             }
465 1         5 $report->store->error($rid, $error->message);
466             }
467 4 100       160 last if $done;
468             }
469              
470 4         22 $self->log_output( $log_data );
471              
472 4 100       14 if ( $success ) {
473 3         34 return 1;
474             }
475 1         15 return 0;
476             }
477              
478             sub log_output {
479 16     16 0 46 my ( $self, $args ) = @_;
480              
481 16         32 my $log_level = LOG_INFO;
482 16         34 my $log_entry = '';
483              
484 16 100       59 if ( ref $args eq 'HASH' ) {
485              
486 8 50       29 if ( $args->{'log_level'} ) {
487 0         0 $log_level = $args->{'log_level'};
488 0         0 delete $args->{'log_level'};
489             }
490              
491 8         28 my @parts;
492 8         67 foreach my $key ( sort keys %$args ) {
493 29   100     85 my $value = $args->{ $key } // '';
494 29         65 $value =~ s/,/#044/g; # Encode commas
495 29         78 push @parts, join( '=', $key, $value );
496             }
497 8         45 $log_entry = join( ', ', @parts );
498             }
499             else {
500 8         21 $log_entry = $args;
501             }
502              
503 16 50       78 syslog( $log_level, $log_entry ) if $self->{syslog};
504 16 50       49 print "$log_entry\n" if $self->{verbose};
505              
506 16         34 return;
507             }
508              
509             1;