diff options
author | Robin Houston <robin.houston@gmail.com> | 2012-06-03 14:20:23 +0100 |
---|---|---|
committer | Robin Houston <robin.houston@gmail.com> | 2012-06-03 14:20:23 +0100 |
commit | 775e122cb4824c9734f50db0dd2967779636080e (patch) | |
tree | 05861e51eb8da822e561e6e02960f0761fd97664 | |
parent | b357d29826a67ad527c63d823290226ae2d0c871 (diff) |
Make handle-mail-replies bundler-aware too
-rwxr-xr-x | script/handle-mail-replies | 182 | ||||
-rwxr-xr-x | script/handle-mail-replies.rb | 180 |
2 files changed, 183 insertions, 179 deletions
diff --git a/script/handle-mail-replies b/script/handle-mail-replies index 7590f5848..ad4b3719e 100755 --- a/script/handle-mail-replies +++ b/script/handle-mail-replies @@ -1,180 +1,4 @@ -#!/usr/bin/env ruby -# -*- coding: utf-8 -*- +#!/bin/bash -# 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 +cd "`dirname "$0"`" +exec bundle exec ./handle-mail-replies.rb 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 |