aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/handlemail193
m---------commonlib0
-rw-r--r--perllib/FixMyStreet/Email.pm35
-rw-r--r--perllib/FixMyStreet/EmailSend.pm9
-rw-r--r--perllib/FixMyStreet/EmailSend/ContactEmail.pm9
-rw-r--r--perllib/FixMyStreet/EmailSend/DoNotReply.pm9
-rw-r--r--perllib/FixMyStreet/EmailSend/Variable.pm17
-rw-r--r--perllib/FixMyStreet/Script/Alerts.pm7
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm15
-rw-r--r--t/email.t20
10 files changed, 247 insertions, 67 deletions
diff --git a/bin/handlemail b/bin/handlemail
index 597d08a5d..e91a8a3a0 100755
--- a/bin/handlemail
+++ b/bin/handlemail
@@ -3,15 +3,10 @@
# handlemail:
# Handle an individual incoming mail message.
#
-# This script should be invoked through the .forward mechanism. It processes
-# replies to non-reply emails and auto-replies accordingly. Could deal with
-# bounces at some point too.
+# This script should be invoked through the .forward mechanism. It
+# processes bounce messages and replies and deals with them accordingly.
#
-# Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved.
-# Email: matthew@mysociety.org; WWW: http://www.mysociety.org/
-#
-
-my $rcsid = ''; $rcsid .= '$Id: handlemail,v 1.2 2009-02-11 11:04:48 matthew Exp $';
+# Copyright (c) 2016 UK Citizens Online Democracy. All rights reserved.
use strict;
use warnings;
@@ -25,45 +20,175 @@ BEGIN {
}
use FixMyStreet;
+use FixMyStreet::DB;
+use FixMyStreet::Email;
use mySociety::Email;
use mySociety::EmailUtil;
use mySociety::HandleMail;
-use mySociety::SystemMisc;
+use mySociety::SystemMisc qw(print_log);
# Don't print diagnostics to standard error, as this can result in bounce
# messages being generated (only in response to non-bounce input, obviously).
mySociety::SystemMisc::log_to_stderr(0);
my %data = mySociety::HandleMail::get_message();
+my @lines = @{$data{lines}};
+my $token = get_envelope_token();
+my $verp = $token !~ /DO-NOT-REPLY/i;
+my ($type, $object) = get_object_from_token();
if ($data{is_bounce_message}) {
- #my $a = mySociety::HandleMail::get_bounce_recipient($data{message});
- #my $token = mySociety::HandleMail::get_token($a,
- # 'fms-', FixMyStreet->config('EMAILDOMAIN')
- #);
- #exit(0) if $token eq 'DO-NOT-REPLY'; # A bounce we don't care about
- exit(0); # drop all other bounces currently
+ if ($object) {
+ handle_bounce_to_verp_address();
+ } else {
+ print_log('info', "bounce received for don't-care email");
+ }
+} else {
+ # This is not a bounce message. If it's to a VERP address, pass it on to
+ # the message sender; otherwise send an auto-reply
+ if ($object) {
+ handle_non_bounce_to_verp_address();
+ } else {
+ handle_non_bounce_to_null_address();
+ }
}
-# Not a bounce, send an automatic response
-my $template = 'reply-autoresponse';
-my $fp = FixMyStreet->path_to("templates", "email", "default", $template)->open or exit 75;
-$template = join('', <$fp>);
-$fp->close;
-
-# We generate this as a bounce.
-my $mail = mySociety::Email::construct_email({
- From => [ FixMyStreet->config('CONTACT_EMAIL'), 'FixMyStreet' ],
- To => $data{return_path},
- _template_ => $template,
- _parameters_ => { },
- _line_indent => '',
-});
-
-if (mySociety::EmailUtil::EMAIL_SUCCESS
- != mySociety::EmailUtil::send_email($mail, '<>', $data{return_path})) {
- exit(75);
+exit(0);
+
+# ---
+
+sub get_envelope_token {
+ my $m = $data{message};
+
+ # If we have a special suffix header for the local part suffix, use that.
+ # This is set by our exim so we have access to it through the domain name
+ # forwarding and routers.
+ my $suffix = $m->head()->get("X-Delivered-Suffix");
+ if ($suffix) {
+ chomp $suffix;
+ return substr($suffix, 1);
+ }
+
+ # Otherwise, fall back to To header
+ my $a = mySociety::HandleMail::get_bounce_recipient($m);
+
+ my $token = mySociety::HandleMail::get_token($a,
+ 'fms-', FixMyStreet->config('EMAIL_DOMAIN')
+ );
+ exit 0 unless $token; # Don't care unless we have a token
+
+ return $token;
}
-exit(0);
+sub get_object_from_token {
+ return unless $verp;
+
+ my ($type, $id) = FixMyStreet::Email::check_verp_token($token);
+ exit 0 unless $type;
+
+ my $rs;
+ if ($type eq 'report') {
+ $rs = FixMyStreet::DB->resultset('Problem');
+ } elsif ($type eq 'alert') {
+ $rs = FixMyStreet::DB->resultset('Alert');
+ }
+ my $object = $rs->find({ id => $id });
+ exit(0) unless $object;
+
+ return ($type, $object);
+}
+
+sub handle_permanent_bounce {
+ if ($type eq 'alert') {
+ print_log('info', "Received bounce for alert " . $object->id . ", unsubscribing");
+ $object->disable();
+ } elsif ($type eq 'report') {
+ print_log('info', "Received bounce for report " . $object->id . ", forwarding to support");
+ forward_on_to(FixMyStreet->config('CONTACT_EMAIL'));
+ }
+}
+
+sub is_out_of_office {
+ my (%attributes) = @_;
+ return 1 if $attributes{problem} && $attributes{problem} == mySociety::HandleMail::ERR_OUT_OF_OFFICE;
+ my $subject = $data{message}->head()->get("Subject");
+ return 1 if $subject =~ /Auto(matic|mated)?[ -]?(reply|response|responder)|Thank you for (your email|contacting)|Thank_you_for_your_email|Out of Office|This office is closed until|^Re: (Problem Report|New updates)|^Auto: |^E-Mail Response$|^Message Received:|have received your email|Acknowledgement of your email/i;
+ return 0;
+}
+
+sub handle_bounce_to_verp_address {
+ my %attributes = mySociety::HandleMail::parse_bounce(\@lines);
+ my $info = '';
+ if ($attributes{is_dsn}) {
+ # If permanent failure, but not mailbox full
+ return handle_permanent_bounce() if $attributes{status} =~ /^5\./ && $attributes{status} ne '5.2.2';
+ $info = ", Status $attributes{status}";
+ } elsif ($attributes{problem}) {
+ my $err_type = mySociety::HandleMail::error_type($attributes{problem});
+ return handle_permanent_bounce() if $err_type == mySociety::HandleMail::ERR_TYPE_PERMANENT;
+ $info = ", Bounce type $attributes{problem}";
+ }
+
+ # Check if the Subject looks like an auto-reply rather than a delivery bounce.
+ # If so, treat as if it were a normal email
+ if (is_out_of_office(%attributes)) {
+ print_log('info', "Treating bounce for $type " . $object->id . " as auto-reply to sender");
+ handle_non_bounce_to_verp_address();
+ } elsif (!$info) {
+ print_log('info', "Unparsed bounce received for $type " . $object->id . ", forwarding to support");
+ forward_on_to(FixMyStreet->config('CONTACT_EMAIL'));
+ } else {
+ print_log('info', "Ignoring bounce received for $type " . $object->id . $info);
+ }
+}
+
+sub handle_non_bounce_to_verp_address {
+ if ($type eq 'alert' && !is_out_of_office()) {
+ print_log('info', "Received non-bounce for alert " . $object->id . ", forwarding to support");
+ forward_on_to(FixMyStreet->config('CONTACT_EMAIL'));
+ } elsif ($type eq 'report') {
+ print_log('info', "Received non-bounce for report " . $object->id . ", forwarding to report creator");
+ forward_on_to($object->user->email);
+ }
+}
+
+sub handle_non_bounce_to_null_address {
+ # Don't send a reply to out of office replies...
+ if (is_out_of_office()) {
+ print_log('info', "Received non-bounce auto-reply to null address, ignoring");
+ return;
+ }
+
+ # Send an automatic response
+ print_log('info', "Received non-bounce to null address, auto-replying");
+ my $template = 'reply-autoresponse';
+ my $fp = FixMyStreet->path_to("templates", "email", "default", $template)->open or exit 75;
+ $template = join('', <$fp>);
+ $fp->close;
+
+ # We generate this as a bounce.
+ my $mail = mySociety::Email::construct_email({
+ From => [ FixMyStreet->config('CONTACT_EMAIL'), 'FixMyStreet' ],
+ To => $data{return_path},
+ _template_ => $template,
+ _parameters_ => { },
+ _line_indent => '',
+ });
+ send_mail($mail, '<>', $data{return_path});
+}
+
+sub forward_on_to {
+ my $recipient = shift;
+ my $text = join("\n", @lines) . "\n";
+ my $sender = $data{return_path} || '<>';
+ send_mail($text, $sender, $recipient);
+}
+
+sub send_mail {
+ my ($text, $sender, $recipient) = @_;
+ if (mySociety::EmailUtil::EMAIL_SUCCESS
+ != mySociety::EmailUtil::send_email($text, $sender, $recipient)) {
+ exit(75);
+ }
+}
diff --git a/commonlib b/commonlib
-Subproject b8516ca3642716852e479f9c0889d267e339b26
+Subproject 4d1ca3580a9eebc56e2e4ef800fdb45a8e2e397
diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm
index e81067da1..1787c32da 100644
--- a/perllib/FixMyStreet/Email.pm
+++ b/perllib/FixMyStreet/Email.pm
@@ -2,11 +2,13 @@ package FixMyStreet::Email;
use Encode;
use Template;
+use Digest::HMAC_SHA1 qw(hmac_sha1_hex);
use mySociety::Email;
use mySociety::Locale;
use mySociety::Random qw(random_bytes);
use Utils::Email;
use FixMyStreet;
+use FixMyStreet::DB;
use FixMyStreet::EmailSend;
sub test_dmarc {
@@ -15,6 +17,33 @@ sub test_dmarc {
return Utils::Email::test_dmarc($email);
}
+sub hash_from_id {
+ my ($type, $id) = @_;
+ my $secret = FixMyStreet::DB->resultset('Secret')->get;
+ # Make sure the ID is stringified, a number is treated differently
+ return substr(hmac_sha1_hex("$type-$id", $secret), 0, 8);
+}
+
+sub generate_verp_token {
+ my ($type, $id) = @_;
+ my $hash = hash_from_id($type, $id);
+ return "$type-$id-$hash";
+}
+
+sub check_verp_token {
+ my ($token) = @_;
+ $token = lc($token);
+ $token =~ s#[./_]##g;
+
+ my ($type, $id, $hash) = $token =~ /(report|alert)-([a-z0-9]+)-([a-z0-9]+)/;
+ return unless $type;
+
+ $hash =~ tr/lo/10/;
+ return unless hash_from_id($type, $id) eq $hash;
+
+ return ($type, $id);
+}
+
sub is_abuser {
my ($schema, $to) = @_;
@@ -87,11 +116,7 @@ sub send_cron {
print $email;
return 1; # Failure
} else {
- my %model_args;
- if (!FixMyStreet->test_mode && $env_from eq FixMyStreet->config('CONTACT_EMAIL')) {
- $model_args{mailer} = 'FixMyStreet::EmailSend::ContactEmail';
- }
- my $result = FixMyStreet::EmailSend->new(\%model_args)->send($email);
+ my $result = FixMyStreet::EmailSend->new({ env_from => $env_from })->send($email);
return $result ? 0 : 1;
}
}
diff --git a/perllib/FixMyStreet/EmailSend.pm b/perllib/FixMyStreet/EmailSend.pm
index 1c6e2cf7a..09f434931 100644
--- a/perllib/FixMyStreet/EmailSend.pm
+++ b/perllib/FixMyStreet/EmailSend.pm
@@ -55,7 +55,7 @@ if ( FixMyStreet->test_mode ) {
push @$mailer_args, username => $username, password => $password
if $username && $password;
$args = {
- mailer => 'FixMyStreet::EmailSend::DoNotReply',
+ mailer => 'FixMyStreet::EmailSend::Variable',
mailer_args => $mailer_args,
};
} else {
@@ -67,5 +67,12 @@ sub new {
my ($cls, $hash) = @_;
$hash ||= {};
my %args = ( %$args, %$hash );
+
+ my $sender = delete($args{env_from});
+ if ($sender) {
+ $args{mailer_args} = [ @{$args{mailer_args}} ] if $args{mailer_args};
+ push @{$args{mailer_args}}, env_from => $sender;
+ }
+
return Email::Send->new(\%args);
}
diff --git a/perllib/FixMyStreet/EmailSend/ContactEmail.pm b/perllib/FixMyStreet/EmailSend/ContactEmail.pm
deleted file mode 100644
index 28bcc983b..000000000
--- a/perllib/FixMyStreet/EmailSend/ContactEmail.pm
+++ /dev/null
@@ -1,9 +0,0 @@
-package FixMyStreet::EmailSend::ContactEmail;
-use base Email::Send::SMTP;
-
-sub get_env_sender {
- my $sender = FixMyStreet->config('CONTACT_EMAIL');
- return $sender;
-}
-
-1;
diff --git a/perllib/FixMyStreet/EmailSend/DoNotReply.pm b/perllib/FixMyStreet/EmailSend/DoNotReply.pm
deleted file mode 100644
index d1368f00f..000000000
--- a/perllib/FixMyStreet/EmailSend/DoNotReply.pm
+++ /dev/null
@@ -1,9 +0,0 @@
-package FixMyStreet::EmailSend::DoNotReply;
-use base Email::Send::SMTP;
-
-sub get_env_sender {
- my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL');
- return $sender;
-}
-
-1;
diff --git a/perllib/FixMyStreet/EmailSend/Variable.pm b/perllib/FixMyStreet/EmailSend/Variable.pm
new file mode 100644
index 000000000..4ba56dd41
--- /dev/null
+++ b/perllib/FixMyStreet/EmailSend/Variable.pm
@@ -0,0 +1,17 @@
+package FixMyStreet::EmailSend::Variable;
+use base Email::Send::SMTP;
+use FixMyStreet;
+
+my $sender;
+
+sub send {
+ my ($class, $message, %args) = @_;
+ $sender = delete($args{env_from}) || FixMyStreet->config('DO_NOT_REPLY_EMAIL');
+ $class->SUPER::send($message, %args);
+}
+
+sub get_env_sender {
+ $sender;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm
index fea897a24..e799a5446 100644
--- a/perllib/FixMyStreet/Script/Alerts.pm
+++ b/perllib/FixMyStreet/Script/Alerts.pm
@@ -250,6 +250,11 @@ sub _send_aggregated_alert_email(%) {
my $template = FixMyStreet->get_email_template($cobrand->moniker, $data{lang}, "$data{template}.txt");
+ my $sender = sprintf('<fms-%s@%s>',
+ FixMyStreet::Email::generate_verp_token('alert', $data{alert_id}),
+ FixMyStreet->config('EMAIL_DOMAIN')
+ );
+
my $result = FixMyStreet::Email::send_cron(
$data{schema},
{
@@ -257,7 +262,7 @@ sub _send_aggregated_alert_email(%) {
_parameters_ => \%data,
To => $data{alert_email},
},
- undef,
+ $sender,
0,
$cobrand,
$data{lang}
diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm
index 4d2c8bb17..7e5c10469 100644
--- a/perllib/FixMyStreet/SendReport/Email.pm
+++ b/perllib/FixMyStreet/SendReport/Email.pm
@@ -98,18 +98,17 @@ sub send {
$params->{Bcc} = $self->bcc if @{$self->bcc};
+ my $sender = sprintf('<fms-%s@%s>',
+ FixMyStreet::Email::generate_verp_token('report', $row->id),
+ FixMyStreet->config('EMAIL_DOMAIN')
+ );
+
if (FixMyStreet::Email::test_dmarc($params->{From}[0])) {
$params->{'Reply-To'} = [ $params->{From} ];
- $params->{From} = [ FixMyStreet->config('CONTACT_EMAIL'), $params->{From}[1] ];
+ $params->{From} = [ $sender, $params->{From}[1] ];
}
- my $result = FixMyStreet::Email::send_cron(
- $row->result_source->schema,
- $params,
- FixMyStreet->config('CONTACT_EMAIL'),
- $nomail,
- $cobrand
- );
+ my $result = FixMyStreet::Email::send_cron($row->result_source->schema, $params, $sender, $nomail, $cobrand);
unless ($result) {
$self->success(1);
diff --git a/t/email.t b/t/email.t
new file mode 100644
index 000000000..40a650da5
--- /dev/null
+++ b/t/email.t
@@ -0,0 +1,20 @@
+use strict;
+use warnings;
+
+use Test::More;
+use FixMyStreet::Email;
+
+my $secret = FixMyStreet::DB->resultset('Secret')->update({
+ secret => 'abcdef123456' });
+
+my $hash = FixMyStreet::Email::hash_from_id("report", 123);
+is $hash, '8fb274c6', 'Hash generation okay';
+
+my $token = FixMyStreet::Email::generate_verp_token("report", 123);
+is $token, "report-123-8fb274c6", 'Token generation okay';
+
+my ($type, $id) = FixMyStreet::Email::check_verp_token($token);
+is $type, "report", 'Correct type from token';
+is $id, 123, 'Correct ID from token';
+
+done_testing();