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
26 changes: 26 additions & 0 deletions app/controllers/undeliverable_address_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class UndeliverableAddressController < ApplicationController
skip_before_action :verify_authenticity_token

def create
authorize do
UndeliverableAddressNotifier.new.call

render plain: 'ok'
end
end

private

def authorize
# Check for empty to make sure that the token is configured
if authorization_token && authorization_token == Figaro.env.usps_download_token
yield
else
head :unauthorized
end
end

def authorization_token
request.headers['X-API-AUTH-TOKEN']
end
end
4 changes: 4 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ def account_reset_cancel(email_address)
def please_reset_password(email_address)
mail(to: email_address.email, subject: t('user_mailer.please_reset_password.subject'))
end

def undeliverable_address(email_address)
mail(to: email_address.email, subject: t('user_mailer.undeliverable_address.subject'))
end
end
19 changes: 19 additions & 0 deletions app/models/usps_confirmation_code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,23 @@ def self.first_with_otp(otp)
def expired?
code_sent_at < Figaro.env.usps_confirmation_max_days.to_i.days.ago
end

def safe_update_bounced_at_and_send_notification
with_lock do
return if bounced_at
update_bounced_at_and_send_notification
end
true
end

def update_bounced_at_and_send_notification
update(bounced_at: Time.zone.now)
self.class.send_email(profile.user)
end

def self.send_email(user)
user.confirmed_email_addresses.each do |email_address|
UserMailer.undeliverable_address(email_address).deliver_later
end
end
end
59 changes: 59 additions & 0 deletions app/services/undeliverable_address_notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class UndeliverableAddressNotifier
TEMP_FILE_BASENAME = 'usps_bounced'.freeze

def call
temp_file = download_file
notifications_sent = process_file(temp_file)
cleanup(temp_file)
notifications_sent
end

private

attr_accessor :ucc

def download_file
file = Tempfile.new(TEMP_FILE_BASENAME)
Net::SFTP.start(*sftp_config) do |sftp|
sftp.download!(Figaro.env.usps_download_sftp_directory, file.path)
end
file
end

def cleanup(file)
file.close
file.unlink
end

def process_file(file)
notifications_sent = 0
File.readlines(file.path).each do |line|
code = line.chomp
sent = process_code(code)
notifications_sent += 1 if sent
end
notifications_sent
end

def process_code(otp)
ucc = usps_confirmation_code(otp)
ucc&.safe_update_bounced_at_and_send_notification
end

def sftp_config
[
env.usps_download_sftp_host,
env.usps_download_sftp_username,
password: env.usps_download_sftp_password,
timeout: env.usps_download_sftp_timeout.to_i,
]
end

def env
Figaro.env
end

def usps_confirmation_code(otp)
@ucc ||= UspsConfirmationCode.find_by(otp_fingerprint: Pii::Fingerprinter.fingerprint(otp))
end
end
18 changes: 18 additions & 0 deletions app/views/user_mailer/undeliverable_address.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
p.lead == t('.intro', app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'))

p.lead == t('.call_to_action')

table.spacer
tbody
tr
td.s10 height="10px"
| &nbsp;
table.hr
tr
th
| &nbsp;

p == t('.help',
app: link_to(APP_NAME, Figaro.env.mailer_domain_name, class: 'gray'),
help_link: link_to(t('user_mailer.help_link_text'), MarketingSite.help_url),
contact_link: link_to(t('user_mailer.contact_link_text'), MarketingSite.contact_url))
16 changes: 16 additions & 0 deletions config/application.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ dashboard_url: 'https://dashboard.demo.login.gov'
valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3"]'

twilio_timeout: '5'
usps_download_sftp_timeout: '5'
usps_upload_sftp_timeout: '5'

development:
Expand Down Expand Up @@ -194,6 +195,11 @@ development:
usps_confirmation_max_days: '10'
enable_i18n_mode: 'false'
enable_load_testing_mode: 'false'
usps_download_sftp_directory: '/undeliverable'
usps_download_sftp_host: 'localhost'
usps_download_sftp_username: 'brady'
usps_download_sftp_password: 'test'
usps_download_token: '123ABC'
usps_upload_sftp_directory: '/gsa_order'
usps_upload_sftp_host: 'localhost'
usps_upload_sftp_username: 'brady'
Expand Down Expand Up @@ -313,6 +319,11 @@ production:
usps_confirmation_max_days: '30'
enable_i18n_mode: 'false'
enable_load_testing_mode: 'false'
usps_download_sftp_directory:
usps_download_sftp_host:
usps_download_sftp_username:
usps_download_sftp_password:
usps_download_token:
usps_upload_sftp_directory:
usps_upload_sftp_host:
usps_upload_sftp_username:
Expand Down Expand Up @@ -436,6 +447,11 @@ test:
usps_confirmation_max_days: '10'
enable_i18n_mode: 'false'
enable_load_testing_mode: 'false'
usps_download_sftp_directory: '/undeliverable'
usps_download_sftp_host:
usps_download_sftp_username:
usps_download_sftp_password:
usps_download_token: 'test_token'
usps_upload_sftp_directory: '/directory'
usps_upload_sftp_host: 'example.com'
usps_upload_sftp_username: 'user'
Expand Down
5 changes: 5 additions & 0 deletions config/locales/user_mailer/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,8 @@ en:
you can ignore this message.
link_text: Go to %{app}
reset_password: If you can't remember your password, go to %{app} to reset it.
undeliverable_address:
call_to_action: Please sign in to login.gov and follow instructions.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tell people to sign in and follow instructions, but the instructions they see will them to enter the code we couldn't send them, right? Should we build in something, either here or in another PR, that shows users something if their letter bounced?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GUI and this email verbiage is up in the air right now. I guess depending on where we go with the GUI will drive what we say here. Docauth currently does not have a way to change the address so we would need to add a new flow for that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, cool. Those are all things we can handle outside this PR once we know what needs to happen.

help: ''
intro: We were unable to send mail to the address you provided.
subject: Mail sent to your address was undeliverable
5 changes: 5 additions & 0 deletions config/locales/user_mailer/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,8 @@ es:
este email, puede ignorar este mensaje.
link_text: Ir a %{app}
reset_password: Si no recuerda su contraseña, vaya a %{app} para restablecerla.
undeliverable_address:
call_to_action: Por favor, inicie sesión en login.gov y siga las instrucciones.
help: ''
intro: No hemos podido enviar el correo a la dirección que proporcionó.
subject: El correo enviado a su dirección no se pudo entregar
5 changes: 5 additions & 0 deletions config/locales/user_mailer/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,8 @@ fr:
link_text: Allez à %{app}
reset_password: Si vous ne vous souvenez plus de votre mot de passe, allez à
%{app} pour le réinitialiser.
undeliverable_address:
call_to_action: Veuillez vous connecter à login.gov et suivre les instructions.
help: ''
intro: Nous n'avons pas pu envoyer de courrier à l'adresse que vous avez fournie.
subject: Le courrier envoyé à votre adresse était non distribuable.
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
defaults: { format: :xml }

post '/api/usps_upload' => 'usps_upload#create'
post '/api/usps_download' => 'undeliverable_address#create'

get '/openid_connect/authorize' => 'openid_connect/authorization#index'
get '/openid_connect/logout' => 'openid_connect/logout#index'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddBouncedAtToUspsConfirmationCode < ActiveRecord::Migration[5.1]
disable_ddl_transaction!

def change
add_column :usps_confirmation_codes, :bounced_at, :timestamp
add_index :usps_confirmation_codes, :otp_fingerprint, algorithm: :concurrently
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20181121223714) do
ActiveRecord::Schema.define(version: 20181122100307) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -268,6 +268,8 @@
t.datetime "code_sent_at", default: -> { "now()" }, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "bounced_at"
t.index ["otp_fingerprint"], name: "index_usps_confirmation_codes_on_otp_fingerprint"
t.index ["profile_id"], name: "index_usps_confirmation_codes_on_profile_id"
end

Expand Down
45 changes: 45 additions & 0 deletions spec/controllers/undeliverable_address_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'rails_helper'

describe UndeliverableAddressController do
describe '#create' do
context 'with no token' do
it 'returns unauthorized' do
post :create

expect(response.status).to eq 401
end
end

context 'with an invalid token' do
before do
headers('foobar')
end

it 'returns unauthorized' do
post :create

expect(response.status).to eq 401
end
end

context 'with a valid token' do
before do
headers(Figaro.env.usps_download_token)
end

it 'returns a good status' do
notifier = instance_double(UndeliverableAddressNotifier)
expect(notifier).to receive(:call)
expect(UndeliverableAddressNotifier).to receive(:new).and_return(notifier)

post :create

expect(response).to have_http_status(:ok)
end
end
end

def headers(token)
request.headers['X-API-AUTH-TOKEN'] = token
end
end
19 changes: 19 additions & 0 deletions spec/mailers/user_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,25 @@ def expect_email_body_to_have_help_and_contact_links
end
end

describe 'undeliverable_address' do
let(:mail) { UserMailer.undeliverable_address(email_address) }

it_behaves_like 'a system email'

it 'sends to the current email' do
expect(mail.to).to eq [email_address.email]
end

it 'renders the subject' do
expect(mail.subject).to eq t('user_mailer.undeliverable_address.subject')
end

it 'renders the body' do
expect(mail.html_part.body).
to have_content(strip_tags(t('user_mailer.undeliverable_address.intro')))
end
end

def strip_tags(str)
ActionController::Base.helpers.strip_tags(str)
end
Expand Down
Loading