Question

I'm using the following code to send emails in 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

It seems fine to me, and the emails also show up like they should in the Gmail web-interface. In Mail (the Apple program), however, I get just 1 attachment (where there should be 2) and there is no text. I just can't seem to figure out what's causing it.

I copied the email from the logs:

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



  
  
  
    

Dear sir

= --mimepart_4a5b035ea0d4_769515bbca0ce9b412a Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: Quoted-printable Content-Disposition: inline Dear sir * 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--
Was it helpful?

Solution

I suspect the issue is you're defining the overall email as multipart/alternative, suggesting each part is just an alternate view of the same message.

I use something like the following to send mixed html/plain emails with attachments, and it seems to work OK.

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|
      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
    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

OTHER TIPS

@jcoleman is correct but if you don't want to use his gem then this may be a better solution:

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

This code begins by creating the following MIME hierarchy:

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

After the call to mail we create a new multipart/related part and add the children of the existing part (removing them as we go). Then we force the Content-Type to be multipart/mixed and continue adding attachments, with the resulting MIME hierarchy:

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

A nod to James on this, as it helped me get our mailer working right.

A slight refinement to this: First, we use the block arguments within the blocks to add parts (I had problems when I didn't).

Also, if you want to use layouts, you have to use #render directly. Here's an example of both principles at work. As shown above, you need to make sure you keep the html part last.

  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

This example uses an options hash to create a generic multipart message with both attachments and layout, which you would use like this:

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' } )

(We don't actually do this: it's nicer to have each mailer fill in a lot of these details for you, but I thought it would make it easier to understand the code.)

Hope that helps. Best of luck.

Regards, Dan

Rails 3 handles mail differently--and while the simple case is easier, adding the correct MIME hierarchy for multipart email with both alternative content types and (inline) attachments is rather complicated (primarily because the hierarchy needed is so complex.)

Phil's answer will seem to work--but the attachments won't be visible on the iPhone (and perhaps other devices) since the MIME hierarchy is still incorrect.

The correct MIME hierarchy ends up looking like this:

  • multipart/mixed
    • multipart/alternative
      • multipart/related
        • text/html
        • image/png (e.g. for an inline attachment; pdf would be another good example)
      • text/plain
    • application/zip (e.g for an attachment--not inline)

I've released a gem that helps support the correct hierarchy: https://github.com/jcoleman/mail_alternatives_with_attachments

Typically when using ActionMailer 3, you would create a message with the following code:

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

Using this gem to create an email with both alternatives and attachments you would use the following code:

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

Note: My technique below works in some instances. However, when combined with inline images, it will cause the attachment to not show on iPhone Mail, and perhaps on other clients. See jcoleman's answer below for a complete solution.

It's worth noting that Rails now handles this, at least as of 3.1rc4. From the ActionMailer guide:

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

The trick is to add the attachment before you make the call to mail—adding the attachment after triggers the three-alternatives problem mentioned in the question.

Rails 3 Solution, multipart alternative email (html and plain) with pdf attachment, no inline attachments

Previously I had emails showing only the pdf attachment and neither plain nor html in the body when they were opened in ios or in mail.app on osx. Gmail has never been a problem.

I used the same solution as Corin, though I didn't need the inline attachment. That got me pretty far - except for one problem - mail.app / iOS mail showed the plain text not the html. This was (if finally transpired) because of the order in which the alternative parts came through, html first and then text (why that should be decisive beats me, but anyway).

So I had to make one more change, rather silly, but it works. add the .reverse! method.

so I have

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

Rails 4 solution

In our project we're sending emails to customer that include a company logo (inline attachment) and a PDF (regular attachment). The workaround that we had in place was similar to the one provided here by @user1581404.

However after upgrading the project to Rails 4 we had to find a new solution because you are no longer allowed to add attachments after calling the mail command.

Our mailers have a base mailer, we fix this issue by overriding the mail method with:

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

N.B. Rails 3.2 solution.

Just like these multipart emails, there are multiple parts to this answer, so I'll dissect accordingly:

  1. The order of plain/html format in @Corin's "mixed" approach is important. I found text followed by html gave me the functionality I required. YMMV

  2. Setting Content-Disposition to nil (to remove it) fixed the iPhone/iOS attachment viewing difficulties expressed in other answers. This solution has been tested as working for Outlook for Mac, Mac OS/X Mail and iOS Mail. I suspect other email clients would work as well.

  3. Unlike with previous versions of Rails, the attachment handling worked as advertised. My biggest issues were usually caused by attempting old workarounds that only compounded the problems for me.

Hope this helps someone avoid my pitfalls and cul-de-sacs.

Working code:

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top