summaryrefslogtreecommitdiffstats
path: root/ses
diff options
context:
space:
mode:
authorbyron jones <byron@glob.com.au>2018-02-27 23:45:15 +0100
committerDylan William Hardison <dylan@hardison.net>2018-02-27 23:45:15 +0100
commit2ac3574928f3bf8b68e881f49f854b61aa023d63 (patch)
tree2fb01fb9a8bf951d215a9b7e20fd609ced75ee31 /ses
parent7e047746fc38dee9e9330d3da81e87585aac92e6 (diff)
downloadbugzilla-2ac3574928f3bf8b68e881f49f854b61aa023d63.tar.gz
bugzilla-2ac3574928f3bf8b68e881f49f854b61aa023d63.tar.xz
Bug 1438206 - Process SES email bounces properly
Diffstat (limited to 'ses')
-rwxr-xr-xses/index.cgi206
1 files changed, 206 insertions, 0 deletions
diff --git a/ses/index.cgi b/ses/index.cgi
new file mode 100755
index 000000000..aa5b34704
--- /dev/null
+++ b/ses/index.cgi
@@ -0,0 +1,206 @@
+#!/usr/bin/perl
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use lib qw(.. ../lib ../local/lib/perl5);
+
+use Bugzilla ();
+use Bugzilla::Constants qw( ERROR_MODE_DIE );
+use Bugzilla::Mailer qw( MessageToMTA );
+use Bugzilla::User ();
+use Bugzilla::Util qw( html_quote remote_ip );
+use JSON::XS qw( decode_json encode_json );
+use LWP::UserAgent ();
+use Try::Tiny qw( try catch );
+
+Bugzilla->error_mode(ERROR_MODE_DIE);
+try {
+ main();
+} catch {
+ warn "SES: Fatal error: $_\n";
+ respond(500 => 'Internal Server Error');
+};
+
+sub main {
+ my $message = decode_json_wrapper(Bugzilla->cgi->param('POSTDATA')) // return;
+ my $message_type = $ENV{HTTP_X_AMZ_SNS_MESSAGE_TYPE} // '(missing)';
+
+ if ($message_type eq 'SubscriptionConfirmation') {
+ confirm_subscription($message);
+ }
+
+ elsif ($message_type eq 'Notification') {
+ my $notification = decode_json_wrapper($message->{Message}) // return;
+
+ my $notification_type = $notification->{notificationType} // '';
+ if ($notification_type eq 'Bounce') {
+ process_bounce($notification);
+ }
+ elsif ($notification_type eq 'Complaint') {
+ process_complaint($notification);
+ }
+ else {
+ warn "SES: Unsupported notification-type: $notification_type\n";
+ respond(200 => 'OK');
+ }
+ }
+
+ else {
+ warn "SES: Unsupported message-type: $message_type\n";
+ respond(200 => 'OK');
+ }
+}
+
+sub confirm_subscription {
+ my ($message) = @_;
+
+ my $subscribe_url = $message->{SubscribeURL};
+ if (!$subscribe_url) {
+ warn "SES: Bad SubscriptionConfirmation request: missing SubscribeURL\n";
+ respond(400 => 'Bad Request');
+ return;
+ }
+
+ my $ua = ua();
+ my $res = $ua->get($message->{SubscribeURL});
+ if (!$res->is_success) {
+ warn "SES: Bad response from SubscribeURL: " . $res->status_line . "\n";
+ respond(400 => 'Bad Request');
+ return;
+ }
+
+ respond(200 => 'OK');
+}
+
+sub process_bounce {
+ my ($notification) = @_;
+ my $type = $notification->{bounce}->{bounceType};
+
+ # these should be infrequent and hopefully small
+ warn("SES: notification: " . encode_json($notification));
+
+ if ($type eq 'Transient') {
+ # just log transient bounces
+ foreach my $recipient (@{ $notification->{bounce}->{bouncedRecipients} }) {
+ my $address = $recipient->{emailAddress};
+ Bugzilla->audit("SES: transient bounce for <$address>");
+ }
+ }
+
+ elsif ($type eq 'Permanent') {
+ # disable each account that is permanently bouncing
+ foreach my $recipient (@{ $notification->{bounce}->{bouncedRecipients} }) {
+ my $address = $recipient->{emailAddress};
+ my $reason = sprintf('(%s) %s', $recipient->{action} // 'error',
+ $recipient->{diagnosticCode} // 'unknown');
+
+ my $user = Bugzilla::User->new({ name => $address, cache => 1 });
+ if ($user) {
+ # never auto-disable admin accounts
+ if ($user->in_group('admin')) {
+ Bugzilla->audit("SES: ignoring permanent bounce for admin <$address>: $reason");
+ }
+
+ else {
+ my $template = Bugzilla->template_inner();
+ my $vars = {
+ mta => $notification->{bounce}->{reportingMTA} // 'unknown',
+ reason => $reason,
+ };
+ my $disable_text;
+ $template->process('admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text)
+ || die $template->error();
+
+ $user->set_disabledtext($disable_text);
+ $user->set_disable_mail(1);
+ $user->update();
+ Bugzilla->audit("SES: permanent bounce for <$address> disabled userid-" . $user->id . ": $reason");
+ }
+ }
+
+ else {
+ Bugzilla->audit("SES: permanent bounce for <$address> has no user: $reason");
+ }
+ }
+ }
+
+ else {
+ warn "SES: Unsupported bounce type: $type\n";
+ }
+
+ respond(200 => 'OK');
+}
+
+sub process_complaint {
+ # email notification to bugzilla admin
+ my ($notification) = @_;
+ my $template = Bugzilla->template_inner();
+ my $json = JSON::XS->new->pretty->utf8->canonical;
+
+ foreach my $recipient (@{ $notification->{complaint}->{complainedRecipients} }) {
+ my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown';
+ my $address = $recipient->{emailAddress};
+ Bugzilla->audit("SES: complaint for <$address> for '$reason'");
+ my $vars = {
+ email => $address,
+ user => Bugzilla::User->new({ name => $address, cache => 1 }),
+ reason => $reason,
+ notification => $json->encode($notification),
+ };
+ my $message;
+ $template->process('email/ses-complaint.txt.tmpl', $vars, \$message)
+ || die $template->error();
+ MessageToMTA($message);
+ }
+
+ respond(200 => 'OK');
+}
+
+sub respond {
+ my ($code, $message) = @_;
+ print Bugzilla->cgi->header(
+ -status => "$code $message",
+ );
+ # apache will generate non-200 response pages for us
+ say html_quote($message) if $code == 200;
+}
+
+sub decode_json_wrapper {
+ my ($json) = @_;
+ my $result;
+ if (!defined $json) {
+ warn 'SES: Missing JSON from ' . remote_ip() . "\n";
+ respond(400 => 'Bad Request');
+ return undef;
+ }
+ my $ok = try {
+ $result = decode_json($json);
+ }
+ catch {
+ warn 'SES: Malformed JSON from ' . remote_ip() . "\n";
+ respond(400 => 'Bad Request');
+ return undef;
+ };
+ return $ok ? $result : undef;
+}
+
+sub ua {
+ my $ua = LWP::UserAgent->new();
+ $ua->timeout(10);
+ $ua->protocols_allowed(['http', 'https']);
+ if (my $proxy_url = Bugzilla->params->{'proxy_url'}) {
+ $ua->proxy(['http', 'https'], $proxy_url);
+ }
+ else {
+ $ua->env_proxy;
+ }
+ return $ua;
+}