aboutsummaryrefslogtreecommitdiffstats
path: root/script
diff options
context:
space:
mode:
Diffstat (limited to 'script')
-rwxr-xr-xscript/handle-mail-replies182
-rwxr-xr-xscript/handle-mail-replies.rb180
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