Last active
May 25, 2021 09:28
-
-
Save TheRusskiy/cb4adca186e5a14dc2a99921b150ca04 to your computer and use it in GitHub Desktop.
Tracking opened emails in Postmark & Rails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ApplicationMailer < ActionMailer::Base | |
def new_blog_post(blog_post, subscriber) | |
# by calling "store_message" we are saying that this | |
# emails need to be saved in our database | |
# for further tracking | |
store_message( | |
email_name: 'new_blog_post', | |
entity: blog_post, | |
user: subscriber | |
) | |
mail( | |
to: subscriber.email, | |
subject: "New Post: #{blog_post.title}", | |
# this param is required if you want Postmark to add a tracking pixel | |
# and send you status updates | |
track_opens: 'true' | |
) | |
end | |
protected | |
# email_name - some name we can later use for statistics | |
# entity - any ActiveRecord model we want to associate the email with | |
# user - user this email is sent to | |
def store_message(email_name:, entity:, user: nil) | |
self.metadata['email_name'] = email_name.to_s.truncate(80) | |
self.metadata['entity_id'] = entity.id | |
self.metadata['entity_type'] = entity.class.name | |
self.metadata['user_id'] = user.id if user | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class EmailBouncedService | |
def self.call(message_id:, error_message:) | |
sent_email = SentEmail.find_by_message_id(message_id) | |
return unless sent_email | |
sent_email.update!(error: error_message, status: 'failed') | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class EmailOpenedService | |
def self.call(message_id:, first_open:, opened_at:) | |
return unless first_open | |
sent_email = SentEmail.find_by_message_id(message_id) | |
return unless sent_email | |
sent_email.update!(error: nil, status: 'opened', opened_at: opened_at) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CreateSentEmails < ActiveRecord::Migration[6.1] | |
def change | |
create_table :sent_emails do |t| | |
t.text :email_name, null: false | |
t.text :message_id | |
t.references :entity, polymorphic: true, index: true | |
t.references :user, foreign_key: true, null: true, index: true | |
t.integer :status, default: 0, null: false | |
t.datetime :opened_at | |
t.text :error | |
t.timestamps | |
t.index :email_name | |
t.index :entity_id | |
t.index :message_id | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class PostmarkController < ActionController::Base | |
skip_before_action :verify_authenticity_token | |
# we are going to secure this webhook endpoint by using basic auth, | |
# when defining your webhook on Postmark you should set it as | |
# https://<username>:<password>@example.com/postmark_opened | |
# https://<username>:<password>@example.com/postmark_bounced | |
# TODO: use real credentials for basic auth | |
http_basic_authenticate_with name: "SECRET_NAME", password: "SECRET_PASSWORD" | |
def email_opened | |
EmailOpenedService.call( | |
message_id: params[:MessageID], | |
first_open: params[:FirstOpen], | |
opened_at: params[:ReceivedAt] | |
) | |
render json: { status: 201 } | |
end | |
def email_bounced | |
EmailBouncedService.call( | |
message_id: params[:MessageID], | |
error_message: params[:Description] | |
) | |
render json: { status: 201 } | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# place this file in config/initializers | |
class PostmarkMailObserver | |
def self.delivered_email(m) | |
# only create a record if API has accepted the message | |
return unless m.delivered? | |
# as a part of API we are going to assume that | |
# an email should be saved if "email_name" is set | |
return unless m.metadata['email_name'].present? | |
SentEmail.create( | |
email_name: m.metadata['email_name'], | |
status: 'sent', | |
message_id: m.message_id, | |
entity_id: m.metadata['entity_id'], | |
entity_type: m.metadata['entity_type'], | |
user_id: m.metadata['user_id'], | |
subject: m.subject | |
) | |
end | |
end | |
ActionMailer::Base.register_observer(PostmarkMailObserver) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Rails.application.routes.draw do | |
post 'postmark_opened', to: 'postmark#email_opened' | |
post 'postmark_bounced', to: 'postmark#email_bounced' | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SentEmail < ApplicationRecord | |
belongs_to :entity, polymorphic: true | |
belongs_to :user, optional: true | |
enum status: { sent: 0, opened: 1, failed: 2 } | |
validates_presence_of :email_name, :status | |
validates_presence_of :message_id, unless: :failed? | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment