使用 ActionMailer 发送多部分邮件时出现问题

Posted

技术标签:

【中文标题】使用 ActionMailer 发送多部分邮件时出现问题【英文标题】:Problem sending multipart mail using ActionMailer 【发布时间】:2010-11-10 06:07:07 【问题描述】:

我正在使用以下代码在 rails 中发送电子邮件:

class InvoiceMailer < ActionMailer::Base

  def invoice(invoice)
    from          CONFIG[:email]
    recipients    invoice.email
    subject       "Bevestiging Inschrijving #invoice.course.name"
    content_type  "multipart/alternative"

    part "text/html" do |p|
      p.body = render_message 'invoice_html', :invoice => invoice
    end

    part "text/plain" do |p|
      p.body = render_message 'invoice_plain', :invoice => invoice
    end

    pdf = Prawn::Document.new(:page_size => 'A4')
    PDFRenderer.render_invoice(pdf, invoice)
    attachment :content_type => "application/pdf", :body => pdf.render, :filename => "factuur.pdf"

    invoice.course.course_files.each do |file|
      attachment :content_type => file.content_type, :body => File.read(file.full_path), :filename => file.filename
    end
  end

end

这对我来说似乎很好,并且电子邮件也会像在 Gmail 网络界面中一样显示。但是,在 Mail(Apple 程序)中,我只收到 1 个附件(应该有 2 个)并且没有文本。我似乎无法弄清楚是什么原因造成的。

我从日志中复制了电子邮件:

发送邮件至 xxx@gmail.com 发件人:yyy@gmail.com 至:xxx@gmail.com 主题:Bevestiging Inschrijving Authentiek Spreken 哑剧版:1.0 内容类型:多部分/替代;边界=mimepart_4a5b035ea0d4_769515bbca0ce9b412a --mimepart_4a5b035ea0d4_769515bbca0ce9b412a 内容类型:文本/html;字符集=utf-8 内容传输编码:引用打印 内容处置:内联

尊敬的先生

= --mimepart_4a5b035ea0d4_769515bbca0ce9b412a 内容类型:文本/纯文本;字符集=utf-8 内容传输编码:引用打印 内容处置:内联 尊敬的先生 * 富= --mimepart_4a5b035ea0d4_769515bbca0ce9b412a 内容类型:应用程序/pdf;名称=factuur.pdf 内容传输编码:Base64 内容处置:附件;文件名=factuur.pdf JVBERi0xLjMK/////woxIDAgb2JqCjw8IC9DcmVhdG9yIChQcmF3bikKL1By b2R1Y2VyIChQcmF3bikKPj4KZW5kb2JqCjIgMCBvYmoKPDwgL0NvdW50IDEK ………… MCBuIAp0cmFpbGVyCjw8IC9JbmZvIDEgMCBSCi9TaXplIDExCi9Sb290IDMg MCBSCj4+CnN0YXJ0eHJlZgo4Nzc1CiUlRU9GCg== --mimepart_4a5b035ea0d4_769515bbca0ce9b412a 内容类型:应用程序/pdf;名称=Spelregels.pdf 内容传输编码:Base64 内容处置:附件;文件名=Spelregels.pdf JVBERi0xLjQNJeLjz9MNCjYgMCBvYmoNPDwvTGluZWFyaXplZCAxL0wgMjEx NjYvTyA4L0UgMTY5NTIvTiAxL1QgMjEwMDAvSCBbIDg3NiAxOTJdPj4NZW5k ………… MDIwNzQ4IDAwMDAwIG4NCnRyYWlsZXINCjw8L1NpemUgNj4+DQpzdGFydHhy ZWYNCjExNg0KJSVFT0YNCg== --mimepart_4a5b035ea0d4_769515bbca0ce9b412a--

【问题讨论】:

【参考方案1】:

我怀疑问题在于您将整个电子邮件定义为多部分/替代,这表明每个部分只是同一消息的替代视图。

我使用类似下面的东西来发送带有附件的混合 html/plain 电子邮件,它似乎工作正常。

class InvoiceMailer < ActionMailer::Base

  def invoice(invoice)
    from          CONFIG[:email]
    recipients    invoice.email
    subject       "Bevestiging Inschrijving #invoice.course.name"
    content_type  "multipart/mixed"

    part(:content_type => "multipart/alternative") do |p|
      p.part "text/html" do |p|
        p.body = render_message 'invoice_html', :invoice => invoice
      end

      p.part "text/plain" do |p|
        p.body = render_message 'invoice_plain', :invoice => invoice
      end
    end

    pdf = Prawn::Document.new(:page_size => 'A4')
    PDFRenderer.render_invoice(pdf, invoice)
    attachment :content_type => "application/pdf", :body => pdf.render, :filename => "factuur.pdf"

    invoice.course.course_files.each do |file|
      attachment :content_type => file.content_type, :body => File.read(file.full_path), :filename => file.filename
    end
  end

end

【讨论】:

请注意,投票最多的答案适用于 Rails 2.x,并且有几个答案适用于 Rails 3(@jcoleman,@Corin)。第一次扫描此线程时,我错过了 Rails 3 的答案并继续前进,然后重新阅读线程并让一切正常运行。 在上面的代码中,我认为它需要是p.part "text/html" 和p.part "text/plain",而不仅仅是part "text/html" ...跨度> 【参考方案2】:

@jcoleman 是正确的,但如果您不想使用他的 gem,那么这可能是一个更好的解决方案:

class MyEmailerClass < ActionMailer::Base
  def my_email_method(address, attachment, logo)

    # Add inline attachments first so views can reference them
    attachments.inline['logo.png'] = logo

    # Call mail as per normal but keep a reference to it
    mixed = mail(:to => address) do |format|
      format.html
      format.text
    end

    # All the message parts from above will be nested into a new 'multipart/related'
    mixed.add_part(Mail::Part.new do
      content_type 'multipart/related'
      mixed.parts.delete_if  |p| add_part p 
    end)
    # Set the message content-type to be 'multipart/mixed'
    mixed.content_type 'multipart/mixed'
    mixed.header['content-type'].parameters[:boundary] = mixed.body.boundary

    # Continue adding attachments normally
    attachments['attachment.pdf'] = attachment
  end
end

此代码首先创建以下 MIME 层次结构:

multipart/related multipart/alternative text/html text/plain image/png

在调用mail 之后,我们创建一个新的multipart/related 部件并添加现有部件的子级(在我们进行时删除它们)。然后我们将Content-Type 强制为multipart/mixed 并继续添加附件,生成的MIME 层次结构:

multipart/mixed multipart/related multipart/alternative text/html text/plain image/png application/pdf

【讨论】:

谢谢!经过2个小时的各种尝试,终于奏效了。请投票赞成这个答案,以便它与 Rails 2.x 一起看到。【参考方案3】:

在这点上向 James 致敬,因为它帮助我让我们的邮件程序正常工作。

对此稍作改进:首先,我们使用块中的块参数来添加部分(我没有这样做时遇到了问题)。

另外,如果你想使用布局,你必须直接使用#render。这是两个原则在工作中的一个例子。如上所示,您需要确保将 html 部分保留在最后。

  def message_with_attachment_and_layout( options )
    from options[:from]
    recipients options[:to]
    subject options[:subject]
    content_type    "multipart/mixed"
    part :content_type => 'multipart/alternative' do |copy|
      copy.part :content_type => 'text/plain' do |plain|
        plain.body = render( :file => "#options[:render].text.plain", 
          :layout => 'email', :body => options )
      end
      copy.part :content_type => 'text/html' do |html|
        html.body = render( :file => "#options[:render].text.html", 
          :layout => 'email', :body => options )
      end
    end
    attachment :content_type => "application/pdf", 
      :filename => options[:attachment][:filename],
      :body => File.read( options[:attachment][:path] + '.pdf' )
  end

此示例使用选项散列创建带有附件和布局的通用多部分消息,您可以这样使用:

TestMailer.deliver_message_with_attachment_and_layout( 
  :from => 'a@fubar.com', :to => 'b@fubar.com', 
  :subject => 'test', :render => 'test', 
  :attachment =>  :filename => 'A Nice PDF', 
    :path => 'path/to/some/nice/pdf'  )

(我们实际上并没有这样做:让每个邮件程序为您填写很多这些细节会更好,但我认为这会更容易理解代码。)

希望对您有所帮助。祝你好运。

问候, 丹

【讨论】:

谢谢。您的回答和Ruby Guide 帮助我解决了多部分电子邮件和附件的类似问题。【参考方案4】:

Rails 3 处理邮件的方式不同——虽然简单的情况更容易,但为具有替代内容类型和(内联)附件的多部分电子邮件添加正确的 MIME 层次结构相当复杂(主要是因为所需的层次结构非常复杂。)

Phil 的回答似乎有效,但附件在 iPhone(可能还有其他设备)上不可见,因为 MIME 层次结构仍然不正确。

正确的 MIME 层次结构最终如下所示:

multipart/mixed multipart/alternative multipart/related text/html image/png(例如,对于内联附件;pdf 是另一个很好的例子) text/plain application/zip(例如用于附件——非内联)

我发布了一个有助于支持正确层次结构的 gem: https://github.com/jcoleman/mail_alternatives_with_attachments

通常在使用 ActionMailer 3 时,您会使用以下代码创建一条消息:

class MyEmailerClass < ActionMailer::Base
  def my_email_method(address)
    mail :to => address, 
         :from => "noreply@myemail.com",
         :subject => "My Subject"
  end
end

使用此 gem 来创建包含备选方案和附件的电子邮件,您将使用以下代码:

class MyEmailerClass < ActionMailer::Base
  def my_email_method(address, attachment, logo)
    message = prepare_message to: address, subject: "My Subject", :content_type => "multipart/mixed"

    message.alternative_content_types_with_attachment(
      :text => render_to_string(:template => "my_template.text"),
      :html => render_to_string("my_template.html")
    ) do |inline_attachments|
      inline_attachments.inline['logo.png'] = logo
    end

    attachments['attachment.pdf'] = attachment

    message
  end
end

【讨论】:

我检查了@jcoleman 的gem,但我不确定我是否遗漏了什么......如果你在加载内联附件之前呈现html 字符串,你如何引用他们的CID ?如果 my_template.html 仅将 logo.png 引用为图像源,则生成的电子邮件在多个客户端(例如 Hotmail)上呈现良好,但在其他客户端(iPhone、Apple Mail)上呈现不佳,因为这些客户端使用内联附件的 CID。这里有什么提示吗? @JuanCarlosMéndez:我没有遇到这种需要(我假设这里的区别是您在 HTML 中使用内联附件,而我设计它时仅考虑是否默认情况下它是内联显示的。)我欢迎在 GitHub 上的项目上提出任何拉取请求。【参考方案5】:

Rails 3 解决方案,带有 pdf 附件的多部分替代电子邮件(html 和纯文本),没有内联附件

以前,当我在 ios 或 osx 上的 mail.app 中打开电子邮件时,我的电子邮件仅显示 pdf 附件,而正文中既没有纯文本也没有 html。 Gmail 从来都不是问题。

我使用了与 Corin 相同的解决方案,但我不需要内联附件。这让我走得很远——除了一个问题——mail.app / iOS 邮件显示的是纯文本而不是 html。这是(如果最终发生的话)是因为替代部分的顺序,首先是 html,然后是文本(为什么这应该是决定性的,但无论如何)。

所以我不得不再做一个改变,相当愚蠢,但它确实有效。添加.reverse!方法。

所以我有

def guest_notification(requirement, message)
 subject     = "Further booking details"
 @booking = requirement.booking
 @message = message

 mixed = mail(:to => [requirement.booking.email], :subject => subject) do |format|
   format.text
   format.html
 end

 mixed.add_part(
  Mail::Part.new do
   content_type 'multipart/alternative'
   # THE ODD BIT vv
   mixed.parts.reverse!.delete_if |p| add_part p 
  end
 )

 mixed.content_type 'multipart/mixed'
 mixed.header['content-type'].parameters[:boundary] = mixed.body.boundary
 attachments['Final_Details.pdf'] = File.read(Rails.root + "public/FinalDetails.pdf")

end

【讨论】:

这是最好的答案。【参考方案6】:

注意:我下面的技术在某些情况下有效。但是,当与内联图像结合使用时,它会导致附件无法显示在 iPhone 邮件上,并且可能不会显示在其他客户端上。请参阅下面 jcoleman 的回答以获得完整的解决方案。

值得注意的是,Rails 现在可以处理这个问题,至少从 3.1rc4 开始。来自 ActionMailer 指南:

class UserMailer < ActionMailer::Base
  def welcome_email(user)
    @user = user
    @url  = user_url(@user)
    attachments['terms.pdf'] = File.read('/path/terms.pdf')
    mail(:to => user.email,
         :subject => "Please see the Terms and Conditions attached")
  end
end

诀窍是在调用mail之前添加附件—在触发问题中提到的三种选择问题之后添加附件。

【讨论】:

要添加的一件事是确保您的视图文件被命名为 *.text.erb 和 *.text.html.erb(如果您有 html 内容),例如 Phil 提供, ActionMailer 会让事情自动发生【参考方案7】:

Rails 4 解决方案

在我们的项目中,我们向客户发送包含公司徽标(内嵌附件)和 PDF(常规附件)的电子邮件。我们采用的解决方法类似于@user1581404 在此处提供的解决方法。

但是在将项目升级到 Rails 4 后,我们不得不寻找新的解决方案,因为在调用 mail 命令后不再允许添加附件。

我们的邮件程序有一个基础邮件程序,我们通过覆盖邮件方法来解决这个问题:

def mail(headers = , &block)
    message = super

    # If there are no regular attachments, we don't have to modify the mail
    return message unless message.parts.any?  |part| part.attachment? && !part.inline? 

    # Combine the html part and inline attachments to prevent issues with clients like iOS
    html_part = Mail::Part.new do
      content_type 'multipart/related'
      message.parts.delete_if  |part| (!part.attachment? || part.inline?) && add_part(part) 
    end

    # Any parts left must be regular attachments
    attachment_parts = message.parts.slice!(0..-1)

    # Reconfigure the message
    message.content_type 'multipart/mixed'
    message.header['content-type'].parameters[:boundary] = message.body.boundary
    message.add_part(html_part)
    attachment_parts.each  |part| message.add_part(part) 

    message
  end

【讨论】:

【参考方案8】:

注意Rails 3.2 解决方案。

就像这些多部分电子邮件一样,这个答案有多个部分,所以我会相应地剖析:

    @Corin 的“混合”方法中纯/html 格式的顺序很重要。我发现后跟 html 的文本给了我所需的功能。 YMMV

    将 Content-Disposition 设置为 nil(以将其删除)修复了其他答案中表达的 iPhone/iOS 附件查看困难。该解决方案已经过测试,适用于 Outlook for Mac、Mac OS/X Mail 和 iOS Mail。我怀疑其他电子邮件客户端也可以。

    与以前版本的 Rails 不同,附件处理与宣传的一样。我最大的问题通常是由尝试旧的解决方法引起的,这些解决方法只会让我的问题更加复杂。

希望这可以帮助某人避免我的陷阱和死胡同。

工作代码:

def example( from_user, quote)
  @quote = quote

  # attach the inline logo
  attachments.inline['logo.png'] = File.read('./public/images/logo.png')

  # attach the pdf quote
  attachments[ 'quote.pdf'] = File.read( 'path/quote.pdf')

  # create a mixed format email body
  mixed = mail( to: @quote.user.email,
                subject: "Quote") do |format|
    format.text
    format.html
  end

  # Set the message content-type to be 'multipart/mixed'
  mixed.content_type 'multipart/mixed'
  mixed.header['content-type'].parameters[:boundary] = mixed.body.boundary

  # Set Content-Disposition to nil to remove it - fixes iOS attachment viewing
  mixed.content_disposition = nil
end

【讨论】:

以上是关于使用 ActionMailer 发送多部分邮件时出现问题的主要内容,如果未能解决你的问题,请参考以下文章

使用 ActionMailer 和 Outlook/Mandrillapp SMTP 服务器发送邮件

在 Rails3 / ActionMailer 中设置 Message-ID 邮件头

设计不通过ActionMailer发送电子邮件

如何使用 Actionmailer/Ruby on Rails 发送具有多个动态 smtp 的电子邮件

使用 ActionMailer 的电子邮件注册确认

ActionMailer发送邮件