Skip to content

Commit 8dab220

Browse files
committed
Store transient bounces as well as permanent bounces
Fixes CNVS-22956 Test plan: - Amazon's documentation is ambiguous about whether ooto@simulator.amazonses.com will actually generate a transient failure, so this may not actually work. - Set up outgoing email through the Amazon SES test account - Set up bounce_notifications.yml with the SQS test creds - Set a user's email address to ooto@simulator.amazonses.com - Cause a message to be sent to that user - Wait for the bounce notification processor to run (~5 minutes) - At a Rails console, check the CommunicationChannel's last_transient_bounce_at and verify that it roughly corresponds to when the initial message was sent - Check the CommunicationChannel's last_transient_bounce_details and ensure that they have been filled in Change-Id: Ic3a4110319673d6ec6f0789885c760faec4775b3 Reviewed-on: https://gerrit.instructure.com/62562 Tested-by: Jenkins Reviewed-by: Jacob Fugal <jacob@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Alex Boyd <aboyd@instructure.com>
1 parent 5b7559c commit 8dab220

6 files changed

Lines changed: 87 additions & 32 deletions

app/models/bounce_notification_processor.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,12 @@ def parse_message(message)
6565
end
6666

6767
def process_bounce_notification(bounce_notification)
68-
return unless is_permanent_bounce?(bounce_notification)
69-
7068
bouncy_addresses(bounce_notification).each do |address|
7169
CommunicationChannel.bounce_for_path(
7270
path: address,
7371
timestamp: bounce_timestamp(bounce_notification),
7472
details: bounce_notification,
73+
permanent_bounce: is_permanent_bounce?(bounce_notification),
7574
suppression_bounce: is_suppression_bounce?(bounce_notification)
7675
)
7776
end

app/models/communication_channel.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class CommunicationChannel < ActiveRecord::Base
2424
attr_accessible :user, :path, :path_type, :build_pseudonym_on_confirm, :pseudonym
2525

2626
serialize :last_bounce_details
27+
serialize :last_transient_bounce_details
2728

2829
belongs_to :pseudonym
2930
has_many :pseudonyms
@@ -441,16 +442,21 @@ def check_if_bouncing_changed
441442
end
442443
private :check_if_bouncing_changed
443444

444-
def self.bounce_for_path(path:, timestamp:, details:, suppression_bounce:)
445+
def self.bounce_for_path(path:, timestamp:, details:, permanent_bounce:, suppression_bounce:)
445446
Shard.with_each_shard(CommunicationChannel.associated_shards(path)) do
446447
CommunicationChannel.unretired.email.by_path(path).each do |channel|
447-
channel.bounce_count = channel.bounce_count + 1
448+
channel.bounce_count = channel.bounce_count + 1 if permanent_bounce
449+
448450
if suppression_bounce
449451
channel.last_suppression_bounce_at = timestamp
450-
else
452+
elsif permanent_bounce
451453
channel.last_bounce_at = timestamp
452454
channel.last_bounce_details = details
455+
else
456+
channel.last_transient_bounce_at = timestamp
457+
channel.last_transient_bounce_details = details
453458
end
459+
454460
channel.save!
455461
end
456462
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class AddTransientBounceColumnsToCommunicationChannels < ActiveRecord::Migration
2+
tag :predeploy
3+
4+
def change
5+
add_column :communication_channels, :last_transient_bounce_at, :datetime
6+
add_column :communication_channels, :last_transient_bounce_details, :text, length: 32768
7+
end
8+
end

spec/fixtures/bounces.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,5 @@
6464
"Signature" : "example==",
6565
"SigningCertURL" : "https://example.com/example.pem",
6666
"UnsubscribeURL" : "https://example.com/unsubscribe"
67-
},
68-
{
69-
"Type" : "Notification",
70-
"MessageId" : "example-id",
71-
"TopicArn" : "example-arn",
72-
"Message" : "{\"notificationType\":\"Bounce\",\"bounce\":{\"bounceSubType\":\"General\",\"bounceType\":\"Transient\",\"reportingMTA\":\"dsn; a14-1.smtp-out.amazonses.com\",\"bouncedRecipients\":[{\"emailAddress\":\"soft@example.edu\",\"status\":\"4.4.7\",\"action\":\"failed\",\"diagnosticCode\":\"smtp; 554 4.4.7 Message expired: unable to deliver in 840 minutes.<421 4.4.0 Unable to lookup DNS for ms-wasatch-edu.mail.protection.outlook.com>\"}],\"timestamp\":\"2014-08-22T12:18:58.226Z\",\"feedbackId\":\"example-id\"},\"mail\":{\"timestamp\":\"2014-08-21T20:51:56.000Z\",\"source\":\"notifications@instructure.com\",\"messageId\":\"example-id\",\"destination\":[\"soft@example.edu\"]}}",
73-
"Timestamp" : "2014-08-22T12:18:58.393Z",
74-
"SignatureVersion" : "1",
75-
"Signature" : "example==",
76-
"SigningCertURL" : "https://example.com/example.pem",
77-
"UnsubscribeURL" : "https://example.com/unsubscribe"
7867
}
7968
]

spec/models/bounce_notification_processor_spec.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,24 @@ def mock_message(json)
4848
queue.expects(:poll).multiple_yields(*@all_bounce_messages_json.map {|m| mock_message(m)})
4949
bnp.stubs(:bounce_queue).returns(queue)
5050

51-
CommunicationChannel.expects(:bounce_for_path).with do |path:, timestamp:, details:, suppression_bounce:|
51+
CommunicationChannel.expects(:bounce_for_path).with do |path:, timestamp:, details:, permanent_bounce:, suppression_bounce:|
5252
path == 'hard@example.edu' &&
5353
timestamp == '2014-08-22T12:25:46.786Z' &&
54+
permanent_bounce == true &&
5455
suppression_bounce == false
5556
end.times(4)
56-
CommunicationChannel.expects(:bounce_for_path).with do |path:, timestamp:, details:, suppression_bounce:|
57+
CommunicationChannel.expects(:bounce_for_path).with do |path:, timestamp:, details:, permanent_bounce:, suppression_bounce:|
5758
path == 'suppressed@example.edu' &&
5859
timestamp == '2014-08-22T12:18:58.044Z' &&
60+
permanent_bounce == true &&
5961
suppression_bounce == true
6062
end.times(1)
61-
CommunicationChannel.expects(:bounce_for_path).with do |path:, timestamp:, details:, suppression_bounce:|
62-
path == 'soft@example.edu'
63-
end.times(0)
63+
CommunicationChannel.expects(:bounce_for_path).with do |path:, timestamp:, details:, permanent_bounce:, suppression_bounce:|
64+
path == 'soft@example.edu' &&
65+
timestamp == '2014-08-22T13:24:31.000Z' &&
66+
permanent_bounce == false &&
67+
suppression_bounce == false
68+
end.times(1)
6469

6570
bnp.process
6671
end

spec/models/communication_channel_spec.rb

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
expect(@cc.state).to eql(:retired)
6565
@cc.re_activate
6666
expect(@cc.state).to eql(:active)
67-
67+
6868
communication_channel_model(:path => "another_path@example.com")
6969
expect(@cc.state).to eql(:unconfirmed)
7070
@cc.retire
@@ -89,27 +89,27 @@
8989
communication_channel_model
9090
expect(@cc.confirmation_code).to eql('abc123')
9191
end
92-
92+
9393
it "should be able to reset a confirmation code" do
9494
communication_channel_model
9595
old_cc = @cc.confirmation_code
9696
@cc.set_confirmation_code(true)
9797
expect(@cc.confirmation_code).not_to eql(old_cc)
9898
end
99-
99+
100100
it "should use a 15-digit confirmation code for default or email path_type settings" do
101101
communication_channel_model
102102
expect(@cc.path_type).to eql('email')
103103
expect(@cc.confirmation_code.size).to eql(25)
104104
end
105-
105+
106106
it "should use a 4-digit confirmation_code for settings other than email" do
107107
communication_channel_model
108108
@cc.path_type = 'sms'
109109
@cc.set_confirmation_code(true)
110110
expect(@cc.confirmation_code.size).to eql(4)
111111
end
112-
112+
113113
it "should default the path type to email" do
114114
communication_channel_model
115115
expect(@cc.path_type).to eql('email')
@@ -134,11 +134,11 @@
134134
@cc.path_type = 'not valid'; @cc.save
135135
expect(@cc.path_type).to eql('email')
136136
end
137-
137+
138138
it "should act as list" do
139139
expect(CommunicationChannel).to be_respond_to(:acts_as_list)
140140
end
141-
141+
142142
it "should scope the list to the user" do
143143
@u1 = User.create!
144144
@u2 = User.create!
@@ -161,13 +161,13 @@
161161
@cc3.reload
162162
expect(@cc3.position).to eql(1)
163163
end
164-
164+
165165
context "can_notify?" do
166166
it "should normally be able to be used" do
167167
communication_channel_model
168168
expect(@communication_channel).to be_can_notify
169169
end
170-
170+
171171
it "should not be able to be used if it has a policy to not use it" do
172172
communication_channel_model
173173
notification_policy_model(:frequency => "never", :communication_channel => @communication_channel)
@@ -298,6 +298,7 @@
298298
path: path,
299299
timestamp: nil,
300300
details: nil,
301+
permanent_bounce: true,
301302
suppression_bounce: false
302303
)
303304
end
@@ -315,36 +316,63 @@
315316
cc = communication_channel_model(
316317
path: 'foo@bar.edu',
317318
last_bounce_at: '2015-01-01T01:01:01.000Z',
318-
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z'
319+
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z',
320+
last_transient_bounce_at: '2015-04-04T04:04:04.000Z'
319321
)
320322
CommunicationChannel.bounce_for_path(
321323
path: 'foo@bar.edu',
322324
timestamp: '2015-02-02T02:02:02.000Z',
323325
details: nil,
326+
permanent_bounce: true,
324327
suppression_bounce: false
325328
)
326329

327330
cc.reload
328331
expect(cc.last_bounce_at).to eq('2015-02-02T02:02:02.000Z')
329332
expect(cc.last_suppression_bounce_at).to eq('2015-03-03T03:03:03.000Z')
333+
expect(cc.last_transient_bounce_at).to eq('2015-04-04T04:04:04.000Z')
334+
end
335+
336+
it "stores the date of the last soft bounce bounce" do
337+
cc = communication_channel_model(
338+
path: 'foo@bar.edu',
339+
last_bounce_at: '2015-01-01T01:01:01.000Z',
340+
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z',
341+
last_transient_bounce_at: '2015-04-04T04:04:04.000Z'
342+
)
343+
CommunicationChannel.bounce_for_path(
344+
path: 'foo@bar.edu',
345+
timestamp: '2015-05-05T05:05:05.000Z',
346+
details: nil,
347+
permanent_bounce: false,
348+
suppression_bounce: false
349+
)
350+
351+
cc.reload
352+
expect(cc.last_bounce_at).to eq('2015-01-01T01:01:01.000Z')
353+
expect(cc.last_suppression_bounce_at).to eq('2015-03-03T03:03:03.000Z')
354+
expect(cc.last_transient_bounce_at).to eq('2015-05-05T05:05:05.000Z')
330355
end
331356

332357
it "stores the date of the last suppression bounce" do
333358
cc = communication_channel_model(
334359
path: 'foo@bar.edu',
335360
last_bounce_at: '2015-01-01T01:01:01.000Z',
336-
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z'
361+
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z',
362+
last_transient_bounce_at: '2015-04-04T04:04:04.000Z'
337363
)
338364
CommunicationChannel.bounce_for_path(
339365
path: 'foo@bar.edu',
340366
timestamp: '2015-02-02T02:02:02.000Z',
341367
details: nil,
368+
permanent_bounce: true,
342369
suppression_bounce: true
343370
)
344371

345372
cc.reload
346373
expect(cc.last_bounce_at).to eq('2015-01-01T01:01:01.000Z')
347374
expect(cc.last_suppression_bounce_at).to eq('2015-02-02T02:02:02.000Z')
375+
expect(cc.last_transient_bounce_at).to eq('2015-04-04T04:04:04.000Z')
348376
end
349377

350378
it "stores the details of the last hard bounce" do
@@ -353,11 +381,28 @@
353381
path: 'foo@bar.edu',
354382
timestamp: nil,
355383
details: {'some' => 'details', 'foo' => 'bar'},
384+
permanent_bounce: true,
356385
suppression_bounce: false
357386
)
358387

359388
cc.reload
360389
expect(cc.last_bounce_details).to eq('some' => 'details', 'foo' => 'bar')
390+
expect(cc.last_transient_bounce_details).to be_nil
391+
end
392+
393+
it "stores the details of the last soft bounce" do
394+
cc = communication_channel_model(path: 'foo@bar.edu')
395+
CommunicationChannel.bounce_for_path(
396+
path: 'foo@bar.edu',
397+
timestamp: nil,
398+
details: {'some' => 'details', 'foo' => 'bar'},
399+
permanent_bounce: false,
400+
suppression_bounce: false
401+
)
402+
403+
cc.reload
404+
expect(cc.last_transient_bounce_details).to eq('some' => 'details', 'foo' => 'bar')
405+
expect(cc.last_bounce_details).to be_nil
361406
end
362407

363408
it "does not store the details of the last suppression bounce" do
@@ -369,11 +414,13 @@
369414
path: 'foo@bar.edu',
370415
timestamp: nil,
371416
details: {'some' => 'details', 'foo' => 'bar'},
417+
permanent_bounce: true,
372418
suppression_bounce: true
373419
)
374420

375421
cc.reload
376422
expect(cc.last_bounce_details).to eq('existing' => 'details')
423+
expect(cc.last_transient_bounce_details).to be_nil
377424
end
378425
end
379426

@@ -431,6 +478,7 @@
431478
path: path,
432479
timestamp: nil,
433480
details: nil,
481+
permanent_bounce: true,
434482
suppression_bounce: false
435483
)
436484
end

0 commit comments

Comments
 (0)