diff options
| author | Seb Bacon <seb.bacon@gmail.com> | 2012-06-20 10:46:57 +0100 | 
|---|---|---|
| committer | Seb Bacon <seb.bacon@gmail.com> | 2012-06-20 10:46:57 +0100 | 
| commit | 6c4c822ef7a4491bf821326af779e5be9118c0a1 (patch) | |
| tree | 39cf3564b1b2fb6be26499eda2a41be7ba59ad65 /script/handle-mail-replies.rb | |
| parent | ea977a0b9e86bc99a84de8577fa4ce1d304ac489 (diff) | |
| parent | 08dac0261325cd757b7146f9626f3c7b48cc672c (diff) | |
Merge branch 'release/0.6'0.6
Conflicts:
	locale/bs/app.po
	locale/ca/app.po
	locale/cs/app.po
	locale/cy/app.po
	locale/de/app.po
	locale/en_IE/app.po
	locale/es/app.po
	locale/eu/app.po
	locale/fr/app.po
	locale/ga_IE/app.po
	locale/gl/app.po
	locale/hu_HU/app.po
	locale/id/app.po
	locale/pt_BR/app.po
	locale/sq/app.po
	locale/sr@latin/app.po
	spec/fixtures/locale/en/app.po
Diffstat (limited to 'script/handle-mail-replies.rb')
| -rwxr-xr-x | script/handle-mail-replies.rb | 180 | 
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..eba6f44cf --- /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.expand_path(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 File.join('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 | 
