Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Attachments not visible in mail clients when additional inline attachments present #2686

Open
icanhasserver opened this Issue · 39 comments
@icanhasserver

When assembling an email with mixed inline / normal attachments, only the inline attachments (i.e. images) are shown. Some mail clients don't detect the attached files, the most prominent being Thunderbird and Outlook.

Code used with ActionMailer 3.0.10:

class MultipartTest < ActionMailer::Base
  def test_email(recipient)
    attachments['file1.pdf'] = File.read('/somewhere/file1.pdf')
    attachments['file2.pdf'] = File.read('/somewhere/file2.pdf')
    attachments.inline['image1.gif'] = File.read('/somewhere/image1.gif')
    mail(
      :to => recipient,
      :subject => 'Multipart test'
    )
  end
end

The generated email comes as follows:

multipart/related
  multipart/alternative
    text/plain
    text/html
  attachment (disposition: inline, image1)
  attachment (disposition: attachment, file1)
  attachment (disposition: attachment, file2)

While ActionMailer::Base::set_content_type() chooses multipart/related as soon as it detects at least one inline attachment, mail clients always wrap the attached files (disposition: attachment) in an additional multipart/mixed layer, conforming to RFC 2046, Section 5.1.3.

When leaving out the inline attachement, the MIME type generated by ActionMailer is correct (multipart/mixed) and the attachments are visible.

Here are some MIME layouts, as generated by mail clients:

multipart/mixed
  multipart/alternative
    text/plain
    multipart/related
      text/html
      attachment (disposition: inline, image1)
  attachment (disposition: attachment, file1)
  attachment (disposition: attachment, file2)

or:

multipart/mixed
  multipart/related
    multipart/alternative
      text/plain
      text/html
    attachment (disposition: inline, image1)
  attachment (disposition: attachment, file1)
  attachment (disposition: attachment, file2)
@noniq

I can confirm this problem. Our workaround for now is to avoid inline attachments at all if there are any normal attachments.

@epoch

I can confirm this as well.

@tuggdev

Confirm, Outlook can see attachments only after removing inline attachments.

@bradhodges

Yep, same here, on my iMac, the normal attachments show in the attachments pulldown, but the email itself has no indication that there are normal attachments.

Maybe a blessing? The inline attachment is just a little company logo in the emails 'signature' , something my users like, but I've always found annoying!

@isaacsanders

Is this still an issue? cc @spastorino @tenderlove

@icanhasserver

I repeated the tests using ActionMailer 3.2.3 and I see no difference in the generated MIME structure: there's no "multipart/mixed".
So: this issue hasn't been solved, as far as I can see.

@dmkl

I confirm this issue as well.

@steveklabnik
Collaborator

@jonleighton is your work on basecamp/mail_view#17 useful here, too? Any plans on fixing this in Rails as well as that app?

@jonleighton
Collaborator

@steveklabnik I want to fix this at some point; it might become a necessity through my work actually. No ETA though.

@steveklabnik
Collaborator

Fair enough! Thanks.

@lathiat

This hit me too, in particular this is blocking sending out e-mails with Passbook attachments (while also including inline files) because the iOS Mail client refuses to see the passkit file attached due to this issue. The Mac Mail.app client however shows it fine.

@jonleighton
Collaborator

I've come up with a workaround for this bug. Add this method to your mailer:

  # Workaround for https://github.com/rails/rails/issues/2686
  def fix_mixed_attachments
    mail = Mail.new
    mail.delivery_method delivery_methods[delivery_method.to_sym], public_send("#{delivery_method}_settings")

    related = Mail::Part.new
    related.content_type = @_message.content_type
    @_message.parts.select { |p| !p.attachment? || p.inline? }.each { |p| related.add_part(p) }
    mail.add_part related

    mail.header       = @_message.header.to_s
    mail.content_type = nil
    @_message.parts.select { |p| p.attachment? && !p.inline? }.each { |p| mail.add_part(p) }

    @_message = mail
  end

Then call it at the end of your action method:

  def notification(...)
    attachments['omg.pdf'] = ...
    attachments.inline['wtf.png'] = ...

    mail(...)
    fix_mixed_attachments
  end

YMMV etc, but it works for me.

@aaronjensen

We're seeing that when attaching a text/csv when we have an html and text view a multipart/alternative wrapper (which includes another multipart/alternative and the attachment itself). We're not including any inline attachments. The outer multipart/alternative wrapper isn't understood by many mail clients. Is this a different issue or related?

@parndt

So based on the original bug and the examples of real mail clients and the workaround posted by @jonleighton it seems like the solution would be to just move attachment (disposition: inline, image1) (for example) underneath a multipart/related heading?

@hlascelles

We see this issue with Outlook 365 only. The same emails to gmail appear to be displayed fine.

Either way, the workaround from @jonleighton seems to work for us. Thanks!

@yyyc514

Ping - any progress on getting a fix for this into Rails proper?

@yyyc514

Improvement to the workaround earlier:

  • Aborts if there are no regular attachments, leaving the original message as it was
  • Calls a higher-level AM method (wrap_delivery_behavior!) that preserves settings like :perform_deliveries and raise_delivery_errors
  def fix_mixed_attachments
    # do nothing if we have no actual attachments
    return if @_message.parts.select { |p| p.attachment? && !p.inline? }.none?

    mail = Mail.new

    related = Mail::Part.new
    related.content_type = @_message.content_type
    @_message.parts.select { |p| !p.attachment? || p.inline? }.each { |p| related.add_part(p) }
    mail.add_part related

    mail.header       = @_message.header.to_s
    mail.content_type = nil
    @_message.parts.select { |p| p.attachment? && !p.inline? }.each { |p| mail.add_part(p) }

    @_message = mail
    wrap_delivery_behavior!(delivery_method.to_sym)
  end

Not using this in production yet but made the improvements during my testing in development.

@steveklabnik
Collaborator

If you find something that works well, you should submit a pull request!

@kitebuggy

yyyc514's workaround seems to have fixed it for me. Thank you.

P.S. Would be nice to see this merged into the main code though...

@magpieuk

yyyc514's workaround also worked for me. For some reason Johns workaround worked in most cases but the attachment vanished in Outlook 2010 but was visible in Outlook 2013. When the missing email was forwarded back to me Outlook 2013 could now see the attachment. weird!

@d-ark

I used to have the same problem in my app. I've solved it changing message structure to this:

multipart/related
  multipart/related
    multipart/alternative
      text/plain
      text/html
    image/jpeg   #inline
    image/jpeg   #inline
    ...          #other inline attachments  
  multipart/related
    application/pdf #non-inline
    text/plain      #non-inline
    ...             #other non-inline attachments

Maybe it seems strange, that all non-inline attachments are wrapped by extra 'related'. I've tested this structure on different mail clients - it works fine. If we don't wrap them, and we have plain/text attachment in our message, some mail clients (gmail as well) parse this attachment as mail text part (it's displayed in previews).

I'd like to fix this issue, but i can't find code where the mail structure is formed... Maybe this is issue of mail gem?

cc / @steveklabnik @jonleighton

@yyyc514

Building the structure is part of ActionMailer, you'll find the code there. Mail just provides the building blocks, nothing more.

@NikoRoberts

So should a PR be made to ActionMailer like:
http://github.com/rails/rails/commit/311d99eef01c268cedc6e9b3bdb9abc2ba5c6bfa
Or should a PR be made into Mail?

@rafaelfranca

I believe we should fix Mail gem if it is broken. Who can investigate this issue and work on it?

@yyyc514

This isn't Mail's problem (as the division of labor currently stands). Mail is the low-level email library and Rails is responsible for building the MIME envelopes for multi-part emails. Someone on the Rails team needs to care about this issue - which doesn't seem to be the case.

@rafaelfranca
Owner

The Rails team don't have time to fix all the issues as I said before, who want to investigate and provide a pull request?

@yyyc514

I'd be happy to, but I don't really know what the actual CORRECT solution is. You see my code above... I could merge it into AM proper... but is this the actual correct solution to the problem? I guess I was hoping someone would come out of the woodwork with an authoritative fix, vs "this works for me".

I'd worry without some official reference that the Core team wouldn't be as likely to merge the pull request since if they had run into this personally you'd think they'd have fixed it already. So those are my fears.

@yyyc514

So it's the "investigate" part I guess I'd be hung up on. :)

@rafaelfranca
Owner

I'll suggest to you open a pull request with your current fix. As you said we don't have this problem as would be way easier if we were discussing about a failing test to understand and some work in progress patch.

Could you do it?

@rafaelfranca rafaelfranca reopened this
@yyyc514

No idea how to write a failing test. Some email clients just don't like some aspect of how you currently build the MIME packets... not even sure if it's right or wrong we're talking about. Could be those clients (even if popular) are the problem... so this isn't really something that has a failing test. It's just that doing it an alternative way seems to work better across multiple clients.

This thread already should contain enough information to discuss the issue. All you could do is write a test that looked for the MIME structure recommended here and then failed... but that's no good unless people understand the issue and agree that the MIME structure above is the right way to go.

@yyyc514

Can we agree which one of these is correct?

multipart/mixed
  multipart/alternative
    text/plain
    multipart/related
      text/html
      attachment (disposition: inline, image1)
  attachment (disposition: attachment, file1)
  attachment (disposition: attachment, file2)
multipart/mixed
  multipart/related
    multipart/alternative
      text/plain
      text/html
    attachment (disposition: inline, image1)
  attachment (disposition: attachment, file1)
  attachment (disposition: attachment, file2)
@matthewd
Collaborator

Isn't that going to depend on the author's intent? IMO, the former is generally more "correct": the (inline) images "belong" to the HTML part, and should be ignored by anything that chooses to use the Plain part.

But the most realistic definition of correct will be in which elicits a more suitable behaviour from more clients, if there's any difference.

@ajb ajb referenced this issue in afeld/tricle
Closed

include sparkline for each metric #29

@swelther

thx yyyc514 for the fix, it works for me.

But it omits the BCC from the header. I inserted this

mail.bcc = @_message.header[:bcc].value

as a workaround. Maybe there is a better way.

Maybe someone else stumbles over this too.

@yyyc514

@swelther Are you sure you're using my code? There is a line that copies over all the headers whole sale... if it didn't work a lot more than the BCC would be broken. Have you tried to track down the issue any further? Honestly not sure why BCC would be some sort of edge case - though I haven't dug in further either.

mail.header       = @_message.header.to_s
@swelther

@yyyc514 Yes, it's your code. I was wondering too, maybe it is a documented behavior in newer Rails versions?

def fix_mixed_attachments
  # do nothing if we have no actual attachments
  return if @_message.parts.select { |p| p.attachment? && !p.inline? }.none?

  mail = Mail.new

  related = Mail::Part.new
  related.content_type = @_message.content_type
  @_message.parts.select { |p| !p.attachment? || p.inline? }.each { |p| related.add_part(p) }
  mail.add_part related

  mail.header = @_message.header.to_s
  mail.content_type = nil
  @_message.parts.select { |p| p.attachment? && !p.inline? }.each { |p| mail.add_part(p) }

  @_message = mail
  wrap_delivery_behavior!(delivery_method.to_sym)
end

this is what I get from @_message.header.to_s in my spec:

> @_message.header.to_s
=> "From: service@mail.com\r\nTo: fred@feuerstein.com\r\nSubject: blah\r\nMime-Version: 1.0\r\nContent-Type: multipart/related;\r\n boundary=\"--==_mimepart_5465f952f0ba1_7dde50133838765\";\r\n charset=UTF-8\r\n"

no bcc. But it is set in the header:

> @_message.header[:bcc].value
=> ["swelther@mail.com"]

Maybe Rails 4.0.10 (or active mailer or whatever) acts in this case a bit different than previous versions? IIRC I tried this on a server and got the same behavior as in my spec.

@yyyc514

Oh, that seems intentional then. I'll look at it later. Perhaps you can paste another full code excerpt here and add the BCC fix... I'm guessing it's something intentional relating to the strange nature of BCC.

@swelther

sure, no big deal, it works for me :grinning:

so if anyone stumbles over not-copied BCC values use this version:

  def fix_mixed_attachments
    # do nothing if we have no actual attachments
    return if @_message.parts.select { |p| p.attachment? && !p.inline? }.none?

    mail = Mail.new

    related = Mail::Part.new
    related.content_type = @_message.content_type
    @_message.parts.select { |p| !p.attachment? || p.inline? }.each { |p| related.add_part(p) }
    mail.add_part related

    mail.header       = @_message.header.to_s
    mail.bcc          = @_message.header[:bcc].value # copy bcc manually because it is omitted in header.to_s
    mail.content_type = nil
    @_message.parts.select { |p| p.attachment? && !p.inline? }.each { |p| mail.add_part(p) }

    @_message = mail
    wrap_delivery_behavior!(delivery_method.to_sym)
  end
@yyyc514

From Mail's bcc_field.rb line 24:

#  mail[:bcc].encoded   #=> ''      # Bcc field does not get output into an email

So BCC is a magic field that is never output (why to_s fails) but rather is (I'd imagine) used internally by Mail (or other gems) during the delivery process. So that's why it's necessary to copy it in such fashion. There may be a simpler way to just copy the whole header object over as-is (or rather clone it), but I didn't spend any time looking into that. I just wanted to answer the question of "why".

@swelther

thx for clarifying @yyyc514. Sounds reasonable. Maybe someone gets bored and develops a simpler version :grinning:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.