#!/usr/bin/perl
our
$VERSION
=
'1.20160612'
;
my
$send_delay
= 5;
my
$batch_size
= 1;
my
$alarm_at
= 120;
my
$syslog
= 0;
GetOptions (
'verbose+'
=> \
my
$verbose
,
'delay=i'
=> \
$send_delay
,
'batch=i'
=> \
$batch_size
,
'timeout=i'
=> \
$alarm_at
,
'syslog+'
=> \
$syslog
,
);
openlog(
'dmarc_send_reports'
,
'pid'
, LOG_MAIL )
if
$syslog
;
syslog( LOG_INFO,
'dmarc_send_reports starting up'
)
if
$syslog
;
$|++;
my
$report
= Mail::DMARC::Report->new();
$report
->verbose(
$verbose
)
if
defined
$verbose
;
my
$dkim_key
;
if
(
$report
->config->{report_sign}->{keyfile} ) {
eval
{
};
if
( UNIVERSAL::can(
'Mail::DKIM::Signer'
,
"new"
) ) {
my
$file
=
$report
->config->{report_sign}->{keyfile};
$dkim_key
= Mail::DKIM::PrivateKey->load(
'File'
=>
$file
,
);
if
( !
$dkim_key
) {
die
"Could not load DKIM key $file"
;
}
}
else
{
die
'DKIM signing requested but Mail::DKIM could not be loaded. Please check that Mail::DKIM is installed.'
;
}
syslog( LOG_INFO,
'DKIM signing key loaded'
)
if
$syslog
;
}
local
$SIG
{
'ALRM'
} =
sub
{
die
"timeout\n"
};
my
$todo_reports
=
$report
->store->retrieve_todo();
my
$batch_do
= 1;
REPORT:
foreach
my
$aggregate
( @{
$todo_reports
} ) {
eval
{
send_single_report(
$aggregate
,
$report
);
};
if
(
my
$error
= $@ ) {
print
"Sending error $error\n"
if
$verbose
;
syslog( LOG_INFO,
'error sending report: '
.
$error
)
if
$syslog
;
}
if
(
$batch_do
++ >
$batch_size
) {
$batch_do
= 1;
if
(
$send_delay
> 0 ) {
print
"sleeping $send_delay"
if
$verbose
;
foreach
( 1 ..
$send_delay
) {
print
'.'
if
$verbose
;
sleep
1; };
print
"done.\n"
if
$verbose
;
}
}
}
alarm
(0);
syslog( LOG_INFO,
'dmarc_send_reports done'
)
if
$syslog
;
closelog()
if
$syslog
;
exit
;
sub
send_single_report {
my
(
$aggregate
,
$report
) =
@_
;
alarm
(
$alarm_at
);
print
'ID: '
.
$aggregate
->metadata->report_id .
"\n"
;
print
'Domain: '
.
$aggregate
->policy_published->domain .
"\n"
;
print
'rua: '
.
$aggregate
->policy_published->rua .
"\n"
;
log_to_syslog({
'id'
=>
$aggregate
->metadata->report_id,
'domain'
=>
$aggregate
->policy_published->domain,
'rua'
=>
$aggregate
->policy_published->rua,
});
my
$xml
=
$aggregate
->as_xml();
my
$shrunk
=
$report
->compress(\
$xml
);
my
$bytes
=
length
Encode::encode_utf8(
$shrunk
);
my
$uri_ref
;
eval
{
$uri_ref
=
$report
->uri->parse(
$aggregate
->policy_published->rua );
};
if
(
my
$error
= $@ ) {
print
"No valid ruas found, deleting report\n"
;
log_to_syslog({
'id'
=>
$aggregate
->metadata->report_id,
'error'
=>
'No valid ruas found - deleting reporti - '
.
$error
,
});
$report
->store->delete_report(
$aggregate
->metadata->report_id);
alarm
(0);
return
;
}
if
(
scalar
@{
$uri_ref
} == 0 ) {
print
"No valid ruas found, deleting report\n"
;
log_to_syslog({
'id'
=>
$aggregate
->metadata->report_id,
'error'
=>
'No valid ruas found - deleting report'
,
});
$report
->store->delete_report(
$aggregate
->metadata->report_id);
alarm
(0);
return
;
}
my
$sent
= 0;
my
$cc_sent
= 0;
my
@too_big
;
URI:
foreach
my
$u_ref
(
@$uri_ref
) {
my
$method
=
$u_ref
->{uri};
my
$max
=
$u_ref
->{max_bytes};
if
(
$max
&&
$bytes
>
$max
) {
log_to_syslog({
'id'
=>
$aggregate
->metadata->report_id,
"info' => 'skipping $method: report size ($bytes) larger than $max"
,
});
print
"skipping $method: report size ($bytes) larger than $max\n"
;
push
@too_big
,
$method
;
next
URI;
}
if
(
'mailto:'
eq
substr
(
$method
, 0, 7 ) ) {
my
(
$to
) = (
split
/:/,
$method
)[-1];
my
$cc
=
$report
->config->{smtp}{cc};
if
(
$cc
&&
$cc
ne
'set.this@for.a.while.example.com'
&& !
$cc_sent
) {
email(
$cc
,
$shrunk
, \
$aggregate
);
$cc_sent
= 1;
};
email(
$to
,
$shrunk
, \
$aggregate
) and
$sent
++;
}
if
(
'http:'
eq
substr
(
$method
, 0, 5 ) ) {
$report
->sendit->http->post(
$method
, \
$aggregate
,
$shrunk
);
$sent
++;
}
}
if
(
$sent
) {
$report
->store->delete_report(
$aggregate
->metadata->report_id);
}
else
{
send_too_big_email(\
@too_big
,
$bytes
,
$aggregate
);
};
alarm
(0);
}
sub
send_too_big_email {
my
(
$too_big
,
$bytes
,
$aggregate
) =
@_
;
BIGURI:
foreach
my
$uri
(
@$too_big
) {
next
BIGURI
if
'mailto:'
ne
substr
(
$uri
, 0, 7 );
my
(
$to
) = (
split
/:/,
$uri
)[-1];
my
$body
=
$report
->sendit->too_big_report(
{
uri
=>
$uri
,
report_bytes
=>
$bytes
,
report_id
=>
$aggregate
->metadata->report_id,
report_domain
=>
$aggregate
->policy_published->domain,
}
);
email(
$to
,
$body
);
};
return
;
};
sub
get_smtp_connection {
my
(
$to
,
$shrunk
,
$agg_ref
) =
@_
;
my
$rid
;
$rid
=
$$agg_ref
->metadata->report_id
if
$agg_ref
;
my
$smtp
=
$report
->sendit->smtp->connect_smtp_tls(
$to
) or
do
{
warn
"\tSSL connection failed\n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'SSL connection failed'
,
});
if
(
$agg_ref
) {
$$agg_ref
->metadata->error(
"SSL connection failed"
);
my
$xml
=
$$agg_ref
->as_xml();
$shrunk
=
$report
->compress(\
$xml
);
};
};
if
( !
$smtp
) {
$smtp
=
$report
->sendit->smtp->connect_smtp(
$to
) or
do
{
warn
"\tSMTP connection failed\n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'SMTP connection failed'
,
});
if
(
$rid
) {
my
$errors
=
scalar
$$agg_ref
->metadata->error;
if
(
$errors
>= 12 ) {
print
"Report $rid deleted (too many errors)\n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'Report deleted, too many errors'
,
});
$report
->store->delete_report(
$rid
);
}
else
{
$report
->store->error(
$rid
,
"SSL connection for $to failed"
);
$report
->store->error(
$rid
,
"SMTP connection for $to failed"
);
log_to_syslog({
'id'
=>
$rid
,
'errorcount'
=>
$errors
,
});
};
};
return
;
};
};
if
( !
$smtp
) {
warn
"\t0 MX available\n"
;
log_to_syslog({
'id'
=>
$rid
,
'info'
=>
'0 MX available'
,
});
return
;
};
return
$smtp
;
};
sub
email {
my
(
$to
,
$shrunk
,
$agg_ref
) =
@_
;
my
$rid
;
$rid
=
$$agg_ref
->metadata->report_id
if
$agg_ref
;
my
$smtp
;
eval
{
$smtp
= get_smtp_connection(
$to
,
$shrunk
,
$agg_ref
) or
return
;
};
if
(
my
$error
= $@ ) {
print
"error "
.
$error
.
"\n"
;
log_to_syslog({
'id'
=>
$rid
,
'deliver_to'
=>
$to
,
'error'
=>
$error
,
});
$report
->store->error(
$rid
,
$error
);
return
;
}
return
if
!
$smtp
;
print
"delivering message to $to, via "
.
$smtp
->host.
"\n"
;
log_to_syslog({
'id'
=>
$rid
,
'deliver_to'
=>
$to
,
'smtp_via'
=>
$smtp
->host,
});
my
$from
=
$report
->config->{organization}{email};
$smtp
->mail(
$from
) or
do
{
my
$err
=
$smtp
->code .
" "
.
$smtp
->message;
print
"MAIL FROM $from rejected\n\t$err\n"
;
log_to_syslog({
'id'
=>
$rid
,
'mail_from'
=>
$from
,
'smtp_error'
=>
$err
,
});
$report
->store->error(
$rid
,
$err
)
if
$rid
;
$smtp
->quit;
return
;
};
$smtp
->recipient(
$to
) or
do
{
my
$err
=
$smtp
->code .
" "
.
$smtp
->message;
print
"RCPT TO $to rejected\n\t$err\n"
;
log_to_syslog({
'id'
=>
$rid
,
'rcpt_to'
=>
$to
,
'smtp_error'
=>
$err
,
});
if
(
$rid
) {
if
(
$smtp
->code =~ /^5/ ) {
print
"Report $rid deleted \n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'Report deleted'
,
'smtp_code'
=>
$smtp
->code,
});
$report
->store->delete_report(
$rid
);
}
else
{
$report
->store->error(
$rid
,
$err
);
};
};
$smtp
->quit;
return
;
};
my
$body
=
$shrunk
;
if
(
$rid
) {
$body
=
$report
->sendit->smtp->assemble_message(
$agg_ref
,
$to
,
$shrunk
);
};
if
(
$dkim_key
) {
my
$dkim_algorithm
=
$report
->config->{report_sign}{algorithm};
my
$dkim_method
=
$report
->config->{report_sign}{method};
my
$dkim_domain
=
$report
->config->{report_sign}{domain};
my
$dkim_selector
=
$report
->config->{report_sign}{selector};
eval
{
my
$dkim
= Mail::DKIM::Signer->new(
Algorithm
=>
$dkim_algorithm
,
Method
=>
$dkim_method
,
Domain
=>
$dkim_domain
,
Selector
=>
$dkim_selector
,
Key
=>
$dkim_key
,
);
$body
=~ s/\015?\012/\015\012/g;
$dkim
->PRINT(
$body
);
$dkim
->CLOSE;
my
$signature
=
$dkim
->signature;
$body
=
$signature
->as_string .
"\015\012"
.
$body
;
};
if
(
my
$error
= $@ ) {
print
"DKIM Signing error\n\t$error\n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'DKIM Signing error'
,
'error_detail'
=>
$error
,
});
$smtp
->quit;
return
;
}
}
$smtp
->data(
$body
) or
do
{
my
$err
=
$smtp
->code .
" "
.
$smtp
->message;
if
(
$agg_ref
) {
my
$to_domain
=
$$agg_ref
->policy_published->domain;
print
"DATA for domain $to_domain report rejected\n\t$err\n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'DATA rejected'
,
'to_domain'
=>
$to_domain
,
'error_detail'
=>
$err
,
});
}
else
{
print
"DATA for report rejected\n\t$err\n"
;
log_to_syslog({
'id'
=>
$rid
,
'error'
=>
'DATA rejected'
,
'error_detail'
=>
$err
,
});
};
$report
->store->error(
$rid
,
$err
)
if
$rid
;
return
;
};
my
@accepts
=
$smtp
->message;
my
$accept
=
$smtp
->code .
' '
.
pop
@accepts
;
$accept
=~ s/\r$//;
print
"SMTP accepted\n\t$accept\n"
;
log_to_syslog({
'id'
=>
$rid
,
'smtp_accept'
=>
$accept
,
});
$smtp
->quit;
return
1;
}
sub
log_to_syslog {
my
(
$args
) =
@_
;
return
if
!
$syslog
;
my
$log_level
= LOG_INFO;
if
(
$args
->{
'log_level'
} ) {
$log_level
=
$args
->{
'log_level'
};
delete
$args
->{
'log_level'
};
}
my
@parts
;
foreach
my
$key
(
sort
keys
%$args
) {
my
$value
=
$args
->{
$key
};
$value
=~ s/,/
push
@parts
,
join
(
'='
,
$key
,
$value
);
}
syslog(
$log_level
,
join
(
', '
,
@parts
) );
return
;
}