diff options
Diffstat (limited to 'script/handle-mail-replies')
-rwxr-xr-x | script/handle-mail-replies | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/script/handle-mail-replies b/script/handle-mail-replies new file mode 100755 index 000000000..9b1fb5b29 --- /dev/null +++ b/script/handle-mail-replies @@ -0,0 +1,149 @@ +#!/usr/bin/ruby + +# 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 + +$:.push(File.join($alaveteli_dir, "vendor", "rails", "actionmailer", "lib", "action_mailer", "vendor", "tmail-1.2.7")) +require 'tmail' + +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 is_oof? message + # Discard out-of-office messages + return 2 + 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 + + 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 subject.start_with? "out of office autoreply:" + return true + end + if subject == "out of office" + 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 |