Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions app/controllers/forms/check_your_answers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,19 @@ def submit_answers
return render template: "errors/incomplete_submission", locals: { form: @form, current_context: }
end

submission_reference = FormSubmissionService.call(current_context:,
email_confirmation_input:,
mode:).submit
begin
submission_reference = FormSubmissionService.call(current_context:,
email_confirmation_input:,
mode:).submit

current_context.save_submission_details(submission_reference, requested_email_confirmation)
current_context.save_submission_details(submission_reference, requested_email_confirmation)

redirect_to :form_submitted
redirect_to :form_submitted
rescue FormSubmissionService::ConfirmationEmailToAddressError
setup_check_your_answers
email_confirmation_input.errors.add(:confirmation_email_address, :invalid_email)
render template: "forms/check_your_answers/show", locals: { email_confirmation_input: }, status: :unprocessable_content
end
rescue StandardError => e
log_rescued_exception(e)

Expand Down
28 changes: 25 additions & 3 deletions app/services/form_submission_service.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
class FormSubmissionService
include RedactionUtils

class ConfirmationEmailToAddressError < StandardError; end

class << self
def call(**args)
new(**args)
Expand All @@ -20,8 +24,10 @@ def initialize(current_context:, email_confirmation_input:, mode:)

def submit
validate_submission

confirmation_mail = setup_confirmation_email if requested_confirmation?
deliver_submission
send_confirmation_email if requested_confirmation?
send_confirmation_email(confirmation_mail) if confirmation_mail.present?

@submission_reference
end
Expand Down Expand Up @@ -83,15 +89,31 @@ def submission_timestamp
Time.use_zone(time_zone) { Time.zone.now }
end

def send_confirmation_email
def setup_confirmation_email
mail = FormSubmissionConfirmationMailer.send_confirmation_email(
what_happens_next_markdown: @form.what_happens_next_markdown,
support_contact_details: @form.support_details,
notify_response_id: @email_confirmation_input.confirmation_email_reference,
confirmation_email_address: @email_confirmation_input.confirmation_email_address,
mailer_options:,
).deliver_now
)

if mail.message.errors.any?
to_address_error = mail.message.errors.select { |error| error[0] == "To" }.first
if to_address_error
redacted_error = redact_emails_from_sentry_message(to_address_error[2].to_s)
Sentry.capture_message("ActionMailer error for To email address in confirmation email", extra: {
action_mailer_error: redacted_error,
})
raise ConfirmationEmailToAddressError
end
end

mail
end

def send_confirmation_email(mail)
mail.deliver_now
CurrentRequestLoggingAttributes.confirmation_email_id = mail.govuk_notify_response.id
end

Expand Down
12 changes: 12 additions & 0 deletions lib/redaction_utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module RedactionUtils
def redact_emails_from_sentry_message(input)
input.gsub(/\S+@\S+/) do |match|
# Redact all alphanumeric characters not directly after a non-alphanumeric character.
# The idea is so we can identify if any special characters or escape sequences are causing the issue with
# ActionMailer parsing emails.
# Also replace the @ with (at) so that Sentry doesn't completely strip out the email
match.gsub(/(?<=[A-Za-z0-9])([A-Za-z0-9])/, "*")
.gsub(/@/, "(at)")
end
end
end
10 changes: 10 additions & 0 deletions spec/lib/redaction_utils_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require "rails_helper"

RSpec.describe RedactionUtils, type: :helper do
describe "#redact_emails_from_sentry_message" do
it "redacts all emails in the string, keeping special characters and characters following special characters" do
expect(helper.redact_emails_from_sentry_message("some text an_email$123@example.com and another^email@example.com"))
.to eq "some text a*_e****$1**(at)e******.c** and a******^e****(at)e******.c**"
end
end
end
22 changes: 22 additions & 0 deletions spec/requests/forms/check_your_answers_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,28 @@

include_examples "for notification references"
end

context "when there is an ActionMailer error with the confirmation email address" do
before do
mock_form_submission_service = instance_double(FormSubmissionService)
allow(FormSubmissionService).to receive(:new).and_return(mock_form_submission_service)
allow(mock_form_submission_service).to receive(:submit).and_raise(FormSubmissionService::ConfirmationEmailToAddressError)

post form_submit_answers_path(2, "form-name", 1, mode:), params: { email_confirmation_input: }
end

it "return 422 error code" do
expect(response).to have_http_status(:unprocessable_content)
end

it "renders the check your answers page" do
expect(response).to render_template("forms/check_your_answers/show")
end

it "has a validation error for the confirmation email address" do
expect(response.body).to include(I18n.t("activemodel.errors.models.email_confirmation_input.attributes.confirmation_email_address.invalid_email"))
end
end
end

private
Expand Down
32 changes: 31 additions & 1 deletion spec/services/form_submission_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
subject(:service) { described_class.call(current_context:, email_confirmation_input:, mode:) }

let(:mode) { Mode.new }
let(:email_confirmation_input) { build :email_confirmation_input_opted_in }
let(:confirmation_email_address) { "testing@gov.uk" }
let(:email_confirmation_input) { build :email_confirmation_input_opted_in, confirmation_email_address: }

let(:form) do
build(:form,
Expand Down Expand Up @@ -243,6 +244,35 @@
end
end

context "when the to email address is rejected by ActionMailer" do
let(:confirmation_email_address) { "rejected-email@gov.uk\n" }

it "raises a ConfirmationEmailToAddressError" do
expect {
service.submit
}.to raise_error(FormSubmissionService::ConfirmationEmailToAddressError)
end

it "sends an error to Sentry" do
expect(Sentry).to receive(:capture_message).with("ActionMailer error for To email address in confirmation email", {
extra: {
action_mailer_error: /Mail::AddressList can not parse |r\*\*\*\*\*\*\*-e\*\*\*\*(at)g\*\*.u\*\n|: Only able to parse up to "r\*\*\*\*\*\*\*-e\*\*\*\*@g\*\*.u\*\\/,
},
})
service.submit
rescue FormSubmissionService::ConfirmationEmailToAddressError
nil
end

it "does not queue sending the submission email" do
assert_no_enqueued_jobs do
service.submit
rescue FormSubmissionService::ConfirmationEmailToAddressError
nil
end
end
end

context "when user does not want a confirmation email" do
let(:email_confirmation_input) { build :email_confirmation_input }

Expand Down