aboutsummaryrefslogtreecommitdiffstats
path: root/script/handle-mail-replies.rb
diff options
context:
space:
mode:
authorRobin Houston <robin.houston@gmail.com>2012-06-03 14:20:23 +0100
committerRobin Houston <robin.houston@gmail.com>2012-06-03 14:20:23 +0100
commit775e122cb4824c9734f50db0dd2967779636080e (patch)
tree05861e51eb8da822e561e6e02960f0761fd97664 /script/handle-mail-replies.rb
parentb357d29826a67ad527c63d823290226ae2d0c871 (diff)
Make handle-mail-replies bundler-aware too
Diffstat (limited to 'script/handle-mail-replies.rb')
-rwxr-xr-xscript/handle-mail-replies.rb180
1 files changed, 180 insertions, 0 deletions
diff --git a/script/handle-mail-replies.rb b/script/handle-mail-replies.rb
new file mode 100755
index 000000000..7590f5848
--- /dev/null
+++ b/script/handle-mail-replies.rb
@@ -0,0 +1,180 @@
+#!/usr/bin/env ruby
+# -*- coding: utf-8 -*-
+
+# Handle email responses sent to us.
+#
+# This script is invoked as a pipe command, i.e. with the raw email message on stdin.
+# - If a message is identified as a permanent bounce, the user is marked as having a
+# bounced address, and will not be sent any more messages.
+# - If a message is identified as an out-of-office autoreply, it is discarded.
+# - Any other messages are forwarded to config.get("FORWARD_NONBOUNCE_RESPONSES_TO")
+
+
+# We want to avoid loading rails unless we need it, so we start by just loading the
+# config file ourselves.
+$alaveteli_dir = File.join(File.dirname(__FILE__), '..')
+$:.push(File.join($alaveteli_dir, "commonlib", "rblib"))
+load "config.rb"
+MySociety::Config.set_file(File.join($alaveteli_dir, 'config', 'general'), true)
+MySociety::Config.load_default
+
+require 'action_mailer'
+
+def main(in_test_mode)
+ Dir.chdir($alaveteli_dir) do
+ raw_message = $stdin.read
+ begin
+ message = TMail::Mail.parse(raw_message)
+ rescue
+ # Error parsing message. Just pass it on, to be on the safe side.
+ forward_on(raw_message) unless in_test_mode
+ return 0
+ end
+
+ pfas = permanently_failed_addresses(message)
+ if !pfas.empty?
+ if in_test_mode
+ puts pfas
+ else
+ pfas.each do |pfa|
+ record_bounce(pfa, raw_message)
+ end
+ end
+ return 1
+ end
+
+ # If we are still here, there are no permanent failures,
+ # so if the message is a multipart/report then it must be
+ # reporting a temporary failure. In this case we discard it
+ if message.content_type == "multipart/report"
+ return 1
+ end
+
+ # Another style of temporary failure message
+ subject = message.header_string("Subject")
+ if message.content_type == "multipart/mixed" && subject == "Delivery Status Notification (Delay)"
+ return 1
+ end
+
+ # Discard out-of-office messages
+ if is_oof?(message)
+ return 2 # Use a different return code, to distinguish OOFs from bounces
+ end
+
+ # Otherwise forward the message on
+ forward_on(raw_message) unless in_test_mode
+ return 0
+ end
+end
+
+def permanently_failed_addresses(message)
+ if message.header_string("Return-Path") == "<>"
+ # Some sort of auto-response
+
+ # Check for Exim’s X-Failed-Recipients header
+ failed_recipients = message.header_string("X-Failed-Recipients")
+ if !failed_recipients.nil?
+ # The X-Failed-Recipients header contains the email address that failed
+ # Check for the words "This is a permanent error." in the body, to indicate
+ # a permanent failure
+ if message.body =~ /This is a permanent error./
+ return failed_recipients.split(/,\s*/)
+ end
+ end
+
+ # Next, look for multipart/report
+ if message.content_type == "multipart/report"
+ permanently_failed_recipients = []
+ message.parts.each do |part|
+ if part.content_type == "message/delivery-status"
+ sections = part.body.split(/\r?\n\r?\n/)
+ # The first section is a generic header; subsequent sections
+ # represent a particular recipient. Since we
+ sections[1..-1].each do |section|
+ if section !~ /^Status: (\d)/ || $1 != '5'
+ # Either we couldn’t find the Status field, or it was a transient failure
+ break
+ end
+ if section =~ /^Final-Recipient: rfc822;(.+)/
+ permanently_failed_recipients.push($1)
+ end
+ end
+ end
+ end
+ if !permanently_failed_recipients.empty?
+ return permanently_failed_recipients
+ end
+ end
+ end
+
+ subject = message.header_string("Subject")
+ # Then look for the style we’ve seen in WebShield bounces
+ # (These do not have a return path of <> in the cases I have seen.)
+ if subject == "Returned Mail: Error During Delivery"
+ if message.body =~ /^\s*---- Failed Recipients ----\s*((?:<[^>]+>\n)+)/
+ return $1.scan(/<([^>]+)>/).flatten
+ end
+ end
+
+ return []
+end
+
+def is_oof?(message)
+ # Check for out-of-office
+
+ if message.header_string("X-POST-MessageClass") == "9; Autoresponder"
+ return true
+ end
+
+ subject = message.header_string("Subject").downcase
+ if message.header_string("Return-Path") == "<>"
+ if subject.start_with? "out of office: "
+ return true
+ end
+ if subject.start_with? "automatic reply: "
+ return true
+ end
+ end
+
+ if message.header_string("Auto-Submitted") == "auto-generated"
+ if subject =~ /out of( the)? office/
+ return true
+ end
+ end
+
+ if subject.start_with? "out of office autoreply:"
+ return true
+ end
+ if subject == "out of office"
+ return true
+ end
+ if subject == "out of office reply"
+ return true
+ end
+ if subject.end_with? "is out of the office"
+ return true
+ end
+ return false
+end
+
+def forward_on(raw_message)
+ forward_non_bounces_to = MySociety::Config.get("FORWARD_NONBOUNCE_RESPONSES_TO", "user-support@localhost")
+ IO.popen("/usr/sbin/sendmail -i #{forward_non_bounces_to}", "w") do |f|
+ f.write(raw_message);
+ f.close;
+ end
+end
+
+def load_rails
+ require File.join('config', 'boot')
+ require Rails.root + '/config/environment'
+end
+
+def record_bounce(email_address, bounce_message)
+ load_rails
+ User.record_bounce_for_email(email_address, bounce_message)
+end
+
+in_test_mode = (ARGV[0] == "--test")
+status = main(in_test_mode)
+exit(status) if in_test_mode