Skip to content

Commit

Permalink
error reply for failed incoming email
Browse files Browse the repository at this point in the history
  • Loading branch information
machisuji committed May 27, 2022
1 parent 4c2d2d5 commit 1a5b0a4
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 23 deletions.
32 changes: 32 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,40 @@ def activation_limit_reached(user_email, admin)
send_mail(admin, t("mail_user_activation_limit_reached.subject"))
end

##
# E-Mail sent to a user when they tried sending an email to OpenProject to create or update
# a work package, or forum message for instance.
#
# @param [User] user User who sent the email
# @param [Mail] mail Sent email
# @param [Array<String>] List of logs collected during processing of the email
def incoming_email_error(user, mail, logs)
@user = user
@mail = mail
@logs = logs
@received_at = DateTime.now
@incoming_text = incoming_email_text mail
@quote = incoming_email_quote mail

headers['References'] = ["<#{mail.message_id}>"]
headers['In-Reply-To'] = ["<#{mail.message_id}>"]

send_mail user, mail.subject.present? ? "Re: #{mail.subject}" : I18n.t("mail_subject_incoming_email_error")
end

private

def incoming_email_text(mail)
mail.text_part.present? ? mail.text_part.body.to_s : mail.body.to_s
end

def incoming_email_quote(mail)
quote = incoming_email_text(mail)
quoted = String(quote).lines.join("> ")

"> #{quoted}"
end

def open_project_wiki_headers(wiki_content)
open_project_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id,
Expand Down
36 changes: 25 additions & 11 deletions app/models/mail_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ class UnauthorizedAction < StandardError; end

class MissingInformation < StandardError; end

attr_reader :email, :sender_email, :user, :options
attr_reader :email, :sender_email, :user, :options, :logs

def initialize
@result = false
@logs = []
end

##
# Code copied from base class and extended with optional options parameter
Expand Down Expand Up @@ -70,7 +75,8 @@ def receive(email)
@sender_email = email.from.to_a.first.to_s.strip
# Ignore emails received from the application emission address to avoid hell cycles
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
log "ignoring email from emission address [#{sender_email}]"
log "ignoring email from emission address [#{sender_email}]", report: false
# don't report back errors to ourselves
return false
end
# Ignore auto generated emails
Expand All @@ -79,7 +85,8 @@ def receive(email)
if value
value = value.to_s.downcase
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
log "ignoring email with #{key}:#{value} header"
log "ignoring email with #{key}:#{value} header", report: false
# no point reporting back in case of auto-generated emails
return false
end
end
Expand Down Expand Up @@ -112,6 +119,8 @@ def receive(email)
end
User.current = @user
dispatch
ensure
report_errors if !@result && Setting.report_incoming_email_errors?
end

def options=(value)
Expand Down Expand Up @@ -151,14 +160,11 @@ def options=(value)
# Relying on the subject of the mail, which had been implemented before, is brittle as it relies on the user not altering
# the subject. Additionally, the subject structure might change, e.g. via localization changes.
def dispatch
if (m, object_id = dispatch_target_from_header)
m.call(object_id)
else
dispatch_to_default
end
m, object_id = dispatch_target_from_header

@result = m ? m.call(object_id) : dispatch_to_default
rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user
logger&.error e.message
log "could not save record: #{e.message}", :error
false
rescue MissingInformation => e
log "missing information from #{user}: #{e.message}", :error
Expand Down Expand Up @@ -597,11 +603,19 @@ def wp_done_ratio_from_keyword
get_keyword(:done_ratio, override: true, format: '(\d|10)?0')
end

def log(message, level = :info)
def log(message, level = :info, report: true)
logs << "#{level}: #{message}" if report

message = "MailHandler: #{message}"
logger.public_send(level, message)
end

def report_errors
return if logs.empty?

UserMailer.incoming_email_error(user, email, logs).deliver_later
end

def work_package_create_contract_class
if options[:no_permission_check]
CreateWorkPackageWithoutAuthorizationsContract
Expand Down
50 changes: 50 additions & 0 deletions app/views/user_mailer/incoming_email_error.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2022 the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>

<h2><%= t(:mail_body_incoming_email_error) %></h2>

<p>
<%= t(:mail_body_incoming_email_error_logs) %>:
</p>

<code>
<%= @logs.join("\n") %>
</code>

<p>
<%= t(
:mail_body_incoming_email_error_in_reply_to,
received_at: format_time(@received_at),
from_email: @mail.from.first
)%>:
</p>

<blockquote>
<%= @incoming_text %>
</blockquote>
42 changes: 42 additions & 0 deletions app/views/user_mailer/incoming_email_error.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2022 the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>

<%= t(:mail_body_incoming_email_error) %>

<%= t(:mail_body_incoming_email_error_logs) %>:

<%= @logs.join("\n") %>

<%= t(
:mail_body_incoming_email_error_in_reply_to,
received_at: format_time(@received_at),
from_email: @mail.from.first
)%>:

<%= @quote %>
2 changes: 2 additions & 0 deletions config/constants/settings/definitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@
},
writable: false

add :report_incoming_email_errors, default: true

add :repositories_automatic_managed_vendor,
default: nil,
format: :string,
Expand Down
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,9 @@ en:
mail_body_backup_token_info: The previous token is no longer valid.
mail_body_backup_waiting_period: The new token will be enabled in %{hours} hours.
mail_body_backup_token_warning: If this wasn't you, login to OpenProject immediately and reset it again.
mail_body_incoming_email_error: The email you sent to OpenProject could not be processed.
mail_body_incoming_email_error_in_reply_to: "At %{received_at} %{from_email} wrote"
mail_body_incoming_email_error_logs: "Logs"
mail_body_lost_password: "To change your password, click on the following link:"
mail_body_register: "Welcome to %{app_title}. Please activate your account by clicking on this link:"
mail_body_register_header_title: "Project member invitation email"
Expand All @@ -2088,6 +2091,7 @@ en:
mail_subject_account_activation_request: "%{value} account activation request"
mail_subject_backup_ready: "Your backup is ready"
mail_subject_backup_token_reset: "Backup token reset"
mail_subject_incoming_email_error: "An email you sent to OpenProject could not be processed"
mail_subject_lost_password: "Your %{value} password"
mail_subject_register: "Your %{value} account activation"
mail_subject_reminder: "%{count} work package(s) due in the next %{days} days"
Expand Down
51 changes: 51 additions & 0 deletions spec/mailers/user_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,57 @@
end
end

describe '#incoming_email_error' do
let(:logs) { ['info: foo', 'error: bar'] }
let(:recipient) { user }
let(:current_time) { "2022-11-03 9:15".to_time }
let(:incoming_email) do
Mail.new(subject: subject, message_id: message_id, body: body, from: from_email )
end

let(:subject) { 'New work package 42' }
let(:message_id) { '<000501c8d452$a95cd7e0$0a00a8c0@osiris>' }
let(:from_email) { 'l.lustig@openproject.com' }
let(:body) { "Project: demo-project" }

let(:outgoing_email) { deliveries.first }

before do
described_class
.incoming_email_error(user, incoming_email, logs)
.deliver_now
end

it_behaves_like 'mail is sent' do
it "references the incoming email's subject in its own" do
expect(outgoing_email.subject).to eql "Re: #{subject}"
end

it "it's a reply to the incoming email" do
expect(message_id).to include outgoing_email.in_reply_to
expect(message_id).to include outgoing_email.references
end

it "contains the incoming email's quoted content" do
expect(html_body).to include body
end

it 'contains the date the mail was received' do
expect(html_body).to include "11/03/2022 09:15 AM"
end

it 'contains the email address from which the email was sent' do
expect(html_body).to include from_email
end

it 'contains the logs' do
logs.each do |log|
expect(html_body).to include log
end
end
end
end

describe '#news_added' do
let(:news) { build_stubbed(:news) }

Expand Down
Loading

0 comments on commit 1a5b0a4

Please sign in to comment.