Slide 1

Slide 1 text

Cody Norman Attraction Mailbox Why I Love Action Mailbox ❤ Rocky Mountain Ruby - 2024 🏔

Slide 2

Slide 2 text

I really ❤ email

Slide 3

Slide 3 text

I’m in love with the idea of email I really ❤ email

Slide 4

Slide 4 text

Introduction What we’ll be covering • What is Action Mailbox? • Practical Use Cases. • Sending inbound emails to your application. • How inbound emails are processed. • Tips for deploying and running in production.

Slide 5

Slide 5 text

Action Mailbox Background What is it? Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. The inbound emails are turned into `InboundEmail` records using Active Record and feature life cycle tracking These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model.

Slide 6

Slide 6 text

Action Mailbox Examples Practical Examples • Create Active Storage attachments from attached fi les • Send SMS messages with inbound emails • Email a question to AI • Information and Promotions for Events

Slide 7

Slide 7 text

Action Mailbox Examples Generate PDF from email attachment class LegacyDocumentMailbox < ApplicationMailbox def process legacy_document = LegacyDocument.new( name: mail.subject, email: mail.from.first, ) legacy_document.imported_document.attach( io: StringIO.new(mail.attachments.first.body.decoded), filename: mail.attachments.first.filename ) legacy_document.save! end end

Slide 8

Slide 8 text

Action Mailbox Examples Email to SMS class SmsMailbox < ApplicationMailbox require 'twilio-ruby' def process @client = Twilio::REST::Client.new from_phone_number = TWILIO_FROM_NUMBER to_phone_number = mail.subject message = mail.decoded @client.messages.create( from: from_phone_number, to: to_phone_number, body: message ) end end

Slide 9

Slide 9 text

Action Mailbox Examples Email with AI class OpenAiMailbox < ApplicationMailbox def process client = OpenAI::Client.new message = mail.decoded response = client.chat( parameters: { model: "gpt-3.5-turbo", messages: [{ role: "user", content: message}], temperature: 0.7, }) ai_reply = response.dig('choices', 0, 'message', 'content') to_email = mail.from.first original_question = message OpenAiMailer.with(email: to_email, response: ai_reply, original_question: original_question).response.deliver end end

Slide 10

Slide 10 text

Action Mailbox Examples Special Event Promotions class OpenAiMailbox < ApplicationMailbox def process client = OpenAI::Client.new message = mail.decoded response = client.chat( parameters: { model: "gpt-3.5-turbo", messages: [{ role: "user", content: message}], temperature: 0.7, }) ai_reply = response.dig('choices', 0, 'message', 'content') to_email = mail.from.first original_question = message OpenAiMailer.with(email: to_email, response: ai_reply, Rocky Mountain Ruby 2024 (add code? Or at least the screenshots of the easter egg Maybe break that up into multiple slides/

Slide 11

Slide 11 text

What happens when I send an inbound email?

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

https://actionmailbox:[email protected]/rails/action_mailbox/postmark/inbound_emails

Slide 16

Slide 16 text

Getting Started • Run generators to install • Routing inbound emails • Implementing the parsing logic

Slide 17

Slide 17 text

Running the Generator Run built in generator $ bin/rails action_mailbox:install $ bin/rails db:migrate

Slide 18

Slide 18 text

Getting Started What’s added bin/rails action_mailbox:install Copying application_mailbox.rb to app/mailboxes create app/mailboxes/application_mailbox.rb rails railties:install:migrations FROM=active_storage,action_mailbox Copied migration 20240123213529_create_action_mailbox_tables.action_mailbox.rb from action_mailbox

Slide 19

Slide 19 text

Getting Started Generated Migrations # This migration comes from action_mailbox (originally 20180917164000) 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, null: false t.string :message_checksum, null: false t.timestamps t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end end end

Slide 20

Slide 20 text

Getting Started ApplicationMailbox # app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base # routing /something/i => :somewhere end

Slide 21

Slide 21 text

Getting Started Generating your fi rst Mailbox bin/rails generate mailbox support class SupportMailbox < ApplicationMailbox def process # email processing logic end end

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

ActionMailbox::InboundEmail class InboundEmail < Record self.table_name = "action_mailbox_inbound_emails" include Incineratable, MessageId, Routable has_one_attached :raw_email, service: ActionMailbox.storage_service enum status: %i[ pending processing delivered failed bounced ] def mail @mail ||= Mail.from_source(source) end def source @source ||= raw_email.download end def processed? delivered? || failed? || bounced? end end end

Slide 24

Slide 24 text

ActionMailbox::InboundEmail include Incineratable, MessageId, Routable

Slide 25

Slide 25 text

ActionMailbox::InboundEmail has_one_attached :raw_email, service: ActionMailbox.storage_service

Slide 26

Slide 26 text

ActionMailbox::InboundEmail inbound_email.raw_email => # inbound_email.source => "Date: Mon, 11 Mar 2024 11:56:41..." inbound_email.mail => #

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Getting Started Email Routing Tips # app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base routing /food@/ => :food_options routing /coffee@/ => :coffee_options routing /winner-winner-lunch-or-dinner@/ => :gift_card_easter_egg routing /death-before-decaf@/ => :gift_card_easter_egg routing all: :promotions end

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

Mailboxes Processing email messages • Where the magic happens 🪄 • Calls the process method • Provides lifecycle hooks like before_processing • Handy methods like bounce_with

Slide 31

Slide 31 text

class SupportMailbox < ApplicationMailbox before_processing :ensure_user ... def ensure_user @user = User.find_by(email: from_email) unless @user bounce_with Mailer.post_not_found(mail) end end end

Slide 32

Slide 32 text

Deploying to Production • Exim • Mailgun • Mandrill • Post fi x • PostmarK • Qmail • SendGrid Default Ingress options

Slide 33

Slide 33 text

Deploying to Production Potential Pitfalls • ActiveStorage URL options won’t be set by default (that happens in the Application Controller). You have to set this the same way you set it for your emails. • You probably want to set up an ActiveStorage service besides ‘disk’. That can cause some issues. • Routing all your inbound email through a subdomain like inbound.yourapp.com can save a lot of headaches. 
 
 ([email protected])

Slide 34

Slide 34 text

Impractical Examples Don’t Try This At Home…. The Ruby people, strange people… - Linus Torvalds

Slide 35

Slide 35 text

Cat Facts on Demand Send an email to [email protected]

Slide 36

Slide 36 text

Action Mailbox “Examples” Cat Facts on Demand class CatFactsMailbox < ApplicationMailbox def process CatFactsMailer.with(email: mail.from.first, cat_fact: cat_fact).cat_fact.deliver end def cat_fact @cat_fact ||= HTTParty.get("https://catfact.ninja/ fact").dig("fact") end end

Slide 37

Slide 37 text

Age Guesser Send an email to [email protected]

Slide 38

Slide 38 text

Action Mailbox “Examples” Age Guesser class GuessMyAgeMailbox < ApplicationMailbox def process response = HTTParty.get(agify_url, { query: { name: name } }) age = response["age"] GuessMyAgeMailer.with(guess: age, sender_email: mail.from.first, name: name).response.deliver_now end def name mail[:from].display_names.first.presence || mail.from.first.split("@").first end end

Slide 39

Slide 39 text

Mail yourself a Postcard Reminder

Slide 40

Slide 40 text

Action Mailbox “Examples” Mail yourself a Postcard Reminder class PostcardMailbox < ApplicationMailbox def process postcardApi = Lob::PostcardsApi.new($lob_config) postcardCreate = Lob::PostcardEditable.new({ description: "Demo Postcard job", from: "adr_210a8d4b0b76d77b", use_type: "operational", front: “https://FAKE_BUCKET.s3.amazonaws.com/boston_ruby_postcard.png”, back: "

{{ message }}

”, to: "adr_123123123123123123", merge_variables: { message: mail.body.decoded, }, }); begin createdPostcard = postcardApi.create(postcardCreate) rescue => err p err.message end end end

Slide 41

Slide 41 text

Front Back

Slide 42

Slide 42 text

Action Mailbox “Examples” Start your vehicle with an Email class TruckMailbox < ApplicationMailbox def process HTTParty.post(TRUCK_API_URL, body: start_request_body, headers: auth_headers) end end

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

Cody Norman Independent Ruby on Rails Consultant codynorman.com @cnorm35