Skip to content

Instantly share code, notes, and snippets.

@TheRusskiy
Last active May 25, 2021 09:28
Show Gist options
  • Select an option

  • Save TheRusskiy/cb4adca186e5a14dc2a99921b150ca04 to your computer and use it in GitHub Desktop.

Select an option

Save TheRusskiy/cb4adca186e5a14dc2a99921b150ca04 to your computer and use it in GitHub Desktop.

Revisions

  1. TheRusskiy revised this gist May 25, 2021. 4 changed files with 13 additions and 4 deletions.
    14 changes: 10 additions & 4 deletions application_mailer.rb
    Original file line number Diff line number Diff line change
    @@ -3,11 +3,15 @@ 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)
    store_message(
    email_name: 'new_blog_post',
    entity: blog_post,
    user: subscriber
    )

    mail(
    to: subscriber.email,
    subject: "New Post: #{blog_post.title}",
    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'
    @@ -18,9 +22,11 @@ def new_blog_post(blog_post, subscriber)

    # email_name - some name we can later use for statistics
    # entity - any ActiveRecord model we want to associate the email with
    def store_message(email_name:, entity:)
    # 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
    end
    1 change: 1 addition & 0 deletions migration.rb
    Original file line number Diff line number Diff line change
    @@ -4,6 +4,7 @@ def change
    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
    1 change: 1 addition & 0 deletions postmark_observer.rb
    Original file line number Diff line number Diff line change
    @@ -15,6 +15,7 @@ def self.delivered_email(m)
    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
    1 change: 1 addition & 0 deletions sent_email.rb
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,6 @@
    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?
  2. TheRusskiy revised this gist May 25, 2021. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion application_mailer.rb
    Original file line number Diff line number Diff line change
    @@ -7,7 +7,9 @@ def new_blog_post(blog_post, subscriber)

    mail(
    to: subscriber.email,
    subject: "New Post: #{blog_post.title}",
    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
  3. TheRusskiy revised this gist May 25, 2021. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions postmark_observer.rb
    Original file line number Diff line number Diff line change
    @@ -19,3 +19,5 @@ def self.delivered_email(m)
    )
    end
    end

    ActionMailer::Base.register_observer(PostmarkMailObserver)
  4. TheRusskiy created this gist May 25, 2021.
    24 changes: 24 additions & 0 deletions application_mailer.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,24 @@
    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)

    mail(
    to: subscriber.email,
    subject: "New Post: #{blog_post.title}",
    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
    def store_message(email_name:, entity:)
    self.metadata['email_name'] = email_name.to_s.truncate(80)
    self.metadata['entity_id'] = entity.id
    self.metadata['entity_type'] = entity.class.name
    end
    end
    9 changes: 9 additions & 0 deletions email_bounced_service.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    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
    10 changes: 10 additions & 0 deletions email_opened_service.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    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
    17 changes: 17 additions & 0 deletions migration.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    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.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
    26 changes: 26 additions & 0 deletions postmark_controller.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,26 @@
    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
    21 changes: 21 additions & 0 deletions postmark_observer.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    # 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'],
    subject: m.subject
    )
    end
    end
    4 changes: 4 additions & 0 deletions routes.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    Rails.application.routes.draw do
    post 'postmark_opened', to: 'postmark#email_opened'
    post 'postmark_bounced', to: 'postmark#email_bounced'
    end
    6 changes: 6 additions & 0 deletions sent_email.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    class SentEmail < ApplicationRecord
    belongs_to :entity, polymorphic: true
    enum status: { sent: 0, opened: 1, failed: 2 }
    validates_presence_of :email_name, :status
    validates_presence_of :message_id, unless: :failed?
    end