Skip to content

Instantly share code, notes, and snippets.

@TheRusskiy
Last active May 25, 2021 09:28
Show Gist options
  • Save TheRusskiy/cb4adca186e5a14dc2a99921b150ca04 to your computer and use it in GitHub Desktop.
Save TheRusskiy/cb4adca186e5a14dc2a99921b150ca04 to your computer and use it in GitHub Desktop.
Tracking opened emails in Postmark & Rails
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
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
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
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
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
# 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)
Rails.application.routes.draw do
post 'postmark_opened', to: 'postmark#email_opened'
post 'postmark_bounced', to: 'postmark#email_bounced'
end
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