Skip to content

Ruby 发送邮件 - SMTP

在现代应用程序中,发送邮件是一个常见的需求,比如用户注册确认、密码重置、通知等。Ruby提供了多种方式来发送邮件,其中最常用的是通过SMTP(Simple Mail Transfer Protocol)协议。本章将详细介绍如何在Ruby中使用SMTP发送邮件。

🎯 SMTP基础

什么是SMTP

SMTP(Simple Mail Transfer Protocol)是用于发送电子邮件的标准协议。它定义了邮件服务器之间以及客户端与服务器之间如何传输邮件。在Ruby中,我们可以使用内置的[Net::SMTP](file:///D:/Workspace/Coding/VueProjects/tutorials-web/docs/ruby/../../../../../../Ruby30-x64/lib/ruby/3.0.0/net/smtp.rb#L89-L774)库来发送邮件。

SMTP工作原理

SMTP发送邮件的基本流程:

  1. 连接到SMTP服务器
  2. 进行身份验证(如果需要)
  3. 指定发件人和收件人
  4. 发送邮件内容
  5. 关闭连接

📧 使用Net::SMTP发送邮件

基本邮件发送

ruby
require 'net/smtp'

# SMTP服务器配置
smtp_server = 'smtp.gmail.com'
port = 587
username = 'your_email@gmail.com'
password = 'your_password'

# 邮件内容
from = 'your_email@gmail.com'
to = 'recipient@example.com'
subject = '测试邮件'
body = "这是一封测试邮件\n\n来自Ruby应用程序"

# 创建邮件消息
message = <<~MESSAGE
  From: #{from}
  To: #{to}
  Subject: #{subject}
  
  #{body}
MESSAGE

# 发送邮件
Net::SMTP.start(smtp_server, port, 'localhost', username, password, :plain) do |smtp|
  smtp.send_message(message, from, to)
end

puts "邮件发送成功!"

发送HTML邮件

ruby
require 'net/smtp'

def send_html_email(smtp_server, port, username, password, from, to, subject, html_body)
  # 创建HTML邮件消息
  message = <<~MESSAGE
    From: #{from}
    To: #{to}
    Subject: #{subject}
    Content-Type: text/html; charset=UTF-8
    
    #{html_body}
  MESSAGE
  
  # 发送邮件
  Net::SMTP.start(smtp_server, port, 'localhost', username, password, :plain) do |smtp|
    smtp.send_message(message, from, to)
  end
end

# 使用示例
smtp_server = 'smtp.gmail.com'
port = 587
username = 'your_email@gmail.com'
password = 'your_password'
from = 'your_email@gmail.com'
to = 'recipient@example.com'
subject = 'HTML测试邮件'

html_body = <<~HTML
  <html>
    <body>
      <h1>欢迎使用我们的服务</h1>
      <p>这是一封HTML格式的邮件</p>
      <ul>
        <li>功能1</li>
        <li>功能2</li>
        <li>功能3</li>
      </ul>
      <p>感谢您的使用!</p>
    </body>
  </html>
HTML

send_html_email(smtp_server, port, username, password, from, to, subject, html_body)
puts "HTML邮件发送成功!"

发送带附件的邮件

ruby
require 'net/smtp'
require 'base64'

def send_email_with_attachment(smtp_server, port, username, password, from, to, subject, body, attachment_path)
  # 读取附件
  filename = File.basename(attachment_path)
  file_content = File.read(attachment_path, mode: 'rb')
  encoded_content = Base64.encode64(file_content).gsub(/\n/, "\n")

  # 创建带附件的邮件
  boundary = "----=_NextPart_#{Time.now.to_i}_#{rand(1000000)}"

  message = <<~MESSAGE
    From: #{from}
    To: #{to}
    Subject: #{subject}
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="#{boundary}"
    
    --#{boundary}
    Content-Type: text/plain; charset=UTF-8
    
    #{body}
    
    --#{boundary}
    Content-Type: application/octet-stream; name="#{filename}"
    Content-Transfer-Encoding: base64
    Content-Disposition: attachment; filename="#{filename}"
    
    #{encoded_content}
    --#{boundary}--
  MESSAGE

  # 发送邮件
  Net::SMTP.start(smtp_server, port, 'localhost', username, password, :plain) do |smtp|
    smtp.send_message(message, from, to)
  end
end

# 使用示例
smtp_server = 'smtp.gmail.com'
port = 587
username = 'your_email@gmail.com'
password = 'your_password'
from = 'your_email@gmail.com'
to = 'recipient@example.com'
subject = '带附件的邮件'
body = "这是一封带附件的邮件\n\n请查看附件内容。"
attachment_path = 'path/to/your/file.pdf'

# send_email_with_attachment(smtp_server, port, username, password, from, to, subject, body, attachment_path)
puts "带附件的邮件发送成功!"

🛠️ 邮件配置管理

配置类

ruby
class EmailConfig
  attr_accessor :smtp_server, :port, :username, :password, :domain
  
  def initialize(smtp_server, port, username, password, domain = 'localhost')
    @smtp_server = smtp_server
    @port = port
    @username = username
    @password = password
    @domain = domain
  end
  
  # 常用邮件服务商配置
  def self.gmail(username, password)
    new('smtp.gmail.com', 587, username, password, 'localhost')
  end
  
  def self.outlook(username, password)
    new('smtp-mail.outlook.com', 587, username, password, 'localhost')
  end
  
  def self.yahoo(username, password)
    new('smtp.mail.yahoo.com', 587, username, password, 'localhost')
  end
  
  def self.qq(username, password)
    new('smtp.qq.com', 587, username, password, 'localhost')
  end
end

# 使用示例
gmail_config = EmailConfig.gmail('your_email@gmail.com', 'your_password')
puts gmail_config.smtp_server  # smtp.gmail.com
puts gmail_config.port         # 587

邮件发送器类

ruby
require 'net/smtp'
require 'base64'

class EmailSender
  def initialize(config)
    @config = config
  end
  
  def send_email(to, subject, body, options = {})
    from = options[:from] || @config.username
    content_type = options[:content_type] || 'text/plain'
    attachments = options[:attachments] || []
    
    message = build_message(from, to, subject, body, content_type, attachments)
    
    Net::SMTP.start(
      @config.smtp_server,
      @config.port,
      @config.domain,
      @config.username,
      @config.password,
      :plain
    ) do |smtp|
      smtp.send_message(message, from, to)
    end
  end
  
  private
  
  def build_message(from, to, subject, body, content_type, attachments)
    if attachments.empty?
      build_simple_message(from, to, subject, body, content_type)
    else
      build_multipart_message(from, to, subject, body, content_type, attachments)
    end
  end
  
  def build_simple_message(from, to, subject, body, content_type)
    <<~MESSAGE
      From: #{from}
      To: #{to}
      Subject: #{subject}
      Content-Type: #{content_type}; charset=UTF-8
      
      #{body}
    MESSAGE
  end
  
  def build_multipart_message(from, to, subject, body, content_type, attachments)
    boundary = "----=_NextPart_#{Time.now.to_i}_#{rand(1000000)}"
    
    message = <<~MESSAGE
      From: #{from}
      To: #{to}
      Subject: #{subject}
      MIME-Version: 1.0
      Content-Type: multipart/mixed; boundary="#{boundary}"
      
      --#{boundary}
      Content-Type: #{content_type}; charset=UTF-8
      
      #{body}
    MESSAGE
    
    attachments.each do |attachment_path|
      filename = File.basename(attachment_path)
      file_content = File.read(attachment_path, mode: 'rb')
      encoded_content = Base64.encode64(file_content).gsub(/\n/, "\n")
      
      message += <<~ATTACHMENT
      
        --#{boundary}
        Content-Type: application/octet-stream; name="#{filename}"
        Content-Transfer-Encoding: base64
        Content-Disposition: attachment; filename="#{filename}"
        
        #{encoded_content}
      ATTACHMENT
    end
    
    message + "\n--#{boundary}--"
  end
end

# 使用示例
config = EmailConfig.gmail('your_email@gmail.com', 'your_password')
sender = EmailSender.new(config)

# 发送简单邮件
sender.send_email(
  'recipient@example.com',
  '测试邮件',
  '这是一封测试邮件'
)

# 发送HTML邮件
sender.send_email(
  'recipient@example.com',
  'HTML邮件',
  '<h1>HTML邮件</h1><p>这是一封HTML邮件</p>',
  content_type: 'text/html'
)

# 发送带附件的邮件
# sender.send_email(
#   'recipient@example.com',
#   '带附件的邮件',
#   '请查看附件',
#   attachments: ['path/to/file1.pdf', 'path/to/file2.jpg']
# )

🔐 安全处理

环境变量管理敏感信息

ruby
# .env文件内容示例
# SMTP_USERNAME=your_email@gmail.com
# SMTP_PASSWORD=your_app_password
# SMTP_SERVER=smtp.gmail.com
# SMTP_PORT=587

require 'net/smtp'

class SecureEmailSender
  def initialize
    @username = ENV['SMTP_USERNAME']
    @password = ENV['SMTP_PASSWORD']
    @smtp_server = ENV['SMTP_SERVER'] || 'smtp.gmail.com'
    @port = (ENV['SMTP_PORT'] || 587).to_i
  end
  
  def send_email(to, subject, body)
    raise "SMTP配置不完整" unless [@username, @password, @smtp_server, @port].all?
    
    from = @username
    message = <<~MESSAGE
      From: #{from}
      To: #{to}
      Subject: #{subject}
      Content-Type: text/plain; charset=UTF-8
      
      #{body}
    MESSAGE
    
    Net::SMTP.start(@smtp_server, @port, 'localhost', @username, @password, :plain) do |smtp|
      smtp.send_message(message, from, to)
    end
  rescue => e
    puts "邮件发送失败: #{e.message}"
    false
  end
end

# 使用示例
# sender = SecureEmailSender.new
# sender.send_email('recipient@example.com', '安全邮件', '这是一封安全邮件')

使用OAuth2认证

ruby
require 'net/smtp'
require 'oauth2'

class OAuth2EmailSender
  def initialize(client_id, client_secret, refresh_token)
    @client_id = client_id
    @client_secret = client_secret
    @refresh_token = refresh_token
  end
  
  def send_email(from, to, subject, body)
    access_token = refresh_access_token
    
    message = <<~MESSAGE
      From: #{from}
      To: #{to}
      Subject: #{subject}
      Content-Type: text/plain; charset=UTF-8
      
      #{body}
    MESSAGE
    
    Net::SMTP.start('smtp.gmail.com', 587, 'localhost', from, access_token, :xoauth2) do |smtp|
      smtp.send_message(message, from, to)
    end
  end
  
  private
  
  def refresh_access_token
    client = OAuth2::Client.new(@client_id, @client_secret, 
      site: 'https://accounts.google.com',
      token_url: '/o/oauth2/token'
    )
    
    token = OAuth2::AccessToken.from_hash(client, {
      refresh_token: @refresh_token,
      expires_at: Time.now.to_i - 100  # 强制刷新
    })
    
    refreshed_token = token.refresh!
    refreshed_token.token
  end
end

🎯 实用示例

用户注册确认邮件

ruby
class UserRegistrationMailer
  def initialize(email_sender)
    @email_sender = email_sender
  end
  
  def send_confirmation_email(user_email, user_name, confirmation_token)
    subject = '欢迎注册 - 请确认您的邮箱'
    
    html_body = <<~HTML
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="UTF-8">
          <title>邮箱确认</title>
        </head>
        <body>
          <h2>欢迎注册我们的平台!</h2>
          <p>亲爱的 #{user_name},</p>
          <p>感谢您注册我们的服务。请点击下面的链接确认您的邮箱地址:</p>
          <p>
            <a href="https://yoursite.com/confirm?token=#{confirmation_token}" 
               style="background-color: #007bff; color: white; padding: 10px 20px; 
                      text-decoration: none; border-radius: 5px;">
              确认邮箱
            </a>
          </p>
          <p>如果您无法点击链接,请复制以下地址到浏览器中打开:</p>
          <p>https://yoursite.com/confirm?token=#{confirmation_token}</p>
          <p>此链接将在24小时后失效。</p>
          <br>
          <p>祝您使用愉快!</p>
          <p>垦荒教程团队</p>
        </body>
      </html>
    HTML
    
    @email_sender.send_email(
      user_email,
      subject,
      html_body,
      content_type: 'text/html'
    )
  end
end

# 使用示例
# config = EmailConfig.gmail(ENV['SMTP_USERNAME'], ENV['SMTP_PASSWORD'])
# sender = EmailSender.new(config)
# mailer = UserRegistrationMailer.new(sender)
# mailer.send_confirmation_email('user@example.com', '张三', 'abc123token')

密码重置邮件

ruby
class PasswordResetMailer
  def initialize(email_sender)
    @email_sender = email_sender
  end
  
  def send_reset_email(user_email, user_name, reset_token)
    subject = '密码重置请求'
    
    html_body = <<~HTML
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="UTF-8">
          <title>密码重置</title>
        </head>
        <body>
          <h2>密码重置请求</h2>
          <p>亲爱的 #{user_name},</p>
          <p>您请求重置您的账户密码。请点击下面的链接来设置新密码:</p>
          <p>
            <a href="https://yoursite.com/reset-password?token=#{reset_token}" 
               style="background-color: #28a745; color: white; padding: 10px 20px; 
                      text-decoration: none; border-radius: 5px;">
              重置密码
            </a>
          </p>
          <p>如果您没有请求密码重置,请忽略此邮件。</p>
          <p>此链接将在1小时内失效。</p>
          <br>
          <p>垦荒教程团队</p>
        </body>
      </html>
    HTML
    
    @email_sender.send_email(
      user_email,
      subject,
      html_body,
      content_type: 'text/html'
    )
  end
end

通知邮件系统

ruby
class NotificationMailer
  def initialize(email_sender)
    @email_sender = email_sender
  end
  
  def send_notification(recipients, subject, message, options = {})
    recipients.each do |recipient|
      begin
        @email_sender.send_email(
          recipient,
          subject,
          message,
          options
        )
        puts "通知邮件已发送至: #{recipient}"
      rescue => e
        puts "发送至 #{recipient} 失败: #{e.message}"
      end
    end
  end
  
  def send_system_alert(admin_emails, alert_message)
    subject = "系统警报 - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
    
    html_body = <<~HTML
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="UTF-8">
          <title>系统警报</title>
        </head>
        <body>
          <h2 style="color: red;">🚨 系统警报</h2>
          <p><strong>时间:</strong> #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}</p>
          <p><strong>消息:</strong></p>
          <div style="background-color: #f8f9fa; padding: 15px; border-left: 4px solid #dc3545;">
            <pre>#{alert_message}</pre>
          </div>
          <p>请立即检查系统状态。</p>
        </body>
      </html>
    HTML
    
    send_notification(
      admin_emails,
      subject,
      html_body,
      content_type: 'text/html'
    )
  end
end

📊 性能优化

批量邮件发送

ruby
class BatchEmailSender
  def initialize(email_sender, batch_size = 50, delay = 1)
    @email_sender = email_sender
    @batch_size = batch_size
    @delay = delay
  end
  
  def send_batch(emails, subject, body, options = {})
    emails.each_slice(@batch_size) do |batch|
      batch.each do |email|
        begin
          @email_sender.send_email(email, subject, body, options)
          puts "邮件已发送至: #{email}"
        rescue => e
          puts "发送至 #{email} 失败: #{e.message}"
        end
      end
      
      # 批次间延迟,避免触发邮件服务商限制
      sleep(@delay) unless batch.equal?(emails.each_slice(@batch_size).to_a.last)
    end
  end
end

# 使用示例
# recipients = ['user1@example.com', 'user2@example.com', ...] # 大量收件人
# config = EmailConfig.gmail(ENV['SMTP_USERNAME'], ENV['SMTP_PASSWORD'])
# sender = EmailSender.new(config)
# batch_sender = BatchEmailSender.new(sender)
# batch_sender.send_batch(recipients, '批量通知', '这是批量发送的通知邮件')

异步邮件发送

ruby
require 'thread'

class AsyncEmailSender
  def initialize(email_sender)
    @email_sender = email_sender
    @queue = Queue.new
    @worker = Thread.new { process_queue }
  end
  
  def send_email_async(to, subject, body, options = {})
    @queue << {
      to: to,
      subject: subject,
      body: body,
      options: options
    }
  end
  
  def shutdown
    @queue << :shutdown
    @worker.join
  end
  
  private
  
  def process_queue
    loop do
      job = @queue.pop
      break if job == :shutdown
      
      begin
        @email_sender.send_email(
          job[:to],
          job[:subject],
          job[:body],
          job[:options]
        )
        puts "异步邮件已发送至: #{job[:to]}"
      rescue => e
        puts "异步发送至 #{job[:to]} 失败: #{e.message}"
      end
    end
  end
end

# 使用示例
# config = EmailConfig.gmail(ENV['SMTP_USERNAME'], ENV['SMTP_PASSWORD'])
# sender = EmailSender.new(config)
# async_sender = AsyncEmailSender.new(sender)
# 
# async_sender.send_email_async('user@example.com', '异步邮件', '这是一封异步发送的邮件')
# 
# # 应用结束前关闭异步发送器
# at_exit { async_sender.shutdown }

🎯 最佳实践

1. 错误处理和重试机制

ruby
class RobustEmailSender
  def initialize(email_sender, max_retries = 3)
    @email_sender = email_sender
    @max_retries = max_retries
  end
  
  def send_email_with_retry(to, subject, body, options = {})
    retries = 0
    
    begin
      @email_sender.send_email(to, subject, body, options)
      puts "邮件发送成功: #{to}"
      true
    rescue => e
      retries += 1
      if retries <= @max_retries
        puts "邮件发送失败,#{2 ** retries}秒后重试 (#{retries}/#{@max_retries}): #{e.message}"
        sleep(2 ** retries)  # 指数退避
        retry
      else
        puts "邮件发送最终失败: #{to} - #{e.message}"
        false
      end
    end
  end
end

2. 邮件模板系统

ruby
class EmailTemplate
  def initialize(template_file)
    @template = File.read(template_file)
  end
  
  def render(bindings = {})
    result = @template.dup
    bindings.each do |key, value|
      result.gsub!("{{#{key}}}", value.to_s)
    end
    result
  end
end

# 邮件模板文件 (welcome_email.html)
=begin
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>欢迎邮件</title>
  </head>
  <body>
    <h1>欢迎, {{name}}!</h1>
    <p>感谢您加入我们。您的注册邮箱是: {{email}}</p>
    <p>注册时间: {{signup_time}}</p>
  </body>
</html>
=end

# 使用示例
# template = EmailTemplate.new('templates/welcome_email.html')
# html_body = template.render(
#   name: '张三',
#   email: 'user@example.com',
#   signup_time: Time.now.strftime('%Y-%m-%d %H:%M:%S')
# )

3. 邮件日志记录

ruby
class EmailLogger
  def self.log_email(to, subject, status, error = nil)
    log_entry = {
      timestamp: Time.now,
      to: to,
      subject: subject,
      status: status,
      error: error
    }
    
    # 写入日志文件
    File.open('email_log.txt', 'a') do |file|
      file.puts log_entry.inspect
    end
    
    # 或者发送到日志系统
    puts "[EMAIL_LOG] #{log_entry}"
  end
end

class LoggedEmailSender
  def initialize(email_sender)
    @email_sender = email_sender
  end
  
  def send_email(to, subject, body, options = {})
    begin
      result = @email_sender.send_email(to, subject, body, options)
      EmailLogger.log_email(to, subject, 'success')
      result
    rescue => e
      EmailLogger.log_email(to, subject, 'failed', e.message)
      raise e
    end
  end
end

📚 下一步学习

掌握了Ruby SMTP邮件发送后,建议继续学习:

继续您的Ruby学习之旅吧!

本站内容仅供学习和研究使用。