Skip to content

Commit

Permalink
Ensure Action Mailbox processes an email only once when received mult…
Browse files Browse the repository at this point in the history
…iple times

This also adds a new column, message_checksum, to the action_mailbox_inbound_emails table
for storing SHA1 digest of the email source. Additionally, it makes generating the missing
message id deterministic and adds a unique index on message_checksum and message_id to
detect duplicate emails.
  • Loading branch information
lifo committed Jan 17, 2019
1 parent 2dee59f commit 5cd733a
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 15 deletions.
22 changes: 11 additions & 11 deletions actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,30 @@
module ActionMailbox::InboundEmail::MessageId
extend ActiveSupport::Concern

included do
before_save :generate_missing_message_id
end

class_methods do
# Create a new +InboundEmail+ from the raw +source+ of the email, which be uploaded as a Active Storage
# attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
# it as an attribute on the new +InboundEmail+.
def create_and_extract_message_id!(source, **options)
create! options.merge(message_id: extract_message_id(source)) do |inbound_email|
message_checksum = Digest::SHA1.hexdigest(source)
message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)

create! options.merge(message_id: message_id, message_checksum: message_checksum) do |inbound_email|
inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
end
rescue ActiveRecord::RecordNotUnique
nil
end

private
def extract_message_id(source)
Mail.from_source(source).message_id rescue nil
end
end

private
def generate_missing_message_id
self.message_id ||= Mail::MessageIdField.new.message_id.tap do |message_id|
logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}"
def generate_missing_message_id(message_checksum)
Mail::MessageIdField.new("<#{message_checksum}@#{::Socket.gethostname}.mail>").message_id.tap do |message_id|
logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}"
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
def change
create_table :action_mailbox_inbound_emails do |t|
t.integer :status, default: 0, null: false
t.string :message_id
t.string :message_id, null: false
t.string :message_checksum, null: false

t.datetime :created_at, precision: 6, null: false
t.datetime :updated_at, precision: 6, null: false

t.index [ :message_id, :message_checksum ], unique: true
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
def change
create_table :action_mailbox_inbound_emails do |t|
t.integer :status, default: 0, null: false
t.string :message_id
t.string :message_id, null: false
t.string :message_checksum, null: false

t.datetime :created_at, precision: 6
t.datetime :updated_at, precision: 6

t.index [ :message_id, :message_checksum ], unique: true
end
end
end
2 changes: 2 additions & 0 deletions actionmailbox/test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
create_table "action_mailbox_inbound_emails", force: :cascade do |t|
t.integer "status", default: 0, null: false
t.string "message_id"
t.string "message_checksum"
t.datetime "created_at", precision: 6
t.datetime "updated_at", precision: 6
t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
end

create_table "active_storage_attachments", force: :cascade do |t|
Expand Down
22 changes: 22 additions & 0 deletions actionmailbox/test/unit/inbound_email_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,27 @@ class InboundEmailTest < ActiveSupport::TestCase
test "source returns the contents of the raw email" do
assert_equal file_fixture("welcome.eml").read, create_inbound_email_from_fixture("welcome.eml").source
end

test "email with message id is processed only once when received multiple times" do
mail = Mail.from_source(file_fixture("welcome.eml").read)
assert mail.message_id

inbound_email_1 = create_inbound_email_from_source(mail.to_s)
assert inbound_email_1

inbound_email_2 = create_inbound_email_from_source(mail.to_s)
assert_nil inbound_email_2
end

test "email with missing message id is processed only once when received multiple times" do
mail = Mail.from_source("Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: [email protected]\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!")
assert_nil mail.message_id

inbound_email_1 = create_inbound_email_from_source(mail.to_s)
assert inbound_email_1

inbound_email_2 = create_inbound_email_from_source(mail.to_s)
assert_nil inbound_email_2
end
end
end
3 changes: 1 addition & 2 deletions actionmailbox/test/unit/mailbox/routing_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ def process
class ActionMailbox::Base::RoutingTest < ActiveSupport::TestCase
setup do
$processed = false
@inbound_email = create_inbound_email_from_fixture("welcome.eml")
end

test "string routing" do
ApplicationMailbox.route @inbound_email
ApplicationMailbox.route create_inbound_email_from_fixture("welcome.eml")
assert_equal "Discussion: Let's debate these attachments", $processed
end

Expand Down

0 comments on commit 5cd733a

Please sign in to comment.