mailer-patterns
Skill211 starsChanged 5 months agoMITView on GitHub
Creates minimal Action Mailer classes with bundled notification patterns following 37signals conventions. Use when sending emails, creating notification systems, digest emails, or when user mentions mailers, emails, notifications, or transactional messages. WHEN NOT: For background job scheduling (use job-patterns), for event-driven triggers (use event-tracking).
In this repository17 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- .opencode/CLAUDE.mdCLAUDE.md
- api-patternsSkill
- auth-setupSkill
- caching-patternsSkill
- concern-patternsSkill
- crud-patternsSkill
- event-trackingSkill
- job-patternsSkill
- migration-patternsSkill
- model-patternsSkill
- multi-tenant-setupSkill
- state-recordsSkill
- stimulus-patternsSkill
- testing-patternsSkill
- turbo-patternsSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 5 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 23
No email service abstraction layers (use Action Mailer directly)
- Very specific: line 108
Always create both .text.erb and .html.erb versions:
- Very specific: line 223
Always use deliver_later in production.
- Very specific: line 351
Create both .text.erb and .html.erb templates
- Very specific: line 352
Use deliver_later for individual emails.
- Distinctive: line 353
Check user email preferences before sending
- Distinctive: line 355
Use inline CSS for HTML emails (no external stylesheets)
- Distinctive: line 357
Create email previews for development
- Distinctive: line 367
Use external CSS in email templates
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Mailer Patterns
Philosophy: Minimal Mailers, Bundled Notifications
- Plain-text first, minimal HTML styling with inline CSS
- Bundle notifications instead of sending one email per event
- Transactional emails only (no marketing campaigns)
deliver_laterfor individual emails;deliver_nowis acceptable inside background jobs that already run asynchronously (e.g., digest delivery jobs)- Email previews for development
- No email service abstraction layers (use Action Mailer directly)
Project Knowledge
Stack: Action Mailer (built-in), Solid Queue for background delivery, email previews in development, plain text + HTML multipart emails.
Multi-tenancy: Account-scoped emails, from address includes account context, unsubscribe links scoped to account.
Commands:
rails generate mailer Comment mentioned # Generate mailer
rails generate mailer Digest daily_activity # With methods
# Visit http://localhost:3000/rails/mailers # Preview emailsPattern 1: Simple Transactional Mailers
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: ENV.fetch("MAILER_FROM_ADDRESS", "Fizzy <support@fizzy.do>")
layout "mailer"
end
# app/mailers/comment_mailer.rb
class CommentMailer < ApplicationMailer
def mentioned(mention)
@mention = mention
@comment = mention.comment
@card = mention.comment.card
@account = mention.account
mail(
to: mention.user.email,
subject: "#{mention.creator.name} mentioned you in #{@card.title}"
)
end
def new_comment(comment, recipient)
@comment = comment
@card = comment.card
@account = comment.account
mail(
to: recipient.email,
subject: "New comment on #{@card.title}"
)
end
end
# app/mailers/membership_mailer.rb
class MembershipMailer < ApplicationMailer
def invitation(membership)
@membership = membership
@account = membership.account
@inviter = membership.inviter
mail(
to: membership.user.email,
subject: "#{@inviter.name} invited you to #{@account.name}"
)
end
end
# app/mailers/card_mailer.rb
class CardMailer < ApplicationMailer
def assigned(assignment)
@assignment = assignment
@card = assignment.card
@account = assignment.account
mail(
to: assignment.user.email,
subject: "#{assignment.assigner.name} assigned you to #{@card.title}"
)
end
endPattern 2: Email Templates
See @references/mailer-templates.md for full template examples.
Always create both .text.erb and .html.erb versions:
<%# app/views/comment_mailer/mentioned.text.erb %>
Hi <%= @mention.user.name %>,
<%= @mention.creator.name %> mentioned you in a comment on <%= @card.title %>:
"<%= @comment.body %>"
View the card: <%= account_board_card_url(@account, @card.board, @card) %>
---
You're receiving this because you were mentioned.
<%# app/views/comment_mailer/mentioned.html.erb %>
<p>Hi <%= @mention.user.name %>,</p>
<p><%= @mention.creator.name %> mentioned you in a comment on
<strong><%= @card.title %></strong>:</p>
<blockquote style="border-left: 3px solid #ccc; padding-left: 15px; color: #666;">
<%= simple_format(@comment.body) %>
</blockquote>
<p><%= link_to "View the card",
account_board_card_url(@account, @card.board, @card),
style: "color: #0066cc; text-decoration: none;" %></p>Pattern 3: Bundled Notifications (Digest Emails)
See @references/bundled-notifications.md for full details.
# app/mailers/digest_mailer.rb
class DigestMailer < ApplicationMailer
def daily_activity(user, account, activities)
@user = user
@account = account
@activities = activities
@grouped_activities = activities.group_by(&:subject_type)
mail(
to: user.email,
subject: "Daily activity summary for #{account.name}"
)
end
def pending_notifications(user, notifications)
@user = user
@notifications = notifications
@accounts = notifications.map(&:account).uniq
mail(
to: user.email,
subject: "You have #{notifications.size} pending notifications"
)
end
end
# app/models/notification_bundler.rb
class NotificationBundler
def initialize(user)
@user = user
end
def should_send_digest?
pending_notifications.count >= 5 ||
oldest_pending_notification_age > 1.hour
end
def send_digest
return unless should_send_digest?
notifications = pending_notifications
DigestMailer.pending_notifications(@user, notifications).deliver_later
notifications.update_all(sent_at: Time.current)
end
private
def pending_notifications
@user.notifications.where(sent_at: nil)
.where("created_at > ?", 1.hour.ago)
.order(created_at: :desc)
end
end
# app/jobs/send_digest_emails_job.rb
class SendDigestEmailsJob < ApplicationJob
queue_as :mailers
def perform(frequency: :daily)
User.where(digest_frequency: frequency).find_each do |user|
user.accounts.each do |account|
activities = user.activities_for_digest(account, frequency)
if activities.any?
DigestMailer.daily_activity(user, account, activities).deliver_now
end
end
end
end
end
# config/recurring.yml
mailers:
daily_digest:
class: SendDigestEmailsJob
args: [{ frequency: 'daily' }]
schedule: every day at 8am
queue: mailersPattern 4: Background Delivery
Always use deliver_later in production. Trigger from model callbacks:
class Comment < ApplicationRecord
after_create_commit :notify_subscribers
after_create_commit :notify_mentions
private
def notify_subscribers
card.subscribers.each do |subscriber|
next if subscriber == creator
next unless subscriber.wants_email?(account, :comments)
CommentMailer.new_comment(self, subscriber).deliver_later
end
end
def notify_mentions
mentions.each do |mention|
next unless mention.user.wants_email?(account, :mentions)
CommentMailer.mentioned(mention).deliver_later
end
end
endPattern 5: Email Preferences and Unsubscribe
class User < ApplicationRecord
has_many :email_preferences, dependent: :destroy
enum :digest_frequency, { never: 0, daily: 1, weekly: 2 }, prefix: true
def wants_email?(account, type)
pref = email_preferences.find_by(account: account, preference_type: type)
pref.nil? || pref.enabled?
end
end
class EmailPreference < ApplicationRecord
belongs_to :user
belongs_to :account
enum :preference_type, { mentions: 0, comments: 1, assignments: 2, digests: 3 }
validates :preference_type, uniqueness: { scope: [:user_id, :account_id] }
endPattern 6: Email Previews
# test/mailers/previews/comment_mailer_preview.rb
class CommentMailerPreview < ActionMailer::Preview
def mentioned
mention = Mention.first || create_sample_mention
CommentMailer.mentioned(mention)
end
private
def create_sample_mention
user = User.first
account = Account.first
board = account.boards.first
card = board.cards.first
comment = card.comments.create!(body: "Hey @alice", creator: user)
Mention.create!(user: user, comment: comment, creator: user, account: account)
end
endVisit previews at http://localhost:3000/rails/mailers.
Pattern 7: Minimal Email Layout
<%# app/views/layouts/mailer.text.erb %>
<%= yield %>
---
<%= @account&.name || "Example App" %>
<%= root_url %>
<%# app/views/layouts/mailer.html.erb %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { margin: 0; padding: 0; font-family: -apple-system, sans-serif;
font-size: 16px; line-height: 1.5; color: #333; background: #f5f5f5; }
a { color: #0066cc; }
</style>
</head>
<body>
<table style="width: 100%; max-width: 600px; margin: 0 auto;" role="presentation">
<tr><td style="background: white; padding: 40px 30px;"><%= yield %></td></tr>
<tr><td style="padding: 20px 30px; text-align: center; color: #999; font-size: 12px;">
<%= @account&.name || "Example App" %><br><%= link_to root_url, root_url %>
</td></tr>
</table>
</body>
</html>Delivery Configuration
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.default_url_options = { host: ENV["APP_HOST"] }
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# config/environments/test.rb
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: "example.com" }Boundaries
Always
- Create both
.text.erband.html.erbtemplates - Use
deliver_laterfor individual emails;deliver_nowis acceptable inside background jobs that already run asynchronously (e.g., digest delivery jobs) - Check user email preferences before sending
- Include unsubscribe links in emails
- Use inline CSS for HTML emails (no external stylesheets)
- Scope emails to account context
- Create email previews for development
Ask First
- Digest frequency and bundling thresholds
- Whether to include inline attachments (logos)
- SMTP provider configuration
Never
- Send one email per event (bundle notifications)
- Mix marketing and transactional emails
- Use external CSS in email templates
- Build email service abstraction layers
- Send emails synchronously in request cycle
- Skip email preferences check
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

