Reputation: 3373
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 [email protected] From: [email protected] To: [email protected] 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: inlineDear 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--
Upvotes: 29
Views: 20306
Reputation: 21
For Rails 6.1
def example(user, attachments_array)
mixed = mail(
to: '[email protected]',
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
Upvotes: 2
Reputation: 15168
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|
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
Upvotes: 23
Reputation: 19263
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
Upvotes: 5
Reputation: 2467
@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
Upvotes: 18
Reputation: 99
N.B. Rails 3.2 solution.
Just like these multipart emails, there are multiple parts to this answer, so I'll dissect accordingly:
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
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.
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
Upvotes: 3
Reputation: 81
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
Upvotes: 6
Reputation: 5105
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.
Upvotes: 5
Reputation: 556
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 => "[email protected]",
: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
Upvotes: 12
Reputation: 131
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 => '[email protected]', :to => '[email protected]',
: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
Upvotes: 13