action-mailer-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAction Mailer Patterns for Rails 8
Rails 8 中的 Action Mailer 实践模式
Overview
概述
Action Mailer handles transactional emails in Rails:
- HTML and text email templates
- Layouts for consistent styling
- Previews for development
- Background delivery via Active Job
- Internationalized emails
Action Mailer 负责处理 Rails 中的事务性邮件:
- HTML 和纯文本邮件模板
- 用于统一样式的布局
- 开发环境下的邮件预览
- 通过 Active Job 实现后台发送
- 多语言国际化邮件
Quick Start
快速开始
bash
undefinedbash
undefinedGenerate mailer
生成邮件发送器
bin/rails generate mailer User welcome password_reset
bin/rails generate mailer User welcome password_reset
This creates:
这会生成以下文件:
- app/mailers/user_mailer.rb
- app/mailers/user_mailer.rb
- app/views/user_mailer/welcome.html.erb
- app/views/user_mailer/welcome.html.erb
- app/views/user_mailer/welcome.text.erb
- app/views/user_mailer/welcome.text.erb
- spec/mailers/user_mailer_spec.rb (if using RSpec)
- spec/mailers/user_mailer_spec.rb (如果使用 RSpec)
undefinedundefinedProject Structure
项目结构
app/
├── mailers/
│ ├── application_mailer.rb # Base mailer
│ └── user_mailer.rb
├── views/
│ ├── layouts/
│ │ └── mailer.html.erb # Email layout
│ └── user_mailer/
│ ├── welcome.html.erb
│ ├── welcome.text.erb
│ ├── password_reset.html.erb
│ └── password_reset.text.erb
spec/
├── mailers/
│ ├── user_mailer_spec.rb
│ └── previews/
│ └── user_mailer_preview.rbapp/
├── mailers/
│ ├── application_mailer.rb # 基础邮件发送器
│ └── user_mailer.rb
├── views/
│ ├── layouts/
│ │ └── mailer.html.erb # 邮件布局
│ └── user_mailer/
│ ├── welcome.html.erb
│ ├── welcome.text.erb
│ ├── password_reset.html.erb
│ └── password_reset.text.erb
spec/
├── mailers/
│ ├── user_mailer_spec.rb
│ └── previews/
│ └── user_mailer_preview.rbTDD Workflow
TDD 工作流
Mailer Progress:
- [ ] Step 1: Write mailer spec (RED)
- [ ] Step 2: Run spec (fails)
- [ ] Step 3: Create mailer method
- [ ] Step 4: Create email templates
- [ ] Step 5: Run spec (GREEN)
- [ ] Step 6: Create preview
- [ ] Step 7: Test delivery integration邮件发送器开发进度:
- [ ] 步骤1:编写邮件发送器测试用例(失败状态)
- [ ] 步骤2:运行测试用例(执行失败)
- [ ] 步骤3:创建邮件发送器方法
- [ ] 步骤4:创建邮件模板
- [ ] 步骤5:运行测试用例(通过状态)
- [ ] 步骤6:创建邮件预览
- [ ] 步骤7:测试邮件发送集成Configuration
配置
Base Setup
基础设置
ruby
undefinedruby
undefinedconfig/environments/development.rb
config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
config/environments/production.rb
config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: "example.com" }
config.action_mailer.smtp_settings = {
address: "smtp.example.com",
port: 587,
user_name: Rails.application.credentials.smtp[:user_name],
password: Rails.application.credentials.smtp[:password],
authentication: "plain",
enable_starttls_auto: true
}
undefinedconfig.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: "example.com" }
config.action_mailer.smtp_settings = {
address: "smtp.example.com",
port: 587,
user_name: Rails.application.credentials.smtp[:user_name],
password: Rails.application.credentials.smtp[:password],
authentication: "plain",
enable_starttls_auto: true
}
undefinedApplication Mailer
应用级邮件发送器
ruby
undefinedruby
undefinedapp/mailers/application_mailer.rb
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout "mailer"
Shared helper for all mailers
helper_method :app_name
private
def app_name
Rails.application.class.module_parent_name
end
end
undefinedclass ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout "mailer"
所有邮件发送器共享的辅助方法
helper_method :app_name
private
def app_name
Rails.application.class.module_parent_name
end
end
undefinedTesting Mailers
测试邮件发送器
Mailer Spec
邮件发送器测试用例
ruby
undefinedruby
undefinedspec/mailers/user_mailer_spec.rb
spec/mailers/user_mailer_spec.rb
require "rails_helper"
RSpec.describe UserMailer, type: :mailer do
describe "#welcome" do
let(:user) { create(:user, email_address: "user@example.com", name: "John") }
let(:mail) { described_class.welcome(user) }
it "renders the headers" do
expect(mail.subject).to eq(I18n.t("user_mailer.welcome.subject"))
expect(mail.to).to eq(["user@example.com"])
expect(mail.from).to eq(["noreply@example.com"])
end
it "renders the HTML body" do
expect(mail.html_part.body.to_s).to include("John")
expect(mail.html_part.body.to_s).to include("Welcome")
end
it "renders the text body" do
expect(mail.text_part.body.to_s).to include("John")
expect(mail.text_part.body.to_s).to include("Welcome")
end
it "includes login link" do
expect(mail.html_part.body.to_s).to include(new_session_url)
endend
describe "#password_reset" do
let(:user) { create(:user) }
let(:token) { "reset-token-123" }
let(:mail) { described_class.password_reset(user, token) }
it "renders the headers" do
expect(mail.subject).to eq(I18n.t("user_mailer.password_reset.subject"))
expect(mail.to).to eq([user.email_address])
end
it "includes reset link with token" do
expect(mail.html_part.body.to_s).to include(token)
end
it "expires link information" do
expect(mail.html_part.body.to_s).to include("24 hours")
endend
end
undefinedrequire "rails_helper"
RSpec.describe UserMailer, type: :mailer do
describe "#welcome" do
let(:user) { create(:user, email_address: "user@example.com", name: "John") }
let(:mail) { described_class.welcome(user) }
it "渲染邮件头" do
expect(mail.subject).to eq(I18n.t("user_mailer.welcome.subject"))
expect(mail.to).to eq(["user@example.com"])
expect(mail.from).to eq(["noreply@example.com"])
end
it "渲染HTML正文" do
expect(mail.html_part.body.to_s).to include("John")
expect(mail.html_part.body.to_s).to include("Welcome")
end
it "渲染纯文本正文" do
expect(mail.text_part.body.to_s).to include("John")
expect(mail.text_part.body.to_s).to include("Welcome")
end
it "包含登录链接" do
expect(mail.html_part.body.to_s).to include(new_session_url)
endend
describe "#password_reset" do
let(:user) { create(:user) }
let(:token) { "reset-token-123" }
let(:mail) { described_class.password_reset(user, token) }
it "渲染邮件头" do
expect(mail.subject).to eq(I18n.t("user_mailer.password_reset.subject"))
expect(mail.to).to eq([user.email_address])
end
it "包含带令牌的重置链接" do
expect(mail.html_part.body.to_s).to include(token)
end
it "包含链接过期信息" do
expect(mail.html_part.body.to_s).to include("24 hours")
endend
end
undefinedTesting Delivery
测试邮件发送
ruby
undefinedruby
undefinedspec/services/user_registration_service_spec.rb
spec/services/user_registration_service_spec.rb
RSpec.describe UserRegistrationService do
describe "#call" do
it "sends welcome email" do
expect {
described_class.new.call(user_params)
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
end
RSpec.describe UserRegistrationService do
describe "#call" do
it "发送欢迎邮件" do
expect {
described_class.new.call(user_params)
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
end
Integration test
集成测试
RSpec.describe "User Registration", type: :request do
it "sends welcome email after registration" do
expect {
post registrations_path, params: valid_params
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
undefinedRSpec.describe "User Registration", type: :request do
it "注册后发送欢迎邮件" do
expect {
post registrations_path, params: valid_params
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
undefinedMailer Implementation
邮件发送器实现
Basic Mailer
基础邮件发送器
ruby
undefinedruby
undefinedapp/mailers/user_mailer.rb
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
@login_url = new_session_url
mail(
to: @user.email_address,
subject: t(".subject")
)end
def password_reset(user, token)
@user = user
@token = token
@reset_url = edit_password_url(token: token)
@expires_in = "24 hours"
mail(
to: @user.email_address,
subject: t(".subject")
)end
end
undefinedclass UserMailer < ApplicationMailer
def welcome(user)
@user = user
@login_url = new_session_url
mail(
to: @user.email_address,
subject: t(".subject")
)end
def password_reset(user, token)
@user = user
@token = token
@reset_url = edit_password_url(token: token)
@expires_in = "24 hours"
mail(
to: @user.email_address,
subject: t(".subject")
)end
end
undefinedMailer with Attachments
带附件的邮件发送器
ruby
class ReportMailer < ApplicationMailer
def monthly_report(user, report)
@user = user
@report = report
# Attach file
attachments["report-#{Date.current}.pdf"] = report.to_pdf
# Attach inline image
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
mail(to: @user.email_address, subject: t(".subject"))
end
endruby
class ReportMailer < ApplicationMailer
def monthly_report(user, report)
@user = user
@report = report
# 附加文件
attachments["report-#{Date.current}.pdf"] = report.to_pdf
# 附加内嵌图片
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
mail(to: @user.email_address, subject: t(".subject"))
end
endMailer with Dynamic Sender
动态发件人的邮件发送器
ruby
class NotificationMailer < ApplicationMailer
def notify(recipient, sender, message)
@recipient = recipient
@sender = sender
@message = message
mail(
to: @recipient.email_address,
from: "#{@sender.name} <notifications@example.com>",
reply_to: @sender.email_address,
subject: t(".subject", sender: @sender.name)
)
end
endruby
class NotificationMailer < ApplicationMailer
def notify(recipient, sender, message)
@recipient = recipient
@sender = sender
@message = message
mail(
to: @recipient.email_address,
from: "#{@sender.name} <notifications@example.com>",
reply_to: @sender.email_address,
subject: t(".subject", sender: @sender.name)
)
end
endEmail Templates
邮件模板
HTML Template
HTML 模板
erb
<%# app/views/user_mailer/welcome.html.erb %>
<h1><%= t(".greeting", name: @user.name) %></h1>
<p><%= t(".intro") %></p>
<p><%= t(".getting_started") %></p>
<ul>
<li><%= t(".step_1") %></li>
<li><%= t(".step_2") %></li>
<li><%= t(".step_3") %></li>
</ul>
<p>
<%= link_to t(".login_button"), @login_url, class: "button" %>
</p>
<p><%= t(".help_text_html", support_email: mail_to("support@example.com")) %></p>erb
<%# app/views/user_mailer/welcome.html.erb %>
<h1><%= t(".greeting", name: @user.name) %></h1>
<p><%= t(".intro") %></p>
<p><%= t(".getting_started") %></p>
<ul>
<li><%= t(".step_1") %></li>
<li><%= t(".step_2") %></li>
<li><%= t(".step_3") %></li>
</ul>
<p>
<%= link_to t(".login_button"), @login_url, class: "button" %>
</p>
<p><%= t(".help_text_html", support_email: mail_to("support@example.com")) %></p>Text Template
纯文本模板
erb
<%# app/views/user_mailer/welcome.text.erb %>
<%= t(".greeting", name: @user.name) %>
<%= t(".intro") %>
<%= t(".getting_started") %>
* <%= t(".step_1") %>
* <%= t(".step_2") %>
* <%= t(".step_3") %>
<%= t(".login_prompt") %>: <%= @login_url %>
<%= t(".help_text", support_email: "support@example.com") %>erb
<%# app/views/user_mailer/welcome.text.erb %>
<%= t(".greeting", name: @user.name) %>
<%= t(".intro") %>
<%= t(".getting_started") %>
* <%= t(".step_1") %>
* <%= t(".step_2") %>
* <%= t(".step_3") %>
<%= t(".login_prompt") %>: <%= @login_url %>
<%= t(".help_text", support_email: "support@example.com") %>Email Layout
邮件布局
erb
<%# app/views/layouts/mailer.html.erb %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #0066cc;
color: #ffffff;
text-decoration: none;
border-radius: 4px;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<%= yield %>
<div class="footer">
<p><%= t("mailer.footer.company_name") %></p>
<p><%= t("mailer.footer.address") %></p>
</div>
</body>
</html>erb
<%# app/views/layouts/mailer.html.erb %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #0066cc;
color: #ffffff;
text-decoration: none;
border-radius: 4px;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<%= yield %>
<div class="footer">
<p><%= t("mailer.footer.company_name") %></p>
<p><%= t("mailer.footer.address") %></p>
</div>
</body>
</html>Previews
邮件预览
Creating Previews
创建预览
ruby
undefinedruby
undefinedspec/mailers/previews/user_mailer_preview.rb
spec/mailers/previews/user_mailer_preview.rb
OR test/mailers/previews/user_mailer_preview.rb
或 test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome
user = User.first || FactoryBot.build(:user, name: "Preview User")
UserMailer.welcome(user)
end
def password_reset
user = User.first || FactoryBot.build(:user)
UserMailer.password_reset(user, "preview-token-123")
end
end
Access previews at: `http://localhost:3000/rails/mailers`class UserMailerPreview < ActionMailer::Preview
def welcome
user = User.first || FactoryBot.build(:user, name: "预览用户")
UserMailer.welcome(user)
end
def password_reset
user = User.first || FactoryBot.build(:user)
UserMailer.password_reset(user, "preview-token-123")
end
end
访问预览地址:`http://localhost:3000/rails/mailers`Preview with Different States
不同状态的预览
ruby
class OrderMailerPreview < ActionMailer::Preview
def confirmation
order = Order.last || build_preview_order
OrderMailer.confirmation(order)
end
def confirmation_with_discount
order = build_preview_order
order.discount_cents = 1000
OrderMailer.confirmation(order)
end
def confirmation_multiple_items
order = build_preview_order
3.times { order.line_items.build(product: Product.first, quantity: 2) }
OrderMailer.confirmation(order)
end
private
def build_preview_order
Order.new(
user: User.first,
total_cents: 5000,
created_at: Time.current
)
end
endruby
class OrderMailerPreview < ActionMailer::Preview
def confirmation
order = Order.last || build_preview_order
OrderMailer.confirmation(order)
end
def confirmation_with_discount
order = build_preview_order
order.discount_cents = 1000
OrderMailer.confirmation(order)
end
def confirmation_multiple_items
order = build_preview_order
3.times { order.line_items.build(product: Product.first, quantity: 2) }
OrderMailer.confirmation(order)
end
private
def build_preview_order
Order.new(
user: User.first,
total_cents: 5000,
created_at: Time.current
)
end
endInternationalization
国际化
Locale Files
语言包文件
yaml
undefinedyaml
undefinedconfig/locales/mailers/en.yml
config/locales/mailers/en.yml
en:
user_mailer:
welcome:
subject: "Welcome to Our App!"
greeting: "Hello %{name}!"
intro: "Thanks for signing up."
getting_started: "Here's how to get started:"
step_1: "Complete your profile"
step_2: "Explore features"
step_3: "Connect with others"
login_button: "Log In Now"
login_prompt: "Log in here"
help_text: "Questions? Email us at %{support_email}"
help_text_html: "Questions? Email us at %{support_email}"
password_reset:
subject: "Reset Your Password"
mailer:
footer:
company_name: "Example Company"
address: "123 Main St, City, Country"
en:
user_mailer:
welcome:
subject: "Welcome to Our App!"
greeting: "Hello %{name}!"
intro: "Thanks for signing up."
getting_started: "Here's how to get started:"
step_1: "Complete your profile"
step_2: "Explore features"
step_3: "Connect with others"
login_button: "Log In Now"
login_prompt: "Log in here"
help_text: "Questions? Email us at %{support_email}"
help_text_html: "Questions? Email us at %{support_email}"
password_reset:
subject: "Reset Your Password"
mailer:
footer:
company_name: "Example Company"
address: "123 Main St, City, Country"
config/locales/mailers/fr.yml
config/locales/mailers/fr.yml
fr:
user_mailer:
welcome:
subject: "Bienvenue sur notre application !"
greeting: "Bonjour %{name} !"
# ...
undefinedfr:
user_mailer:
welcome:
subject: "Bienvenue sur notre application !"
greeting: "Bonjour %{name} !"
# ...
undefinedSending Localized Emails
发送多语言邮件
ruby
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
I18n.with_locale(user.locale || I18n.default_locale) do
mail(to: @user.email_address, subject: t(".subject"))
end
end
endruby
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
I18n.with_locale(user.locale || I18n.default_locale) do
mail(to: @user.email_address, subject: t(".subject"))
end
end
endDelivery Methods
发送方式
Immediate Delivery (Avoid in production)
立即发送(生产环境避免使用)
ruby
UserMailer.welcome(user).deliver_nowruby
UserMailer.welcome(user).deliver_nowBackground Delivery (Preferred)
后台发送(推荐使用)
ruby
undefinedruby
undefinedUses Active Job
使用 Active Job
UserMailer.welcome(user).deliver_later
UserMailer.welcome(user).deliver_later
With options
带选项的发送
UserMailer.welcome(user).deliver_later(wait: 5.minutes)
UserMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
UserMailer.welcome(user).deliver_later(queue: :mailers)
undefinedUserMailer.welcome(user).deliver_later(wait: 5.minutes)
UserMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
UserMailer.welcome(user).deliver_later(queue: :mailers)
undefinedFrom Services
从服务中调用
ruby
class UserRegistrationService
def call(params)
user = User.create!(params)
UserMailer.welcome(user).deliver_later
success(user)
end
endruby
class UserRegistrationService
def call(params)
user = User.create!(params)
UserMailer.welcome(user).deliver_later
success(user)
end
endCommon Patterns
常见实践模式
Conditional Emails
条件发送邮件
ruby
class NotificationMailer < ApplicationMailer
def daily_digest(user)
@user = user
@notifications = user.notifications.unread.today
# Don't send if nothing to report
return if @notifications.empty?
mail(to: @user.email_address, subject: t(".subject"))
end
endruby
class NotificationMailer < ApplicationMailer
def daily_digest(user)
@user = user
@notifications = user.notifications.unread.today
# 无内容时不发送
return if @notifications.empty?
mail(to: @user.email_address, subject: t(".subject"))
end
endBulk Emails with Batching
批量邮件分批发送
ruby
class NewsletterJob < ApplicationJob
def perform
User.subscribed.find_each(batch_size: 100) do |user|
NewsletterMailer.weekly(user).deliver_later
end
end
endruby
class NewsletterJob < ApplicationJob
def perform
User.subscribed.find_each(batch_size: 100) do |user|
NewsletterMailer.weekly(user).deliver_later
end
end
endEmail Callbacks
邮件回调
ruby
class ApplicationMailer < ActionMailer::Base
after_action :log_delivery
private
def log_delivery
Rails.logger.info("Sending #{action_name} to #{mail.to}")
end
endruby
class ApplicationMailer < ActionMailer::Base
after_action :log_delivery
private
def log_delivery
Rails.logger.info("Sending #{action_name} to #{mail.to}")
end
endChecklist
检查清单
- Mailer spec written first (RED)
- Mailer method created
- HTML template created
- Text template created
- Uses I18n for all text
- Preview created
- Uses (not
deliver_later)deliver_now - Email layout styled
- All specs GREEN
- 先编写邮件发送器测试用例(失败状态)
- 创建邮件发送器方法
- 创建HTML模板
- 创建纯文本模板
- 所有文本使用国际化I18n
- 创建邮件预览
- 使用 (而非
deliver_later)deliver_now - 邮件布局已样式化
- 所有测试用例通过(成功状态)