aboutsummaryrefslogtreecommitdiffstats
path: root/app/mailers
diff options
context:
space:
mode:
Diffstat (limited to 'app/mailers')
-rw-r--r--app/mailers/application_mailer.rb30
-rw-r--r--app/mailers/contact_mailer.rb45
-rw-r--r--app/mailers/outgoing_mailer.rb96
-rw-r--r--app/mailers/request_mailer.rb471
-rw-r--r--app/mailers/track_mailer.rb135
-rw-r--r--app/mailers/user_mailer.rb44
6 files changed, 821 insertions, 0 deletions
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
new file mode 100644
index 000000000..d2230bb82
--- /dev/null
+++ b/app/mailers/application_mailer.rb
@@ -0,0 +1,30 @@
+# models/application_mailer.rb:
+# Shared code between different mailers.
+#
+# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved.
+# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
+
+require 'action_mailer/version'
+class ApplicationMailer < ActionMailer::Base
+ # Include all the functions views get, as emails call similar things.
+ helper :application
+ include MailerHelper
+
+ # This really should be the default - otherwise you lose any information
+ # about the errors, and have to do error checking on return codes.
+ self.raise_delivery_errors = true
+
+ def blackhole_email
+ AlaveteliConfiguration::blackhole_prefix+"@"+AlaveteliConfiguration::incoming_email_domain
+ end
+
+ # URL generating functions are needed by all controllers (for redirects),
+ # views (for links) and mailers (for use in emails), so include them into
+ # all of all.
+ include LinkToHelper
+
+ # Site-wide access to configuration settings
+ include ConfigHelper
+
+end
+
diff --git a/app/mailers/contact_mailer.rb b/app/mailers/contact_mailer.rb
new file mode 100644
index 000000000..4dc49bf8b
--- /dev/null
+++ b/app/mailers/contact_mailer.rb
@@ -0,0 +1,45 @@
+# models/contact_mailer.rb:
+# Sends contact form mails.
+#
+# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved.
+# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
+
+class ContactMailer < ApplicationMailer
+ # Send message to administrator
+ def to_admin_message(name, email, subject, message, logged_in_user, last_request, last_body)
+ @message, @logged_in_user, @last_request, @last_body = message, logged_in_user, last_request, last_body
+
+ mail(:from => "#{name} <#{email}>",
+ :to => contact_from_name_and_email,
+ :subject => subject)
+ end
+
+ # We always set Reply-To when we set Return-Path to be different from From,
+ # since some email clients seem to erroneously use the envelope from when
+ # they shouldn't, and this might help. (Have had mysterious cases of a
+ # reply coming in duplicate from a public body to both From and envelope
+ # from)
+
+ # Send message to another user
+ def user_message(from_user, recipient_user, from_user_url, subject, message)
+ @message, @from_user, @recipient_user, @from_user_url = message, from_user, recipient_user, from_user_url
+
+ # Do not set envelope from address to the from_user, so they can't get
+ # someone's email addresses from transitory bounce messages.
+ headers('Return-Path' => blackhole_email, 'Reply-To' => from_user.name_and_email)
+
+ mail(:from => from_user.name_and_email,
+ :to => recipient_user.name_and_email,
+ :subject => subject)
+ end
+
+ # Send message to a user from the administrator
+ def from_admin_message(recipient_user, subject, message)
+ @message, @from_user, @recipient_user = message, contact_from_name_and_email, recipient_user
+
+ mail(:from => contact_from_name_and_email,
+ :to => recipient_user.name_and_email,
+ :bcc => AlaveteliConfiguration::contact_email,
+ :subject => subject)
+ end
+end
diff --git a/app/mailers/outgoing_mailer.rb b/app/mailers/outgoing_mailer.rb
new file mode 100644
index 000000000..083c05a7c
--- /dev/null
+++ b/app/mailers/outgoing_mailer.rb
@@ -0,0 +1,96 @@
+# models/outgoing_mailer.rb:
+# Emails which go to public bodies on behalf of users.
+#
+# Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved.
+# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
+
+# Note: The layout for this wraps messages by lines rather than (blank line
+# separated) paragraphs, as is the convention for all the other mailers. This
+# turned out to fit better with user exepectations when formatting messages.
+#
+# XXX The other mail templates are written to use blank line separated
+# paragraphs. They could be rewritten, and the wrapping method made uniform
+# throughout the application.
+
+class OutgoingMailer < ApplicationMailer
+ # Email to public body requesting info
+ def initial_request(info_request, outgoing_message)
+ @info_request, @outgoing_message, @contact_email = info_request, outgoing_message, AlaveteliConfiguration::contact_email
+ @wrap_lines_as_paragraphs = true
+ headers["message-id"] = OutgoingMailer.id_for_message(outgoing_message)
+
+ mail(:from => info_request.incoming_name_and_email,
+ :to => info_request.recipient_name_and_email,
+ :subject => info_request.email_subject_request)
+ end
+
+ # Later message to public body regarding existing request
+ def followup(info_request, outgoing_message, incoming_message_followup)
+ @info_request, @outgoing_message, @incoming_message_followup, @contact_email = info_request, outgoing_message, incoming_message_followup, AlaveteliConfiguration::contact_email
+ @wrap_lines_as_paragraphs = true
+ headers["message-id"] = OutgoingMailer.id_for_message(outgoing_message)
+
+ mail(:from => info_request.incoming_name_and_email,
+ :to => OutgoingMailer.name_and_email_for_followup(info_request, incoming_message_followup),
+ :subject => OutgoingMailer.subject_for_followup(info_request, outgoing_message))
+ end
+
+ # XXX the condition checking valid_to_reply_to? also appears in views/request/_followup.html.erb,
+ # it shouldn't really, should call something here.
+ # XXX also OutgoingMessage.get_salutation
+ # XXX these look like they should be members of IncomingMessage, but logically they
+ # need to work even when IncomingMessage is nil
+ def OutgoingMailer.name_and_email_for_followup(info_request, incoming_message_followup)
+ if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to?
+ return info_request.recipient_name_and_email
+ else
+ # calling safe_mail_from from so censor rules are run
+ return MailHandler.address_from_name_and_email(incoming_message_followup.safe_mail_from,
+ incoming_message_followup.from_email)
+ end
+ end
+ # Used in the preview of followup
+ def OutgoingMailer.name_for_followup(info_request, incoming_message_followup)
+ if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to?
+ return info_request.public_body.name
+ else
+ # calling safe_mail_from from so censor rules are run
+ return incoming_message_followup.safe_mail_from || info_request.public_body.name
+ end
+ end
+ # Used when making list of followup places to remove duplicates
+ def OutgoingMailer.email_for_followup(info_request, incoming_message_followup)
+ if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to?
+ return info_request.recipient_email
+ else
+ return incoming_message_followup.from_email
+ end
+ end
+ # Subject to use for followup
+ def OutgoingMailer.subject_for_followup(info_request, outgoing_message)
+ if outgoing_message.what_doing == 'internal_review'
+ return "Internal review of " + info_request.email_subject_request
+ else
+ return info_request.email_subject_followup(outgoing_message.incoming_message_followup)
+ end
+ end
+ # Whether we have a valid email address for a followup
+ def OutgoingMailer.is_followupable?(info_request, incoming_message_followup)
+ if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to?
+ return info_request.recipient_email_valid_for_followup?
+ else
+ # email has been checked in incoming_message_followup.valid_to_reply_to? above
+ return true
+ end
+ end
+ # Message-ID to use
+ def OutgoingMailer.id_for_message(outgoing_message)
+ message_id = "ogm-" + outgoing_message.id.to_s
+ t = Time.now
+ message_id += "+" + '%08x%05x-%04x' % [t.to_i, t.tv_usec, rand(0xffff)]
+ message_id += "@" + AlaveteliConfiguration::incoming_email_domain
+ return "<" + message_id + ">"
+ end
+
+end
+
diff --git a/app/mailers/request_mailer.rb b/app/mailers/request_mailer.rb
new file mode 100644
index 000000000..4dbce6738
--- /dev/null
+++ b/app/mailers/request_mailer.rb
@@ -0,0 +1,471 @@
+# models/request_mailer.rb:
+# Alerts relating to requests.
+#
+# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved.
+# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
+
+class RequestMailer < ApplicationMailer
+ # Used when an FOI officer uploads a response from their web browser - this is
+ # the "fake" email used to store in the same format in the database as if they
+ # had emailed it.
+ def fake_response(info_request, from_user, message_body, attachment_name, attachment_content)
+ @message_body = message_body
+
+ if !attachment_name.nil? && !attachment_content.nil?
+ content_type = AlaveteliFileTypes.filename_to_mimetype(attachment_name) || 'application/octet-stream'
+
+ attachments[attachment_name] = {:content => attachment_content,
+ :content_type => content_type}
+ end
+
+ mail(:from => from_user.name_and_email,
+ :to => info_request.incoming_name_and_email)
+ end
+
+ # Used when a response is uploaded using the API
+ def external_response(info_request, message_body, sent_at, attachment_hashes)
+ @message_body = message_body
+
+ attachment_hashes.each do |attachment_hash|
+ attachments[attachment_hash[:filename]] = {:content => attachment_hash[:body],
+ :content_type => attachment_hash[:content_type]}
+ end
+
+ mail(:from => blackhole_email,
+ :to => info_request.incoming_name_and_email,
+ :date => sent_at)
+ end
+
+ # Incoming message arrived for a request, but new responses have been stopped.
+ def stopped_responses(info_request, email, raw_email_data)
+ headers('Return-Path' => blackhole_email, # we don't care about bounces, likely from spammers
+ 'Auto-Submitted' => 'auto-replied') # http://tools.ietf.org/html/rfc3834
+
+ attachments.inline["original.eml"] = raw_email_data
+
+ @info_request = info_request
+ @contact_email = AlaveteliConfiguration::contact_email
+
+ mail(:to => email.from_addrs[0].to_s,
+ :from => contact_from_name_and_email,
+ :reply_to => contact_from_name_and_email,
+ :subject => _("Your response to an FOI request was not delivered"))
+ end
+
+ # An FOI response is outside the scope of the system, and needs admin attention
+ def requires_admin(info_request, set_by = nil, message = "")
+ user = set_by || info_request.user
+ @reported_by = user
+ @url = request_url(info_request)
+ @admin_url = admin_request_show_url(info_request)
+ @info_request = info_request
+ @message = message
+
+ mail(:from => user.name_and_email,
+ :to => contact_from_name_and_email,
+ :subject => _("FOI response requires admin ({{reason}}) - {{title}}", :reason => info_request.described_state, :title => info_request.title))
+ end
+
+ # Tell the requester that a new response has arrived
+ def new_response(info_request, incoming_message)
+ # Don't use login link here, just send actual URL. This is
+ # because people tend to forward these emails amongst themselves.
+ @url = incoming_message_url(incoming_message)
+ @incoming_message, @info_request = incoming_message, info_request
+
+ headers('Return-Path' => blackhole_email,
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => info_request.user.name_and_email,
+ :subject => _("New response to your FOI request - ") + info_request.title,
+ :charset => "UTF-8",
+ # not much we can do if the user's email is broken
+ :reply_to => contact_from_name_and_email)
+ end
+
+ # Tell the requester that the public body is late in replying
+ def overdue_alert(info_request, user)
+ respond_url = respond_to_last_url(info_request) + "#followup"
+
+ post_redirect = PostRedirect.new(
+ :uri => respond_to_last_url(info_request) + "#followup",
+ :user_id => user.id)
+ post_redirect.save!
+ url = confirm_url(:email_token => post_redirect.email_token)
+
+ @url = confirm_url(:email_token => post_redirect.email_token)
+ @info_request = info_request
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => user.name_and_email,
+ :subject => (_("Delayed response to your FOI request - ") + info_request.title).html_safe)
+ end
+
+ # Tell the requester that the public body is very late in replying
+ def very_overdue_alert(info_request, user)
+ respond_url = respond_to_last_url(info_request) + "#followup"
+
+ post_redirect = PostRedirect.new(
+ :uri => respond_to_last_url(info_request) + "#followup",
+ :user_id => user.id)
+ post_redirect.save!
+ @url = confirm_url(:email_token => post_redirect.email_token)
+ @info_request = info_request
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => user.name_and_email,
+ :subject => (_("You're long overdue a response to your FOI request - ") + info_request.title).html_safe)
+ end
+
+ # Tell the requester that they need to say if the new response
+ # contains info or not
+ def new_response_reminder_alert(info_request, incoming_message)
+ # Make a link going to the form to describe state, and which logs the
+ # user in.
+ post_redirect = PostRedirect.new(
+ :uri => request_url(info_request) + "#describe_state_form_1",
+ :user_id => info_request.user.id)
+ post_redirect.save!
+ @url = confirm_url(:email_token => post_redirect.email_token)
+ @incoming_message = incoming_message
+ @info_request = info_request
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => info_request.user.name_and_email,
+ :subject => _("Was the response you got to your FOI request any good?"))
+ end
+
+ # Tell the requester that someone updated their old unclassified request
+ def old_unclassified_updated(info_request)
+ @url = request_url(info_request)
+ @info_request = info_request
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => info_request.user.name_and_email,
+ :subject => _("Someone has updated the status of your request"))
+ end
+
+ # Tell the requester that they need to clarify their request
+ def not_clarified_alert(info_request, incoming_message)
+ respond_url = show_response_url(:id => info_request.id, :incoming_message_id => incoming_message.id)
+ respond_url = respond_url + "#followup"
+
+ post_redirect = PostRedirect.new(
+ :uri => respond_url,
+ :user_id => info_request.user.id)
+ post_redirect.save!
+ @url = confirm_url(:email_token => post_redirect.email_token)
+ @incoming_message = incoming_message
+ @info_request = info_request
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => info_request.user.name_and_email,
+ :subject => _("Clarify your FOI request - ") + info_request.title)
+ end
+
+ # Tell requester that somebody add an annotation to their request
+ def comment_on_alert(info_request, comment)
+ @comment, @info_request = comment, info_request
+ @url = comment_url(comment)
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => info_request.user.name_and_email,
+ :subject => (_("Somebody added a note to your FOI request - ") + info_request.title).html_safe)
+ end
+ def comment_on_alert_plural(info_request, count, earliest_unalerted_comment)
+ @count, @info_request = count, info_request
+ @url = comment_url(earliest_unalerted_comment)
+
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'X-Auto-Response-Suppress' => 'OOF')
+
+ mail(:from => contact_from_name_and_email,
+ :to => info_request.user.name_and_email,
+ :subject => (_("Some notes have been added to your FOI request - ") + info_request.title).html_safe)
+ end
+
+ # Class function, called by script/mailin with all incoming responses.
+ # [ This is a copy (Monkeypatch!) of function from action_mailer/base.rb,
+ # but which additionally passes the raw_email to the member function, as we
+ # want to record it.
+ #
+ # That is because we want to be sure we properly record the actual message
+ # received in its raw form - so any information won't be lost in a round
+ # trip via the mail handler, or by bugs in it, and so we can use something
+ # other than TMail at a later date. And so we can offer an option to download the
+ # actual original mail sent by the authority in the admin interface (so
+ # can check that attachment decoding failures are problems in the message,
+ # not in our code). ]
+ def self.receive(raw_email)
+ logger.info "Received mail:\n #{raw_email}" unless logger.nil?
+ mail = MailHandler.mail_from_raw_email(raw_email)
+ new.receive(mail, raw_email)
+ end
+
+ # Find which info requests the email is for
+ def requests_matching_email(email)
+ # We deliberately don't use Envelope-to here, so ones that are BCC
+ # drop into the holding pen for checking.
+ reply_info_requests = [] # XXX should be set?
+ for address in (email.to || []) + (email.cc || [])
+ reply_info_request = InfoRequest.find_by_incoming_email(address)
+ reply_info_requests.push(reply_info_request) if reply_info_request
+ end
+ return reply_info_requests
+ end
+
+ # Member function, called on the new class made in self.receive above
+ def receive(email, raw_email)
+ # Find which info requests the email is for
+ reply_info_requests = self.requests_matching_email(email)
+ # Nothing found, so save in holding pen
+ if reply_info_requests.size == 0
+ reason = _("Could not identify the request from the email address")
+ request = InfoRequest.holding_pen_request
+ request.receive(email, raw_email, false, reason)
+ return
+ end
+
+ # Send the message to each request, to be archived with it
+ for reply_info_request in reply_info_requests
+ # If environment variable STOP_DUPLICATES is set, don't send message with same id again
+ if ENV['STOP_DUPLICATES']
+ if reply_info_request.already_received?(email, raw_email)
+ raise "message " + email.message_id + " already received by request"
+ end
+ end
+ reply_info_request.receive(email, raw_email)
+ end
+ end
+
+ # Send email alerts for overdue requests
+ def self.alert_overdue_requests()
+ info_requests = InfoRequest.find(:all,
+ :conditions => [
+ "described_state = 'waiting_response'
+ AND awaiting_description = ?
+ AND user_id is not null
+ AND (SELECT id
+ FROM user_info_request_sent_alerts
+ WHERE alert_type = 'very_overdue_1'
+ AND info_request_id = info_requests.id
+ AND user_id = info_requests.user_id
+ AND info_request_event_id = (SELECT max(id)
+ FROM info_request_events
+ WHERE event_type in ('sent',
+ 'followup_sent',
+ 'resent',
+ 'followup_resent')
+ AND info_request_id = info_requests.id)
+ ) IS NULL", false
+ ],
+ :include => [ :user ]
+ )
+ for info_request in info_requests
+ alert_event_id = info_request.last_event_forming_initial_request.id
+ # Only overdue requests
+ calculated_status = info_request.calculate_status
+ if ['waiting_response_overdue', 'waiting_response_very_overdue'].include?(calculated_status)
+ if calculated_status == 'waiting_response_overdue'
+ alert_type = 'overdue_1'
+ elsif calculated_status == 'waiting_response_very_overdue'
+ alert_type = 'very_overdue_1'
+ else
+ raise "unknown request status"
+ end
+
+ # For now, just to the user who created the request
+ sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = ?
+ AND user_id = ?
+ AND info_request_id = ?
+ AND info_request_event_id = ?",
+ alert_type,
+ info_request.user_id,
+ info_request.id,
+ alert_event_id])
+ if sent_already.nil?
+ # Alert not yet sent for this user, so send it
+ store_sent = UserInfoRequestSentAlert.new
+ store_sent.info_request = info_request
+ store_sent.user = info_request.user
+ store_sent.alert_type = alert_type
+ store_sent.info_request_event_id = alert_event_id
+ # Only send the alert if the user can act on it by making a followup
+ # (otherwise they are banned, and there is no point sending it)
+ if info_request.user.can_make_followup?
+ if calculated_status == 'waiting_response_overdue'
+ RequestMailer.overdue_alert(info_request, info_request.user).deliver
+ elsif calculated_status == 'waiting_response_very_overdue'
+ RequestMailer.very_overdue_alert(info_request, info_request.user).deliver
+ else
+ raise "unknown request status"
+ end
+ end
+ store_sent.save!
+ end
+ end
+ end
+ end
+
+ # Send email alerts for new responses which haven't been classified. By default,
+ # it goes out 3 days after last update of event, then after 10, then after 24.
+ def self.alert_new_response_reminders
+ AlaveteliConfiguration::new_response_reminder_after_days.each_with_index do |days, i|
+ self.alert_new_response_reminders_internal(days, "new_response_reminder_#{i+1}")
+ end
+ end
+ def self.alert_new_response_reminders_internal(days_since, type_code)
+ info_requests = InfoRequest.find_old_unclassified(:order => 'info_requests.id',
+ :include => [:user],
+ :age_in_days => days_since)
+
+ for info_request in info_requests
+ alert_event_id = info_request.get_last_response_event_id
+ last_response_message = info_request.get_last_response
+ if alert_event_id.nil?
+ raise "internal error, no last response while making alert new response reminder, request id " + info_request.id.to_s
+ end
+ # To the user who created the request
+ sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = ? and user_id = ? and info_request_id = ? and info_request_event_id = ?", type_code, info_request.user_id, info_request.id, alert_event_id])
+ if sent_already.nil?
+ # Alert not yet sent for this user
+ store_sent = UserInfoRequestSentAlert.new
+ store_sent.info_request = info_request
+ store_sent.user = info_request.user
+ store_sent.alert_type = type_code
+ store_sent.info_request_event_id = alert_event_id
+ # XXX uses same template for reminder 1 and reminder 2 right now.
+ RequestMailer.new_response_reminder_alert(info_request, last_response_message).deliver
+ store_sent.save!
+ end
+ end
+ end
+
+ # Send email alerts for requests which need clarification. Goes out 3 days
+ # after last update of event.
+ def self.alert_not_clarified_request()
+ info_requests = InfoRequest.find(:all, :conditions => [ "awaiting_description = ? and described_state = 'waiting_clarification' and info_requests.updated_at < ?", false, Time.now() - 3.days ], :include => [ :user ], :order => "info_requests.id" )
+ for info_request in info_requests
+ alert_event_id = info_request.get_last_response_event_id
+ last_response_message = info_request.get_last_response
+ if alert_event_id.nil?
+ raise "internal error, no last response while making alert not clarified reminder, request id " + info_request.id.to_s
+ end
+ # To the user who created the request
+ sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = 'not_clarified_1' and user_id = ? and info_request_id = ? and info_request_event_id = ?", info_request.user_id, info_request.id, alert_event_id])
+ if sent_already.nil?
+ # Alert not yet sent for this user
+ store_sent = UserInfoRequestSentAlert.new
+ store_sent.info_request = info_request
+ store_sent.user = info_request.user
+ store_sent.alert_type = 'not_clarified_1'
+ store_sent.info_request_event_id = alert_event_id
+ # Only send the alert if the user can act on it by making a followup
+ # (otherwise they are banned, and there is no point sending it)
+ if info_request.user.can_make_followup?
+ RequestMailer.not_clarified_alert(info_request, last_response_message).deliver
+ end
+ store_sent.save!
+ end
+ end
+ end
+
+ # Send email alert to request submitter for new comments on the request.
+ def self.alert_comment_on_request()
+
+ # We only check comments made in the last month - this means if the
+ # cron jobs broke for more than a month events would be lost, but no
+ # matter. I suspect the performance gain will be needed (with an index on updated_at)
+
+ # XXX the :order part info_request_events.created_at is a work around
+ # for a very old Rails bug which means eager loading does not respect
+ # association orders.
+ # http://dev.rubyonrails.org/ticket/3438
+ # http://lists.rubyonrails.org/pipermail/rails-core/2006-July/001798.html
+ # That that patch has not been applied, despite bribes of beer, is
+ # typical of the lack of quality of Rails.
+
+ info_requests = InfoRequest.find(:all,
+ :conditions => [
+ "info_requests.id in (
+ select info_request_id
+ from info_request_events
+ where event_type = 'comment'
+ and created_at > (now() - '1 month'::interval)
+ )"
+ ],
+ :include => [ { :info_request_events => :user_info_request_sent_alerts } ],
+ :order => "info_requests.id, info_request_events.created_at"
+ )
+ for info_request in info_requests
+
+ next if info_request.is_external?
+ # Count number of new comments to alert on
+ earliest_unalerted_comment_event = nil
+ last_comment_event = nil
+ count = 0
+ for e in info_request.info_request_events.reverse
+ # alert on comments, which were not made by the user who originally made the request
+ if e.event_type == 'comment' && e.comment.user_id != info_request.user_id
+ last_comment_event = e if last_comment_event.nil?
+
+ alerted_for = e.user_info_request_sent_alerts.find(:first, :conditions => [ "alert_type = 'comment_1' and user_id = ?", info_request.user_id])
+ if alerted_for.nil?
+ count = count + 1
+ earliest_unalerted_comment_event = e
+ else
+ break
+ end
+ end
+ end
+
+ # Alert needs sending if there are new comments
+ if count > 0
+ store_sent = UserInfoRequestSentAlert.new
+ store_sent.info_request = info_request
+ store_sent.user = info_request.user
+ store_sent.alert_type = 'comment_1'
+ store_sent.info_request_event_id = last_comment_event.id
+ if count > 1
+ RequestMailer.comment_on_alert_plural(info_request, count, earliest_unalerted_comment_event.comment).deliver
+ elsif count == 1
+ RequestMailer.comment_on_alert(info_request, last_comment_event.comment).deliver
+ else
+ raise "internal error"
+ end
+ store_sent.save!
+ end
+ end
+ end
+
+
+end
+
+
diff --git a/app/mailers/track_mailer.rb b/app/mailers/track_mailer.rb
new file mode 100644
index 000000000..391143214
--- /dev/null
+++ b/app/mailers/track_mailer.rb
@@ -0,0 +1,135 @@
+# models/track_mailer.rb:
+# Emails which go to users who are tracking things.
+#
+# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved.
+# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
+
+class TrackMailer < ApplicationMailer
+ def event_digest(user, email_about_things)
+ @user, @email_about_things = user, email_about_things
+
+ post_redirect = PostRedirect.new(
+ :uri => user_url(user) + "#email_subscriptions",
+ :user_id => user.id)
+ post_redirect.save!
+ @unsubscribe_url = confirm_url(:email_token => post_redirect.email_token)
+
+ headers('Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834
+ 'Precedence' => 'bulk')# http://www.vbulletin.com/forum/project.php?issueid=27687 (Exchange hack)
+ # 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces for tracks
+ # (We let it return bounces for now, so we can manually kill the tracks that bounce so Yahoo
+ # etc. don't decide we are spammers.)
+
+ mail(:from => contact_from_name_and_email,
+ :to => user.name_and_email,
+ :subject => _("Your {{site_name}} email alert", :site_name => site_name))
+ end
+
+ def contact_from_name_and_email
+ "#{AlaveteliConfiguration::track_sender_name} <#{AlaveteliConfiguration::track_sender_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
+ # weeks.
+
+ # Useful query to run by hand to see how many alerts are due:
+ # User.find(:all, :conditions => [ "last_daily_track_email < ?", Time.now - 1.day ]).size
+ def self.alert_tracks
+ done_something = false
+ now = Time.now()
+ users = User.find(:all, :conditions => [ "last_daily_track_email < ?", now - 1.day ])
+ if users.empty?
+ return done_something
+ end
+ for user in users
+ next if !user.should_be_emailed? || !user.receive_email_alerts
+
+ 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
+ # What have we alerted on already?
+ #
+ # We only use track_things_sent_emails records which are less than 14 days old.
+ # In the search query loop below, we also only use items described in last 7 days.
+ # An item described that recently definitely can't appear in track_things_sent_emails
+ # earlier, so this is safe (with a week long margin of error). If the alerts break
+ # for a whole week, then they will miss some items. Tough.
+ done_info_request_events = {}
+ tt_sent = track_thing.track_things_sent_emails.find(:all, :conditions => ['created_at > ?', now - 14.days])
+ for t in tt_sent
+ if not t.info_request_event_id.nil?
+ done_info_request_events[t.info_request_event_id] = 1
+ end
+ end
+
+ # Query for things in this track. We use described_at for the
+ # ordering, so we catch anything new (before described), or
+ # anything whose new status has been described.
+ xapian_object = InfoRequest.full_search([InfoRequestEvent], track_thing.track_query, 'described_at', true, nil, 100, 1)
+ # Go through looking for unalerted things
+ alert_results = []
+ for result in xapian_object.results
+ if result[:model].class.to_s != "InfoRequestEvent"
+ raise "need to add other types to TrackMailer.alert_tracks (unalerted)"
+ end
+
+ next if track_thing.created_at >= result[:model].described_at # made before the track was created
+ next if result[:model].described_at < now - 7.days # older than 1 week (see 14 days / 7 days in comment above)
+ next if done_info_request_events.include?(result[:model].id) # definitely already done
+
+ # OK alert this one
+ alert_results.push(result)
+ end
+ # If there were more alerts for this track, then store them
+ if alert_results.size > 0
+ email_about_things.push([track_thing, alert_results, xapian_object])
+ end
+ end
+
+ # If we have anything to send, then send everything for the user in one mail
+ if email_about_things.size > 0
+ # Send the email
+
+ I18n.with_locale(user.get_locale) do
+ TrackMailer.event_digest(user, email_about_things).deliver
+ end
+ end
+
+ # Record that we've now sent those alerts to that user
+ for track_thing, alert_results in email_about_things
+ for result in alert_results
+ track_things_sent_email = TrackThingsSentEmail.new
+ track_things_sent_email.track_thing_id = track_thing.id
+ if result[:model].class.to_s == "InfoRequestEvent"
+ track_things_sent_email.info_request_event_id = result[:model].id
+ else
+ raise "need to add other types to TrackMailer.alert_tracks (mark alerted)"
+ end
+ track_things_sent_email.save!
+ end
+ end
+ user.last_daily_track_email = now
+ user.no_xapian_reindex = true
+ user.save!
+ done_something = true
+ end
+ return done_something
+ end
+
+ def self.alert_tracks_loop
+ # Run alert_tracks in an endless loop, sleeping when there is nothing to do
+ while true
+ sleep_seconds = 1
+ while !alert_tracks
+ sleep sleep_seconds
+ sleep_seconds *= 2
+ sleep_seconds = 300 if sleep_seconds > 300
+ end
+ end
+ end
+
+end
+
+
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
new file mode 100644
index 000000000..a351147f9
--- /dev/null
+++ b/app/mailers/user_mailer.rb
@@ -0,0 +1,44 @@
+# models/user_mailer.rb:
+# Emails relating to user accounts. e.g. Confirming a new account
+#
+# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved.
+# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
+
+class UserMailer < ApplicationMailer
+ def confirm_login(user, reasons, url)
+ @reasons, @name, @url = reasons, user.name, url
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email) # we don't care about bounces when people are fiddling with their account
+
+ mail(:from => contact_from_name_and_email,
+ :to => user.name_and_email,
+ :subject => reasons[:email_subject])
+ end
+
+ def already_registered(user, reasons, url)
+ @reasons, @name, @url = reasons, user.name, url
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email) # we don't care about bounces when people are fiddling with their account
+
+ mail(:from => contact_from_name_and_email,
+ :to => user.name_and_email,
+ :subject => reasons[:email_subject])
+ end
+
+ def changeemail_confirm(user, new_email, url)
+ @name, @url, @old_email, @new_email = user.name, url, user.email, new_email
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email) # we don't care about bounces when people are fiddling with their account
+
+ mail(:from => contact_from_name_and_email,
+ :to => new_email,
+ :subject => _("Confirm your new email address on {{site_name}}", :site_name => site_name))
+ end
+
+ def changeemail_already_used(old_email, new_email)
+ @old_email, @new_email = old_email, new_email
+ headers('Return-Path' => blackhole_email, 'Reply-To' => contact_from_name_and_email) # we don't care about bounces when people are fiddling with their account
+
+ mail(:from => contact_from_name_and_email,
+ :to => new_email,
+ :subject => _("Unable to change email address on {{site_name}}", :site_name=>site_name))
+ end
+end
+