diff options
Diffstat (limited to 'app/mailers')
-rw-r--r-- | app/mailers/application_mailer.rb | 153 | ||||
-rw-r--r-- | app/mailers/contact_mailer.rb | 56 | ||||
-rw-r--r-- | app/mailers/outgoing_mailer.rb | 98 | ||||
-rw-r--r-- | app/mailers/request_mailer.rb | 459 | ||||
-rw-r--r-- | app/mailers/track_mailer.rb | 134 | ||||
-rw-r--r-- | app/mailers/user_mailer.rb | 48 |
6 files changed, 948 insertions, 0 deletions
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 000000000..84b045795 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,153 @@ +# models/application_mailer.rb: +# Shared code between different mailers. +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: francis@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 + Configuration::blackhole_prefix+"@"+Configuration::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 + + # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer + # will be initialized according to the named method. If not, the mailer will + # remain uninitialized (useful when you only need to invoke the "receive" + # method, for instance). + + # TEMPORARY: commented out method below while upgrading to Rails 3 + #def initialize(method_name=nil, *parameters) #:nodoc: + # create!(method_name, *parameters) if method_name + #end + + # For each multipart template (e.g. "the_template_file.text.html.erb") available, + # add the one from the view path with the highest priority as a part to the mail + def render_multipart_templates + added_content_types = {} + self.view_paths.each do |view_path| + Dir.glob("#{view_path}/#{mailer_name}/#{@template}.*").each do |path| + template = view_path["#{mailer_name}/#{File.basename(path)}"] + + # Skip unless template has a multipart format + next unless template && template.multipart? + next if added_content_types[template.content_type] == true + @parts << Part.new( + :content_type => template.content_type, + :disposition => "inline", + :charset => charset, + :body => render_message(template, @body) + ) + added_content_types[template.content_type] = true + end + end + end + + # Look for the current template in each element of view_paths in order, + # return the first + def find_template + self.view_paths.each do |view_path| + if template = view_path["#{mailer_name}/#{@template}"] + return template + end + end + return nil + end + + # FIXME: This check was disabled temporarily during the Rails 3 upgrade + if ActionMailer::VERSION::MAJOR == 2 + + # This method is a customised version of ActionMailer::Base.create! + # modified to allow templates to be selected correctly for multipart + # mails when themes have added to the view_paths. The problem from our + # point of view with ActionMailer::Base is that it sets template_root to + # the first element of the view_paths array and then uses only that (directly + # and via template_path, which is created from it) in the create! method when + # looking for templates. Our modified version looks for templates in the view_paths + # in order. + # Changed lines marked with *** + + # Initialize the mailer via the given +method_name+. The body will be + # rendered and a new TMail::Mail object created. + def create!(method_name, *parameters) #:nodoc: + initialize_defaults(method_name) + __send__(method_name, *parameters) + + # If an explicit, textual body has not been set, we check assumptions. + unless String === @body + # First, we look to see if there are any likely templates that match, + # which include the content-type in their file name (i.e., + # "the_template_file.text.html.erb", etc.). Only do this if parts + # have not already been specified manually. + if @parts.empty? + # *** render_multipart_templates replaces the following code + # Dir.glob("#{template_path}/#{@template}.*").each do |path| + # template = template_root["#{mailer_name}/#{File.basename(path)}"] + # + # # Skip unless template has a multipart format + # next unless template && template.multipart? + # + # @parts << Part.new( + # :content_type => template.content_type, + # :disposition => "inline", + # :charset => charset, + # :body => render_message(template, @body) + # ) + # end + render_multipart_templates + + unless @parts.empty? + @content_type = "multipart/alternative" if @content_type !~ /^multipart/ + @parts = sort_parts(@parts, @implicit_parts_order) + end + end + + # Then, if there were such templates, we check to see if we ought to + # also render a "normal" template (without the content type). If a + # normal template exists (or if there were no implicit parts) we render + # it. + template_exists = @parts.empty? + + # *** find_template replaces template_root call + # template_exists ||= template_root["#{mailer_name}/#{@template}"] + template_exists ||= find_template + + @body = render_message(@template, @body) if template_exists + + # Finally, if there are other message parts and a textual body exists, + # we shift it onto the front of the parts and set the body to nil (so + # that create_mail doesn't try to render it in addition to the parts). + if !@parts.empty? && String === @body + @parts.unshift ActionMailer::Part.new(:charset => charset, :body => @body) + @body = nil + end + end + + # If this is a multipart e-mail add the mime_version if it is not + # already set. + @mime_version ||= "1.0" if !@parts.empty? + + # build the mail object itself + @mail = create_mail + end + else + #raise "ApplicationMailer.create! is obsolete - find another way to ensure that themes can override mail templates for multipart mails" + end + +end + diff --git a/app/mailers/contact_mailer.rb b/app/mailers/contact_mailer.rb new file mode 100644 index 000000000..abde64928 --- /dev/null +++ b/app/mailers/contact_mailer.rb @@ -0,0 +1,56 @@ +# models/contact_mailer.rb: +# Sends contact form mails. +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: francis@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) + @from = name + " <" + email + ">" + @recipients = contact_from_name_and_email + @subject = subject + @body = { :message => message, + :logged_in_user => logged_in_user , + :last_request => last_request, + :last_body => last_body + } + 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) + @from = from_user.name_and_email + # 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 + @recipients = recipient_user.name_and_email + @subject = subject + @body = { + :message => message, + :from_user => from_user, + :recipient_user => recipient_user, + :from_user_url => from_user_url + } + end + + # Send message to a user from the administrator + def from_admin_message(recipient_user, subject, message) + @from = contact_from_name_and_email + @recipients = recipient_user.name_and_email + @subject = subject + @body = { + :message => message, + :from_user => @from, + :recipient_user => recipient_user, + } + bcc Configuration::contact_email + end + +end diff --git a/app/mailers/outgoing_mailer.rb b/app/mailers/outgoing_mailer.rb new file mode 100644 index 000000000..503166b8a --- /dev/null +++ b/app/mailers/outgoing_mailer.rb @@ -0,0 +1,98 @@ +# 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: francis@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) + @wrap_lines_as_paragraphs = true + @from = info_request.incoming_name_and_email + @recipients = info_request.recipient_name_and_email + @subject = info_request.email_subject_request + @headers["message-id"] = OutgoingMailer.id_for_message(outgoing_message) + @body = {:info_request => info_request, :outgoing_message => outgoing_message, + :contact_email => Configuration::contact_email } + end + + # Later message to public body regarding existing request + def followup(info_request, outgoing_message, incoming_message_followup) + @wrap_lines_as_paragraphs = true + @from = info_request.incoming_name_and_email + @recipients = OutgoingMailer.name_and_email_for_followup(info_request, incoming_message_followup) + @subject = OutgoingMailer.subject_for_followup(info_request, outgoing_message) + @headers["message-id"] = OutgoingMailer.id_for_message(outgoing_message) + @body = {:info_request => info_request, :outgoing_message => outgoing_message, + :incoming_message_followup => incoming_message_followup, + :contact_email => Configuration::contact_email } + end + + # XXX the condition checking valid_to_reply_to? also appears in views/request/_followup.rhtml, + # 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 += "@" + Configuration::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..116a9fe81 --- /dev/null +++ b/app/mailers/request_mailer.rb @@ -0,0 +1,459 @@ +# models/request_mailer.rb: +# Alerts relating to requests. +# +# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. +# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ + +require 'alaveteli_file_types' + +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, body, attachment_name, attachment_content) + @from = from_user.name_and_email + @recipients = info_request.incoming_name_and_email + @body = { + :body => body + } + if !attachment_name.nil? && !attachment_content.nil? + content_type = AlaveteliFileTypes.filename_to_mimetype(attachment_name) || 'application/octet-stream' + + attachment :content_type => content_type, + :body => attachment_content, + :filename => attachment_name + end + end + + # Used when a response is uploaded using the API + def external_response(info_request, body, sent_at, attachments) + @from = blackhole_email + @recipients = info_request.incoming_name_and_email + @body = { :body => body } + + # ActionMailer only works properly when the time is in the local timezone: + # see https://rails.lighthouseapp.com/projects/8994/tickets/3113-actionmailer-only-works-correctly-with-sent_on-times-that-are-in-the-local-time-zone + @sent_on = sent_at.dup.localtime + + attachments.each do |attachment_hash| + attachment attachment_hash + end + 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 = Configuration::contact_email + + mail(:from => contact_from_name_and_email, :to => email.from_addrs[0].to_s, + :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) + if !set_by.nil? + user = set_by + else + user = info_request.user + end + @from = user.name_and_email + @recipients = contact_from_name_and_email + @subject = _("FOI response requires admin ({{reason}}) - {{title}}", :reason => info_request.described_state, :title => info_request.title) + url = main_url(request_url(info_request)) + admin_url = request_admin_url(info_request) + @body = {:reported_by => user, :info_request => info_request, :url => url, :admin_url => admin_url } + 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 = main_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_url, + :user_id => user.id) + post_redirect.save! + url = confirm_url(:email_token => post_redirect.email_token) + + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = user.name_and_email + @subject = _("Delayed response to your FOI request - ") + info_request.title + @body = { :info_request => info_request, :url => url } + 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_url, + :user_id => user.id) + post_redirect.save! + url = confirm_url(:email_token => post_redirect.email_token) + + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = user.name_and_email + @subject = _("You're long overdue a response to your FOI request - ") + info_request.title + @body = { :info_request => info_request, :url => url } + 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 => main_url(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) + + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = info_request.user.name_and_email + @subject = _("Was the response you got to your FOI request any good?") + @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } + end + + # Tell the requester that someone updated their old unclassified request + def old_unclassified_updated(info_request) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = info_request.user.name_and_email + @subject = _("Someone has updated the status of your request") + url = main_url(request_url(info_request)) + @body = {:info_request => info_request, :url => url} + 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) + + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = info_request.user.name_and_email + @subject = _("Clarify your FOI request - ") + info_request.title + @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } + end + + # Tell requester that somebody add an annotation to their request + def comment_on_alert(info_request, comment) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = info_request.user.name_and_email + @subject = _("Somebody added a note to your FOI request - ") + info_request.title + @body = { :comment => comment, :info_request => info_request, :url => main_url(comment_url(comment)) } + end + def comment_on_alert_plural(info_request, count, earliest_unalerted_comment) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # 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' + @recipients = info_request.user.name_and_email + @subject = _("Some notes have been added to your FOI request - ") + info_request.title + @body = { :count => count, :info_request => info_request, :url => main_url(comment_url(earliest_unalerted_comment)) } + 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.deliver_overdue_alert(info_request, info_request.user) + elsif calculated_status == 'waiting_response_very_overdue' + RequestMailer.deliver_very_overdue_alert(info_request, info_request.user) + 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 + Configuration::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.deliver_new_response_reminder_alert(info_request, last_response_message) + 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.deliver_not_clarified_alert(info_request, last_response_message) + 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.deliver_comment_on_alert_plural(info_request, count, earliest_unalerted_comment_event.comment) + elsif count == 1 + RequestMailer.deliver_comment_on_alert(info_request, last_comment_event.comment) + 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..03310478a --- /dev/null +++ b/app/mailers/track_mailer.rb @@ -0,0 +1,134 @@ +# models/track_mailer.rb: +# Emails which go to users who are tracking things. +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ + +class TrackMailer < ApplicationMailer + def event_digest(user, email_about_things) + post_redirect = PostRedirect.new( + :uri => main_url(user_url(user)) + "#email_subscriptions", + :user_id => user.id) + post_redirect.save! + unsubscribe_url = confirm_url(:email_token => post_redirect.email_token) + + @from = contact_from_name_and_email + 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.) + + @recipients = user.name_and_email + @subject = _("Your {{site_name}} email alert", :site_name => site_name) + @body = { :user => user, :email_about_things => email_about_things, :unsubscribe_url => unsubscribe_url } + end + + def contact_from_name_and_email + "#{Configuration::track_sender_name} <#{Configuration::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.deliver_event_digest(user, email_about_things) + 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..1be4f8aa3 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,48 @@ +# 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: francis@mysociety.org; WWW: http://www.mysociety.org/ + +class UserMailer < ApplicationMailer + def confirm_login(user, reasons, url) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account + @recipients = user.name_and_email + @subject = reasons[:email_subject] + @body[:reasons] = reasons + @body[:name] = user.name + @body[:url] = url + end + + def already_registered(user, reasons, url) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account + @recipients = user.name_and_email + @subject = reasons[:email_subject] + @body[:reasons] = reasons + @body[:name] = user.name + @body[:url] = url + end + + def changeemail_confirm(user, new_email, url) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account + @recipients = new_email + @subject = _("Confirm your new email address on {{site_name}}", :site_name=>site_name) + @body[:name] = user.name + @body[:url] = url + @body[:old_email] = user.email + @body[:new_email] = new_email + end + + def changeemail_already_used(old_email, new_email) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account + @recipients = new_email + @subject = _("Unable to change email address on {{site_name}}", :site_name=>site_name) + @body[:old_email] = old_email + @body[:new_email] = new_email + end +end + |