Share
Embed
Rocky_Mountian_Ruby_2024.pdf
Cody Norman
October 08, 2024
Transcript
44 slides
-
Cody Norman Attraction Mailbox Why I Love Action Mailbox ❤
Rocky Mountain Ruby - 2024 🏔 -
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. -
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. -
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 -
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 -
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 -
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 -
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/ -
Getting Started • Run generators to install • Routing inbound
emails • Implementing the parsing logic -
Running the Generator Run built in generator $ bin/rails action_mailbox:install
$ bin/rails db:migrate -
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 -
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 -
Getting Started ApplicationMailbox # app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base #
routing /something/i => :somewhere end -
Getting Started Generating your fi rst Mailbox bin/rails generate mailbox
support class SupportMailbox < ApplicationMailbox def process # email processing logic end end -
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 -
ActionMailbox::InboundEmail inbound_email.raw_email => #<ActiveStorage::Attached::One:0x0000000112e396e0 @name="raw_email", @record=#<ActionMailbox::InboundEmail:0x0000000110523968> inbound_email.source => "Date: Mon,
11 Mar 2024 11:56:41..." inbound_email.mail => #<Mail::Message:41200> -
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 -
Mailboxes Processing email messages • Where the magic happens 🪄
• Calls the process method • Provides lifecycle hooks like before_processing • Handy methods like bounce_with -
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 -
Deploying to Production • Exim • Mailgun • Mandrill •
Post fi x • PostmarK • Qmail • SendGrid Default Ingress options -
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]) -
Impractical Examples Don’t Try This At Home…. The Ruby people,
strange people… - Linus Torvalds -
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 -
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 -
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: "<html style='padding: 1in; font-size: 20;'><p>{{ message }}</p></html>”, to: "adr_123123123123123123", merge_variables: { message: mail.body.decoded, }, }); begin createdPostcard = postcardApi.create(postcardCreate) rescue => err p err.message end end end -
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