使用ActionMailer发送多部分邮件的问题

29

我正在使用以下代码在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(苹果程序)中,我只收到了一个附件(实际应该有两个),而且没有文本内容。我就是无法弄清楚是什么原因导致这样。

我从日志中复制了邮件:

Sent mail to xxx@gmail.com
From: yyy@gmail.com
To: xxx@gmail.com
Subject: Bevestiging Inschrijving Authentiek Spreken
Mime-Version: 1.0
Content-Type: multipart/alternative; boundary=mimepart_4a5b035ea0d4_769515bbca0ce9b412a
--mimepart_4a5b035ea0d4_769515bbca0ce9b412a Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: Quoted-printable Content-Disposition: inline

亲爱的先生

--mimepart_4a5b035ea0d4_769515bbca0ce9b412a Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: Quoted-printable Content-Disposition: inline 亲爱的先生
* Foo=
--mimepart_4a5b035ea0d4_769515bbca0ce9b412a Content-Type: application/pdf; name=factuur.pdf Content-Transfer-Encoding: Base64 Content-Disposition: attachment; filename=factuur.pdf
JVBERi0xLjMK/////woxIDAgb2JqCjw8IC9DcmVhdG9yIChQcmF3bikKL1By b2R1Y2VyIChQcmF3bikKPj4KZW5kb2JqCjIgMCBvYmoKPDwgL0NvdW50IDEK ... ... ... MCBuIAp0cmFpbGVyCjw8IC9JbmZvIDEgMCBSCi9TaXplIDExCi9Sb290IDMg MCBSCj4+CnN0YXJ0eHJlZgo4Nzc1CiUlRU9GCg==
--mimepart_4a5b035ea0d4_769515bbca0ce9b412a Content-Type: application/pdf; name=Spelregels.pdf Content-Transfer-Encoding: Base64 Content-Disposition: attachment; filename=Spelregels.pdf
JVBERi0xLjQNJeLjz9MNCjYgMCBvYmoNPDwvTGluZWFyaXplZCAxL0wgMjEx NjYvTyA4L0UgMTY5NTIvTiAxL1QgMjEwMDAvSCBbIDg3NiAxOTJdPj4NZW5k ... ... ... MDIwNzQ4IDAwMDAwIG4NCnRyYWlsZXINCjw8L1NpemUgNj4+DQpzdGFydHhy ZWYNCjExNg0KJSVFT0YNCg==
--mimepart_4a5b035ea0d4_769515bbca0ce9b412a--
9个回答

23

我猜想问题出在你将整个电子邮件定义为multipart/alternative,这意味着每个部分只是同一消息的备选视图。

我通常使用以下方式发送混合的HTML /纯文本电子邮件以及附件,它似乎运行良好。

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

2
请注意,得票最高的答案适用于Rails 2.x,还有一些回答涉及Rails 3(@jcoleman,@Corin)。我第一次浏览这个线程时错过了Rails 3的答案并继续前进,然后重新阅读了该线程并使一切正常运作。 - Juan Carlos Méndez
1
在上面的代码中,我认为它需要是 p.part "text/html" 和 p.part "text/plain",而不仅仅是 part "text/html" ... - harrysny

18

@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-Typemultipart/mixed 并继续添加附件,生成的MIME层次结构如下:

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

谢谢!经过两个小时的尝试各种方法,终于成功了。请点赞这个答案,以便它与Rails 2.x一起被看到。 - cassi.lup

13

在这方面要感谢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指南帮助我解决了一个类似的多部分电子邮件和附件问题。 - LapinLove404

12
Rails 3处理邮件不同于以前版本,尽管简单情况更容易处理,但对于含有备选内容和内嵌(inline)附件的多部分电子邮件需要添加正确的MIME层次结构,这相当复杂(主要是因为所需的层次结构非常复杂)。Phil的答案似乎可行,但在iPhone(和其他设备)上附件将无法显示,因为MIME层次结构仍然不正确。正确的MIME层次结构最终看起来像这样:
- multipart/mixed - multipart/alternative - multipart/related - text/html - image/png (例如用于内嵌附件;pdf将是另一个很好的例子) - text/plain - application/zip (例如用于附件--不是内嵌)
我发布了一个宝石,帮助支持正确的层次结构: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的宝石,但我不确定是否遗漏了什么……如果在加载内联附件之前呈现HTML字符串,如何引用它们的CID?如果my_template.html仅将图像源引用为logo.png,则生成的电子邮件在多个客户端上(例如Hotmail)都可以很好地呈现,但在其他客户端(iPhone,Apple Mail)中则无法呈现,因为这些客户端按照内联附件的CID进行操作。有什么提示吗? - Juan Carlos Méndez
1
@JuanCarlosMéndez:我之前没有遇到过这种需求(我猜想你是在HTML中使用内联附件,而我只是设计了默认是否内联显示)。我欢迎在GitHub上的项目中接受任何拉取请求。 - jcoleman

6

Rails 3解决方案,多部分的邮件(包括html和纯文本)并带有pdf附件,无内联附件

之前,当在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

这是最好的答案。 - smoothdvd

5

注意:下面的技巧在某些情况下是有效的。然而,当与内联图片结合使用时,它会导致附件在iPhone Mail上不显示,可能也会在其他客户端上出现类似情况。请参见下面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之前添加附件,添加附件之后会触发问题中提到的三种替代方案。请注意不要更改HTML标签。

1
需要补充的一点是确保你的视图文件命名为*.text.erb和*.text.html.erb(如果你有HTML内容),就像Phil提供的例子一样,ActionMailer会自动完成所有事情。 - user1089194

5

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

3

注意:适用于Rails 3.2解决方案。

就像这些多部分电子邮件一样,这个答案也有多个部分,因此我将进行分解:

  1. @Corin的“混合”方法中普通/ HTML格式的顺序很重要。我发现文本后跟HTML可以给我所需的功能。你的情况可能不同。

  2. 将Content-Disposition设置为nil(以删除它)可以解决其他答案中表达的iPhone / iOS附件查看困难。此解决方案已经过测试,可在Outlook for Mac、Mac OS/X Mail和iOS Mail中使用。我认为其他电子邮件客户端也可以正常工作。

  3. 与以前版本的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

2

针对Rails 6.1版本

def example(user, attachments_array)
    mixed = mail(
      to: 'email@email.com',
      subject: 'my custom title',
      body: 'my custom body'
    )

    mixed.add_part(Mail::Part.new do
      content_type 'multipart/related'
      mixed.parts.delete_if { |p| add_part p }
    end)

    mixed.content_type 'multipart/mixed'
    mixed.header['content-type'].parameters[:boundary] = mixed.body.boundary

# Add attachments
    attachments_array.each do |attachment|
      mixed.attachments[attachment.filename.to_s] = File.read(attachment.path)
    end
  end

1
嘿,感谢您添加答案!13年后仍然有新的解决方案被添加,这让我感到非常温暖。 - Pieter Jongsma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接