app/mailers/request_mailer.rb
# 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
include AlaveteliFeatures::Helpers
before_action :set_footer_template,
only: [
:new_response, :overdue_alert, :very_overdue_alert,
:new_response_reminder_alert, :old_unclassified_updated,
:not_clarified_alert, :comment_on_alert,
:comment_on_alert_plural
]
# 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,
subject: info_request.email_subject_followup(html: false))
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
@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)
@info_request = info_request
@message = message
set_reply_to_headers(nil, 'Reply-To' => user.name_and_email)
# From is an address we control so that strict DMARC senders don't get refused
mail(from: MailHandler.address_from_name_and_email(
user.name,
blackhole_email
),
to: contact_for_user(user),
subject: _("FOI response requires admin ({{reason}}) - " \
"{{request_title}}",
reason: info_request.described_state,
request_title: info_request.title.html_safe))
end
# Tell the requester that a new response has arrived
def new_response(info_request, incoming_message)
@incoming_message = incoming_message
@info_request = info_request
set_reply_to_headers(info_request.user)
set_auto_generated_headers
mail(
from: contact_for_user(info_request.user),
to: info_request.user.name_and_email,
subject: _("New response to your FOI request - {{request_title}}",
request_title: info_request.title.html_safe),
charset: "UTF-8"
)
end
# Tell the requester that the public body is late in replying
def overdue_alert(info_request, user)
@url = respond_to_last_url(info_request)
@info_request = info_request
set_reply_to_headers(user)
set_auto_generated_headers
mail_user(
user,
_("Delayed response to your FOI request - {{request_title}}",
request_title: 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)
@url = respond_to_last_url(info_request)
@info_request = info_request
set_reply_to_headers(user)
set_auto_generated_headers
mail_user(
user,
_("You're long overdue a response to your FOI request - {{request_title}}",
request_title: 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)
target = show_request_url(info_request.url_title,
anchor: 'describe_state_form_1',
only_path: true)
@url = signin_url(r: target)
@incoming_message = incoming_message
@info_request = info_request
set_reply_to_headers(info_request.user)
set_auto_generated_headers
mail_user(
info_request.user,
_("Please update the status of your request - {{request_title}}",
request_title: info_request.title.html_safe)
)
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
set_reply_to_headers(info_request.user)
set_auto_generated_headers
mail_user(info_request.user, _("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 = new_request_incoming_followup_url(
info_request.url_title,
incoming_message_id: incoming_message.id,
anchor: 'followup'
)
@url = respond_url
@incoming_message = incoming_message
@info_request = info_request
set_reply_to_headers(info_request.user)
set_auto_generated_headers
mail_user(
info_request.user,
_("Clarify your FOI request - {{request_title}}",
request_title: info_request.title.html_safe)
)
end
# Tell requester that somebody add an annotation to their request
def comment_on_alert(info_request, comment)
@comment = comment
@info_request = info_request
@url = comment_url(comment)
set_reply_to_headers(info_request.user)
set_auto_generated_headers
mail_user(
info_request.user,
_("Somebody added a note to your FOI request - {{request_title}}",
request_title: info_request.title.html_safe)
)
end
# Tell requester that somebody added annotations to more than one of
# their requests
def comment_on_alert_plural(info_request, count, earliest_unalerted_comment)
@count = count
@info_request = info_request
@url = comment_url(earliest_unalerted_comment)
set_reply_to_headers(info_request.user)
set_auto_generated_headers
mail_user(
info_request.user,
_("Some notes have been added to your FOI request - {{request_title}}",
request_title: 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, source = :mailin)
unless logger.nil?
logger.debug "Received mail from #{source}:\n #{raw_email}"
end
mail = MailHandler.mail_from_raw_email(raw_email)
new.receive(mail, raw_email, source)
end
# Find which info requests the email is for
def requests_matching_email(email)
addresses = MailHandler.get_all_addresses(email)
InfoRequest.matching_incoming_email(addresses)
end
def send_to_holding_pen(email, raw_email, opts)
opts[:rejected_reason] =
_("Could not identify the request from the email address")
request = InfoRequest.holding_pen_request
request.receive(email, raw_email, opts)
end
# Member function, called on the new class made in self.receive above
def receive(email, raw_email, source = :mailin)
opts = { source: source }
# Only check mail that doesn't have spam in the header
return if SpamAddress.spam?(MailHandler.get_all_addresses(email))
# Find exact matches for info requests
exact_info_requests = requests_matching_email(email)
if exact_info_requests.count > 0
# Go through each exact info request and deliver the email
exact_info_requests.each do |info_request|
info_request.receive(email, raw_email, opts)
end
return
end
# If there are no exact matches, find any guessed requests
guessed_info_requests = Guess.guessed_info_requests(email)
if guessed_info_requests.count == 1
# If there one guess automatically redeliver the email to that and log it
# as an event
info_request = guessed_info_requests.first
info_request.log_event(
'redeliver_incoming',
editor: 'automatic',
destination_request: info_request
)
info_request.receive(email, raw_email, opts)
else
# Otherwise we send the mail to the holding pen
send_to_holding_pen(email, raw_email, opts)
end
end
# Send email alerts for overdue requests
def self.alert_overdue_requests
info_requests = InfoRequest.where("described_state = 'waiting_response'
AND awaiting_description = ?
AND user_id is not null
AND use_notifications = ?
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, false).includes(:user)
info_requests.each do |info_request|
alert_event_id = info_request.last_event_forming_initial_request.id
# Only overdue requests
calculated_status = info_request.calculate_status
if %w[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.
where("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).
first
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_now
elsif calculated_status == 'waiting_response_very_overdue'
RequestMailer.
very_overdue_alert(
info_request,
info_request.user
).deliver_now
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|
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.
where_old_unclassified(days_since).
where(use_notifications: false).
order(:id).
includes(:user)
info_requests.each do |info_request|
alert_event_id = info_request.get_last_public_response_event_id
last_response_message = info_request.get_last_public_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.
where("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).
first
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
# TODO: uses same template for reminder 1 and reminder 2 right now.
RequestMailer.
new_response_reminder_alert(
info_request,
last_response_message
).deliver_now
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.
where("awaiting_description = ?
AND described_state = 'waiting_clarification'
AND info_requests.updated_at < ?
AND use_notifications = ?",
false,
Time.zone.now - 3.days,
false
).
includes(:user).order(:id)
info_requests.each do |info_request|
alert_event_id = info_request.get_last_public_response_event_id
last_response_message = info_request.get_last_public_response
next if alert_event_id.nil?
# To the user who created the request
sent_already = UserInfoRequestSentAlert.
where("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).
first
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_now
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)
# TODO: 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.
conditions = <<-EOF.strip_heredoc
info_requests.id in (
SELECT info_request_id
FROM info_request_events
WHERE event_type = 'comment'
AND created_at > (now() - '1 month'::interval)
)
EOF
info_requests =
InfoRequest.
includes(info_request_events: :user_info_request_sent_alerts).
where(conditions).
order(:id).merge(InfoRequestEvent.order(:created_at)).
references(:info_request_events)
info_requests.each do |info_request|
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
info_request.info_request_events.reverse.each do |e|
# 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.
where("alert_type = 'comment_1'
AND user_id = ?",
info_request.user_id).
first
if alerted_for.nil?
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_now
elsif count == 1
RequestMailer.comment_on_alert(
info_request,
last_comment_event.comment
).deliver_now
else
raise "internal error"
end
store_sent.save!
end
end
end
private
def set_footer_template
@footer_template = 'default'
end
end