diff options
| author | David Cabo <david@calibea.com> | 2011-10-13 00:29:51 +0200 | 
|---|---|---|
| committer | David Cabo <david@calibea.com> | 2011-10-13 00:29:51 +0200 | 
| commit | c8983b923e4dc7db9ba22156daaddd94d2b5ed4d (patch) | |
| tree | 7741c3655fe5e3cbc90dd20a4626ac7acc1bf6b0 /script/handle-mail-replies | |
| parent | a29b3aaf0ae77af49d38813b62dddcb6889c1ebe (diff) | |
| parent | e13127a8ebc8bf8379d92f778af5a2bb6931d80c (diff) | |
Merge branch 'release/0.4'0.4
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 | 
