diff options
-rw-r--r-- | app/models/track_mailer.rb | 8 | ||||
-rw-r--r-- | app/models/user.rb | 30 | ||||
-rw-r--r-- | config/alert-tracks-debian.ugly | 2 | ||||
-rw-r--r-- | config/general.yml-example | 7 | ||||
-rw-r--r-- | db/migrate/103_add_user_bounce_columns.rb | 15 | ||||
-rw-r--r-- | lib/external_command.rb | 135 | ||||
-rwxr-xr-x | script/handle-mail-replies | 128 | ||||
-rwxr-xr-x | script/mailin | 24 | ||||
-rw-r--r-- | spec/controllers/track_controller_spec.rb | 2 | ||||
-rw-r--r-- | spec/controllers/user_controller_spec.rb | 6 | ||||
-rw-r--r-- | spec/fixtures/files/track-response-exim-bounce.email | 67 | ||||
-rw-r--r-- | spec/fixtures/files/track-response-multipart-report.email | 113 | ||||
-rw-r--r-- | spec/fixtures/users.yml | 15 | ||||
-rwxr-xr-x | spec/lib/external_command_scripts/output.sh | 22 | ||||
-rw-r--r-- | spec/lib/external_command_spec.rb | 40 | ||||
-rw-r--r-- | spec/models/track_mailer_spec.rb | 33 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 23 | ||||
-rw-r--r-- | spec/script/handle-mail-replies_spec.rb | 33 |
18 files changed, 483 insertions, 220 deletions
diff --git a/app/models/track_mailer.rb b/app/models/track_mailer.rb index f8bef4d61..85b1fedd8 100644 --- a/app/models/track_mailer.rb +++ b/app/models/track_mailer.rb @@ -26,6 +26,12 @@ class TrackMailer < ApplicationMailer @body = { :user => user, :email_about_things => email_about_things, :unsubscribe_url => unsubscribe_url } end + def contact_from_name_and_email + contact_name = MySociety::Config.get("TRACK_SENDER_NAME", 'Alaveteli') + contact_email = MySociety::Config.get("TRACK_SENDER_EMAIL", 'contact@localhost') + return "#{contact_name} <#{contact_email}>" + end + # Send email alerts for tracked things. Never more than one email # a day, nor about events which are more than a week old, nor # events about which emails have been sent within the last two @@ -40,6 +46,8 @@ class TrackMailer < ApplicationMailer return false end for user in users + next if !user.should_be_emailed? + email_about_things = [] track_things = TrackThing.find(:all, :conditions => [ "tracking_user_id = ? and track_medium = ?", user.id, 'email_daily' ]) for track_thing in track_things diff --git a/app/models/user.rb b/app/models/user.rb index c3c3da6f7..e98d777b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -16,6 +16,8 @@ # admin_level :string(255) default("none"), not null # ban_text :text default(""), not null # about_me :text default(""), not null +# email_bounced_at :datetime +# email_bounce_message :text default(""), not null # # models/user.rb: @@ -350,15 +352,37 @@ class User < ActiveRecord::Base } end + def record_bounce(message) + self.email_bounced_at = Time.now + self.email_bounce_message = message + self.save! + end + + def should_be_emailed? + return (self.email_confirmed && self.email_bounced_at.nil?) + end + + ## Private instance methods private + def create_new_salt + self.salt = self.object_id.to_s + rand.to_s + end + + ## Class methods def User.encrypted_password(password, salt) string_to_hash = password + salt # XXX need to add a secret here too? Digest::SHA1.hexdigest(string_to_hash) end - - def create_new_salt - self.salt = self.object_id.to_s + rand.to_s + + def User.record_bounce_for_email(email, message) + user = User.find_user_by_email(email) + return false if user.nil? + + if user.email_bounced_at.nil? + user.record_bounce(message) + end + return true end end diff --git a/config/alert-tracks-debian.ugly b/config/alert-tracks-debian.ugly index 730a125b4..5bd146061 100644 --- a/config/alert-tracks-debian.ugly +++ b/config/alert-tracks-debian.ugly @@ -32,7 +32,7 @@ start_daemon() { } stop_daemon() { - /sbin/start-stop-daemon --stop --pidfile "$PIDFILE" + /sbin/start-stop-daemon --stop --oknodo --pidfile "$PIDFILE" } restart() { stop; start; } diff --git a/config/general.yml-example b/config/general.yml-example index ae50afc5a..dcaa0d648 100644 --- a/config/general.yml-example +++ b/config/general.yml-example @@ -64,6 +64,10 @@ ADMIN_PASSWORD: 'passwordx' CONTACT_EMAIL: 'postmaster@localhost' CONTACT_NAME: 'Alaveteli Webmaster' +# Email "from" details for track messages +TRACK_SENDER_EMAIL: 'postmaster@localhost' +TRACK_SENDER_NAME: 'Alaveteli Webmaster' + # Where the raw incoming email data gets stored; make sure you back # this up! RAW_EMAILS_LOCATION: 'files/raw_emails' @@ -115,3 +119,6 @@ USE_GHOSTSCRIPT_COMPRESSION: true # mySociety's gazeteer service. Shouldn't change. GAZE_URL: http://gaze.mysociety.org + +# The email address to which non-bounce responses should be forwarded +FORWARD_NONBOUNCE_RESPONSES_TO: user-support@localhost diff --git a/db/migrate/103_add_user_bounce_columns.rb b/db/migrate/103_add_user_bounce_columns.rb new file mode 100644 index 000000000..a16ecde75 --- /dev/null +++ b/db/migrate/103_add_user_bounce_columns.rb @@ -0,0 +1,15 @@ +require 'digest/sha1' + +class AddUserBounceColumns < ActiveRecord::Migration + def self.up + add_column :users, :email_bounced_at, :datetime + add_column :users, :email_bounce_message, :text, :default => "", :null => false + end + def self.down + remove_column :users, :email_bounced_at + remove_column :users, :email_bounce_message + end +end + + + diff --git a/lib/external_command.rb b/lib/external_command.rb deleted file mode 100644 index 96292854f..000000000 --- a/lib/external_command.rb +++ /dev/null @@ -1,135 +0,0 @@ -# Run an external command, capturing its stdout and stderr -# streams into variables. -# -# So it’s rather like the `backtick` built-in, except that: -# - The command is run as-is, rather than being parsed by the shell; -# - Standard error is also captured. -# -# After the run() method has been called, the instance variables -# out, err and status contain the contents of the process’s stdout, -# the contents of its stderr, and the exit status. -# -# Example usage: -# require 'external_command' -# xc = ExternalCommand("ls", "-l").run() -# puts "Ran ls -l with exit status #{xc.status}" -# puts "===STDOUT===\n#{xc.out}" -# puts "===STDERR===\n#{xc.err}" -# -# The out and err attributes are writeable. If you assign -# a string, after calling the constructor and before calling -# run(), then the subprocess output/error will be appended -# to this string. - -# <rant author="robin"> -# In any sane language, this would be implemented with a -# single child process. The parent process would block on -# select(), and when the child process terminated, the -# select call would be interrupted by a CHLD signal -# and return EINTR. Unfortunately Ruby goes out of its -# way to prevent this from working, automatically restarting -# the select call if EINTR is returned. Therefore we -# use a parent-child-grandchild arrangement, where the -# parent blocks on select() and the child blocks on -# waitpid(). When the child detects that the grandchild -# has finished, it writes to a pipe that’s included in -# the parent’s select() for this purpose. -# </rant> - -class ExternalCommand - attr_accessor :out, :err - attr_reader :status - - def initialize(cmd, *args) - @cmd = cmd - @args = args - - # Strings to collect stdout and stderr from the child process - # These may be replaced by the caller, to append to existing strings. - @out = "" - @err = "" - @fin = "" - end - - def run() - # Pipes for parent-child communication - @out_read, @out_write = IO::pipe - @err_read, @err_write = IO::pipe - @fin_read, @fin_write = IO::pipe - - @pid = fork do - # Here we’re in the child process. - child_process - end - - # Here we’re in the parent process. - parent_process - - return self - end - - private - - def child_process() - # Reopen stdout and stderr to point at the pipes - STDOUT.reopen(@out_write) - STDERR.reopen(@err_write) - - # Close all the filehandles other than the ones we intend to use. - ObjectSpace.each_object(IO) do |fh| - fh.close unless ( - [STDOUT, STDERR, @fin_write].include?(fh) || fh.closed?) - end - - Process::waitpid(fork { grandchild_process }) - @fin_write.puts($?.exitstatus.to_s) - - exit! 0 - end - - def grandchild_process() - exec(@cmd, *@args) - - # This is only reached if the exec fails - @err_write.print("Failed to exec: #{[@cmd, *@args].join(' ')}") - exit! 99 - end - - def parent_process() - # Close the writing ends of the pipes - @out_write.close - @err_write.close - @fin_write.close - - @fhs = {@out_read => @out, @err_read => @err, @fin_read => @fin} - - while @fin.empty? - ok = read_data - if !ok - raise "select() timed out even with a nil (infinite) timeout" - end - end - - while read_data(0) - # Pull out any data that’s left in the pipes - end - - Process::waitpid(@pid) - @status = @fin.to_i - @out_read.close - @err_read.close - end - - def read_data(timeout=nil) - ready_array = IO.select(@fhs.keys, [], [], timeout) - return false if ready_array.nil? - ready_array[0].each do |fh| - begin - @fhs[fh] << fh.readpartial(8192) - rescue EOFError - @fhs.delete fh - end - end - return true - end -end diff --git a/script/handle-mail-replies b/script/handle-mail-replies new file mode 100755 index 000000000..93cdc8cfd --- /dev/null +++ b/script/handle-mail-replies @@ -0,0 +1,128 @@ +#!/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 + +def main(in_test_mode) + Dir.chdir($alaveteli_dir) do + load_rails + + raw_message = $stdin.read + message = TMail::Mail.parse(raw_message) + + 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("Return-Path") == "<>" + subject = message.header_string("Subject") + if subject.start_with? "Out of Office: " + return true + end + if subject.start_with? "Automatic reply: " + return true + end + 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) + 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 diff --git a/script/mailin b/script/mailin index 733eaff3d..b4d77f7a4 100755 --- a/script/mailin +++ b/script/mailin @@ -2,18 +2,18 @@ # Wire this script to receive incoming email for request responses. -INPUT=/tmp/foi-mailin-mail-$RANDOM$RANDOM$RANDOM$RANDOM.txt -OUTPUT=/tmp/foi-mailin-output-$RANDOM$RANDOM$RANDOM$RANDOM.txt +INPUT=$(mktemp -t foi-mailin-mail-XXXXXXXX) +OUTPUT=$(mktemp -t foi-mailin-output-XXXXXXXX) -cat >$INPUT +# Read the email message from stdin, and write it to the file $INPUT +cat >"$INPUT" -cd `dirname $0` -cd ../ +cd "$(dirname "$0")"/.. source commonlib/shlib/deployfns read_conf config/general -(cat $INPUT | ./script/runner 'RequestMailer.receive(STDIN.read)') >$OUTPUT 2>&1 +script/runner 'RequestMailer.receive(STDIN.read)' <"$INPUT" >"$OUTPUT" 2>&1 ERROR_CODE=$? if [ ! "$ERROR_CODE" = "0" ] then @@ -21,17 +21,15 @@ then # send error to administators (use mutt for MIME encoding) SUBJ="Mail import error for $OPTION_DOMAIN" - BODY="There was an error code $ERROR_CODE returned by the RequestMailer.receive command in script/mailin. See attached for details. This might be quite serious, such as the database was down, or might be an email with corrupt headers that Rails is choking on. The email was returned with an exit code 75, which for Exim at least means the MTA will try again later. A well configured installation of this code will separately have had Exim make a backup copy of the email in a separate mailbox, just in case." - echo "$BODY" | /usr/bin/mutt -s "$SUBJ" -a $OUTPUT $INPUT -- $OPTION_CONTACT_EMAIL + BODY="There was an error code $ERROR_CODE returned by the RequestMailer.receive command in script/mailin. See attached for details. This might be quite serious, such as the database was down, or might be an email with corrupt headers that Rails is choking on. We returned the email with an exit code 75, which for Exim at least instructs the MTA to try again later. A well configured installation of this code will separately have had Exim make a backup copy of the email in a separate mailbox, just in case." + /usr/bin/mutt -s "$SUBJ" -a "$OUTPUT" "$INPUT" -- "$OPTION_CONTACT_EMAIL" <<<"$BODY" # tell exim error was temporary, so try again later (no point bouncing message to authority) - rm -f $OUTPUT - rm -f $INPUT + rm -f "$INPUT" "$OUTPUT" exit 75 fi -cat $OUTPUT -rm -f $OUTPUT -rm -f $INPUT +cat "$OUTPUT" +rm -f "$INPUT" "$OUTPUT" exit 0 diff --git a/spec/controllers/track_controller_spec.rb b/spec/controllers/track_controller_spec.rb index 8c2e19c58..435d9a0d3 100644 --- a/spec/controllers/track_controller_spec.rb +++ b/spec/controllers/track_controller_spec.rb @@ -36,7 +36,7 @@ end describe TrackController, "when sending alerts for a track" do integrate_views - fixtures :info_requests, :outgoing_messages, :incoming_messages, :raw_emails, :info_request_events, :users, :track_things, :track_things_sent_emails, :public_bodies, :public_body_translations + fixtures :comments, :info_requests, :outgoing_messages, :incoming_messages, :raw_emails, :info_request_events, :users, :track_things, :track_things_sent_emails, :public_bodies, :public_body_translations include LinkToHelper # for main_url before(:each) do diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb index 0ba542630..b4cc0d6e3 100644 --- a/spec/controllers/user_controller_spec.rb +++ b/spec/controllers/user_controller_spec.rb @@ -111,7 +111,7 @@ describe UserController, "when signing in" do get :signin, :r => "/list" response.should render_template('sign') post_redirect = get_last_postredirect - post :signin, { :user_signin => { :email => 'silly@localhost', :password => 'jonespassword' }, + post :signin, { :user_signin => { :email => 'unconfirmed@localhost', :password => 'jonespassword' }, :token => post_redirect.token } response.should render_template('confirm') @@ -123,7 +123,7 @@ describe UserController, "when signing in" do get :signin, :r => "/list" post_redirect = get_last_postredirect - post :signin, { :user_signin => { :email => 'silly@localhost', :password => 'jonespassword' }, + post :signin, { :user_signin => { :email => 'unconfirmed@localhost', :password => 'jonespassword' }, :token => post_redirect.token } response.should send_email @@ -143,7 +143,7 @@ describe UserController, "when signing in" do # check confirmation URL works session[:user_id].should be_nil get :confirm, :email_token => post_redirect.email_token - session[:user_id].should == users(:silly_name_user).id + session[:user_id].should == users(:unconfirmed_user).id response.should redirect_to(:controller => 'request', :action => 'list', :post_redirect => 1) end diff --git a/spec/fixtures/files/track-response-exim-bounce.email b/spec/fixtures/files/track-response-exim-bounce.email new file mode 100644 index 000000000..8d40380b1 --- /dev/null +++ b/spec/fixtures/files/track-response-exim-bounce.email @@ -0,0 +1,67 @@ +Delivered-To: mysociety.robin@gmail.com +Received: by 10.216.187.197 with SMTP id y47cs98510wem; + Tue, 6 Sep 2011 14:22:44 -0700 (PDT) +Received: by 10.216.203.79 with SMTP id e57mr78207weo.42.1315344164092; + Tue, 06 Sep 2011 14:22:44 -0700 (PDT) +Return-Path: <> +Received: from wildfire.ukcod.org.uk (wildfire.ukcod.org.uk [89.238.145.74]) + by mx.google.com with ESMTPS id n64si9483505weq.102.2011.09.06.14.22.42 + (version=TLSv1/SSLv3 cipher=OTHER); + Tue, 06 Sep 2011 14:22:43 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of wildfire.ukcod.org.uk designates 89.238.145.74 as permitted sender) client-ip=89.238.145.74; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of wildfire.ukcod.org.uk designates 89.238.145.74 as permitted sender) smtp.mail= +Received: from Debian-exim by wildfire.ukcod.org.uk with local (Exim 4.69) + id 1R136L-0003xr-1Q + for team@whatdotheyknow.com; Tue, 06 Sep 2011 22:22:37 +0100 +X-Failed-Recipients: user@example.com +Auto-Submitted: auto-replied +From: Mail Delivery System <Mailer-Daemon@wildfire.ukcod.org.uk> +To: team@whatdotheyknow.com +Subject: Mail delivery failed: returning message to sender +Message-Id: <E1R136L-0003xr-1Q@wildfire.ukcod.org.uk> +Date: Tue, 06 Sep 2011 22:22:37 +0100 +List-Id: Admin Team for What Do They Know <team@whatdotheyknow.com> + +This message was created automatically by mail delivery software. + +A message that you sent could not be delivered to one or more of its +recipients. This is a permanent error. The following address(es) failed: + + user@example.com + Unrouteable address + +------ This is a copy of the message, including all the headers. ------ + +Return-path: <team@whatdotheyknow.com> +Received: from foi by wildfire.ukcod.org.uk with local (Exim 4.69) + (envelope-from <team@whatdotheyknow.com>) + id 1R136J-0003xp-Td + for user@example.com; Tue, 06 Sep 2011 22:22:36 +0100 +Date: Tue, 6 Sep 2011 22:22:35 +0100 +From: WhatDoTheyKnow <team@whatdotheyknow.com> +To: Nonexistent User <user@example.com> +Subject: Your WhatDoTheyKnow email alert +Mime-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Precedence: bulk +Auto-Submitted: auto-generated +Message-Id: <E1R136J-0003xp-Td@wildfire.ukcod.org.uk> + +FOI requests to 'Maritime and Coastguard Agency' +================================================ + +-- MCA & HM Coastguard Official Vehicles -- +Maritime and Coastguard Agency sent a response to Peter Smith (12 August 2011) + "Peter In response (reference F0000881) to your FOI questions the + MCA answer is:- 1. All MCA vehicles are purchased outright. 2. Yes + there is a volu..." +http://www.whatdotheyknow.com/request/mca_hm_coastguard_official_vehic#incoming-201529 + + +Alter your subscription +======================= + +Please click on the link below to cancel or alter these emails. +http://www.whatdotheyknow.com/c/ie4pkpy70dl4b8flsig + +-- the WhatDoTheyKnow team diff --git a/spec/fixtures/files/track-response-multipart-report.email b/spec/fixtures/files/track-response-multipart-report.email new file mode 100644 index 000000000..4f8e6d86b --- /dev/null +++ b/spec/fixtures/files/track-response-multipart-report.email @@ -0,0 +1,113 @@ +Delivered-To: mysociety.robin@gmail.com +Received: by 10.216.187.197 with SMTP id y47cs96752wem; + Tue, 6 Sep 2011 13:37:26 -0700 (PDT) +Received: by 10.216.212.37 with SMTP id x37mr3361871weo.35.1315341445852; + Tue, 06 Sep 2011 13:37:25 -0700 (PDT) +Return-Path: <> +Received: from wildfire.ukcod.org.uk (wildfire.ukcod.org.uk [89.238.145.74]) + by mx.google.com with ESMTPS id h49si1800318wed.40.2011.09.06.13.37.25 + (version=TLSv1/SSLv3 cipher=OTHER); + Tue, 06 Sep 2011 13:37:25 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of wildfire.ukcod.org.uk designates 89.238.145.74 as permitted sender) client-ip=89.238.145.74; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of wildfire.ukcod.org.uk designates 89.238.145.74 as permitted sender) smtp.mail= +Received: from cluster-a.mailcontrol.com ([85.115.52.190]:43258) + by wildfire.ukcod.org.uk with esmtp (Exim 4.69) + id 1R12OV-0003KQ-9c + for team@whatdotheyknow.com; Tue, 06 Sep 2011 21:37:19 +0100 +Received: from mail.example.com ([62.6.240.178]) + by rly22a.srv.mailcontrol.com (MailControl) with ESMTP id p86KbIZV025877 + (version=TLSv1/SSLv3 cipher=AES128-SHA bits=128 verify=NO) + for <team@whatdotheyknow.com>; Tue, 6 Sep 2011 21:37:18 +0100 +Received: from MX05.example.com (10.100.14.57) by GCEX534.PHSGROUP.local + (10.100.21.78) with Microsoft SMTP Server id 14.1.270.1; Tue, 6 Sep 2011 + 21:38:52 +0100 +From: <postmaster@xyz.local> +To: <team@whatdotheyknow.com> +Date: Tue, 6 Sep 2011 21:37:13 +0100 +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="9B095B5ADSN=_01CC68D9CD29F1E300015B60MX05.example.com" +X-DSNContext: 7ce717b1 - 1148 - 00000002 - 00000000 +Message-ID: <DEU8FnRwh00000d1b@MX05.example.com> +Subject: Delivery Status Notification (Failure) +X-Scanned-By: MailControl A-12-01-02 (www.mailcontrol.com) on 10.65.0.132 +List-Id: Admin Team for What Do They Know <team@whatdotheyknow.com> + +--9B095B5ADSN=_01CC68D9CD29F1E300015B60MX05.example.com +Content-Type: text/plain; charset="unicode-1-1-utf-7" + +This is an automatically generated Delivery Status Notification. + +Delivery to the following recipients failed. + + FailedUser@example.com + + + + +--9B095B5ADSN=_01CC68D9CD29F1E300015B60MX05.example.com +Content-Type: message/delivery-status + +Reporting-MTA: dns;MX05.example.com +Received-From-MTA: dns;MX04.example.com +Arrival-Date: Tue, 6 Sep 2011 21:37:13 +0100 + +Final-Recipient: rfc822;FailedUser@example.com +Action: failed +Status: 5.2.2 +X-Display-Name: Failed User + + +--9B095B5ADSN=_01CC68D9CD29F1E300015B60MX05.example.com +Content-Type: message/rfc822 + +Received: from MX04.example.com ([10.100.14.56]) by MX05.example.com with + Microsoft SMTPSVC(5.0.2195.6713); Tue, 6 Sep 2011 21:37:13 +0100 +Received: from DCEX553.example.com ([10.211.10.27]) by MX04.example.com with + Microsoft SMTPSVC(5.0.2195.6713); Tue, 6 Sep 2011 21:37:13 +0100 +Received: from cluster-a.mailcontrol.com (85.115.52.190) by mail.example.com + (10.211.10.27) with Microsoft SMTP Server id 14.1.270.1; Tue, 6 Sep 2011 + 20:36:49 +0100 +Received: from wildfire.ukcod.org.uk (wildfire.ukcod.org.uk [89.238.145.74]) + by rly01a.srv.mailcontrol.com (MailControl) with ESMTP id p86KbAZN016792 + (version=TLSv1/SSLv3 cipher=AES256-SHA bits=256 verify=NO) for + <faileduser@example.com>; Tue, 6 Sep 2011 21:37:11 +0100 +Received: from foi by wildfire.ukcod.org.uk with local (Exim 4.69) + (envelope-from <team@whatdotheyknow.com>) id 1R12OL-0003K9-UE for + faileduser@example.com; Tue, 06 Sep 2011 21:37:10 +0100 +Date: Tue, 6 Sep 2011 21:37:09 +0100 +From: WhatDoTheyKnow <team@whatdotheyknow.com> +To: Failed <faileduser@example.com> +Subject: Your WhatDoTheyKnow email alert +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Precedence: bulk +Auto-Submitted: auto-generated +Message-ID: <E1R12OL-0003K9-UE@wildfire.ukcod.org.uk> +X-Mailcontrol-Inbound: 7DN0MnCsYCrl5jj2!rZ4BuxxWQ2q0l7t3Lrex4V7ScCE2eoC2RNzHw== +X-Spam-Score: -0.857 +X-Scanned-By: MailControl A-12-01-02 (www.mailcontrol.com) on 10.65.0.111 +Return-Path: team@whatdotheyknow.com +X-OriginalArrivalTime: 06 Sep 2011 20:37:13.0070 (UTC) FILETIME=[C20250E0:01CC6CD4] + +Requests or responses matching 'bottled water cooler' +===================================================== + +-- HS2 meetings - Agendas and Minutes -- +Warwickshire County Council sent a response to Richard Jones (21 July 2011) + "Dear Mr Jones FREEDOM OF INFORMATION ACT 2000 - INFORMATION + REQUEST Your request for information has now been considered. The + information you have..." +http://www.whatdotheyknow.com/request/hs2_meetings_agendas_and_minutes_2#incoming-195748 + + +Alter your subscription +======================= + +Please click on the link below to cancel or alter these emails. +http://www.whatdotheyknow.com/c/f76ffwifzlo5sk4egr3 + +-- the WhatDoTheyKnow team + + +--9B095B5ADSN=_01CC68D9CD29F1E300015B60MX05.example.com--
\ No newline at end of file diff --git a/spec/fixtures/users.yml b/spec/fixtures/users.yml index c54ac0985..16ffec034 100644 --- a/spec/fixtures/users.yml +++ b/spec/fixtures/users.yml @@ -21,7 +21,7 @@ silly_name_user: hashed_password: 6b7cd45a5f35fd83febc0452a799530398bfb6e8 # jonespassword updated_at: 2007-11-01 10:39:15.491593 created_at: 2007-11-01 10:39:15.491593 - email_confirmed: false + email_confirmed: true admin_level: 'none' ban_text: '' locale: 'en' @@ -40,3 +40,16 @@ admin_user: ban_text: '' locale: '' about_me: '' +unconfirmed_user: + id: "4" + name: "Unconfirmed" + url_name: unconfirmed + email: unconfirmed@localhost + salt: "-6116981980.392287733335677" + hashed_password: 6b7cd45a5f35fd83febc0452a799530398bfb6e8 # jonespassword + updated_at: 2007-11-01 10:39:15.491593 + created_at: 2007-11-01 10:39:15.491593 + email_confirmed: false + admin_level: 'none' + ban_text: '' + about_me: '' diff --git a/spec/lib/external_command_scripts/output.sh b/spec/lib/external_command_scripts/output.sh deleted file mode 100755 index 0472c89a3..000000000 --- a/spec/lib/external_command_scripts/output.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -out_msg=${1:-out} -err_msg=${2:-} -repeats=${3:-10} -exit_status=${4:-0} - -n=0 -while [ "$n" -lt "$repeats" ] -do - if [ -n "$out_msg" ] - then - echo "$out_msg $n" - fi - if [ -n "$err_msg" ] - then - echo >&2 "$err_msg $n" - fi - n=$[$n + 1] -done - -exit "$exit_status" diff --git a/spec/lib/external_command_spec.rb b/spec/lib/external_command_spec.rb deleted file mode 100644 index 0ff1a9c0a..000000000 --- a/spec/lib/external_command_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# This is a test of the external_command library - -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') -script_dir = File.join(File.dirname(__FILE__), 'external_command_scripts') -output_script = File.join(script_dir, "output.sh") - -require 'external_command' - -describe "when running ExternalCommand" do - - it "should get correct status code for /bin/true" do - t = ExternalCommand.new("/bin/true").run() - t.status.should == 0 - t.out.should == "" - t.err.should == "" - end - - it "should get correct status code for /bin/false" do - f = ExternalCommand.new("/bin/false").run() - f.status.should == 1 - f.out.should == "" - f.err.should == "" - end - - it "should get stdout and stderr" do - f = ExternalCommand.new(output_script, "out", "err", "10", "23").run() - f.status.should == 23 - f.out.should == (0..9).map {|i| "out #{i}\n"}.join("") - f.err.should == (0..9).map {|i| "err #{i}\n"}.join("") - end - - it "should work with large amounts of data" do - f = ExternalCommand.new(output_script, "a longer output line", "a longer error line", "10000", "5").run() - f.status.should == 5 - f.out.should == (0..9999).map {|i| "a longer output line #{i}\n"}.join("") - f.err.should == (0..9999).map {|i| "a longer error line #{i}\n"}.join("") - end - -end - diff --git a/spec/models/track_mailer_spec.rb b/spec/models/track_mailer_spec.rb index b90ca7e52..6612641c1 100644 --- a/spec/models/track_mailer_spec.rb +++ b/spec/models/track_mailer_spec.rb @@ -22,7 +22,8 @@ describe TrackMailer do :last_daily_track_email= => true, :save! => true, :url_name => 'test-name', - :get_locale => 'en') + :get_locale => 'en', + :should_be_emailed? => true) User.stub!(:find).and_return([@user]) @user.stub!(:no_xapian_reindex=) end @@ -110,6 +111,36 @@ describe TrackMailer do end + describe 'when a user should not be emailed' do + before do + @user = mock_model(User, :no_xapian_reindex= => false, + :last_daily_track_email= => true, + :save! => true, + :url_name => 'test-name', + :should_be_emailed? => false) + User.stub!(:find).and_return([@user]) + @user.stub!(:no_xapian_reindex=) + end + + it 'should not ask for any daily track things for the user' do + expected_conditions = [ "tracking_user_id = ? and track_medium = ?", @user.id, 'email_daily' ] + TrackThing.should_not_receive(:find).with(:all, :conditions => expected_conditions).and_return([]) + TrackMailer.alert_tracks + end + + + it 'should not set the no_xapian_reindex flag on the user' do + @user.should_not_receive(:no_xapian_reindex=).with(true) + TrackMailer.alert_tracks + end + + it 'should not update the time of the user\'s last daily tracking email' do + @user.should_not_receive(:last_daily_track_email=).with(Time.now) + @user.should_not_receive(:save!) + TrackMailer.alert_tracks + end + end + end describe 'delivering the email' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ee6916ffc..751a61060 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -282,3 +282,26 @@ describe User, "when setting a profile photo" do # end end +describe User, "when unconfirmed" do + fixtures :users + + before do + @user = users(:unconfirmed_user) + end + + it "should not be emailed" do + @user.should_be_emailed?.should be_false + end +end + +describe User, "when emails have bounced" do + fixtures :users + + it "should record bounces" do + User.record_bounce_for_email("bob@localhost", "The reason we think the email bounced (e.g. a bounce message)") + + user = User.find_user_by_email("bob@localhost") + user.email_bounced_at.should_not be_nil + user.email_bounce_message.should == "The reason we think the email bounced (e.g. a bounce message)" + end +end diff --git a/spec/script/handle-mail-replies_spec.rb b/spec/script/handle-mail-replies_spec.rb new file mode 100644 index 000000000..856476c3f --- /dev/null +++ b/spec/script/handle-mail-replies_spec.rb @@ -0,0 +1,33 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require "external_command" + +def mail_reply_test(email_filename) + Dir.chdir RAILS_ROOT do + xc = ExternalCommand.new("script/handle-mail-replies", "--test") + xc.run(load_file_fixture(email_filename)) + + xc.err.should == "" + return xc + end +end + +describe "When filtering" do + it "should detect an Exim bounce" do + r = mail_reply_test("track-response-exim-bounce.email") + r.status.should == 1 + r.out.should == "user@example.com\n" + end + + it "should pass on a non-bounce message" do + r = mail_reply_test("incoming-request-bad-uuencoding.email") + r.status.should == 0 + r.out.should == "" + end + + it "should detect a multipart bounce" do + r = mail_reply_test("track-response-multipart-report.email") + r.status.should == 1 + r.out.should == "FailedUser@example.com\n" + end +end + |