Home > Net >  Multipart email with ical attachment displays incorrectly
Multipart email with ical attachment displays incorrectly

Time:10-02

I'm not sure if this is an issue with my code, ActionMailer, Mail, or maybe even the icalendar gem?

A user registers for an event and they get an email with an ical attachment:

# app/mailers/registration_mailer.rb

class RegistrationMailer < ApplicationMailer
  helper MailerHelper

  def created(registration)
    ...

    cal = Icalendar::Calendar.new
    cal.event do |e|
      e.dtstart = @event.start_time
      e.dtend = @event.end_time
      e.organizer = 'mailto:[email protected]'
      e.attendee = @recipient
      e.location = @location.addr_one_liner
      e.summary = @summary
      e.description = @description
    end
    cal.append_custom_property('METHOD', 'REQUEST')
    mail.attachments[@attachment_title] = { mime_type: 'text/calendar', content: cal.to_ical }

    mail(to: @recipient.email, subject: "[20 Liters] You registered for a filter build on #{@event.mailer_time}")
  end

  ...
end

I have text and HTML views:

  • app/views/registration_mailer/created.text.erb
  • app/views/registration_mailer/created.html.erb

When I omit the attachment, the email is structured like this:

Header stuff...
Mime-Version: 1.0
Content-Type: multipart/alternative; boundary="--==_mimepart_63358693571_1146901122e"; charset=UTF-8
Content-Transfer-Encoding: 7bit

----==_mimepart_63358693571_1146901122e
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit

[the text version of the email here]

----==_mimepart_63358693571_1146901122e
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit

[the HTML version of the email here]
----==_mimepart_63358693571_1146901122e--

When the attachment is present, the email is structured like this:

Header stuff...
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="--==_mimepart_6335c3388b140_114924286ed"; charset=UTF-8
Content-Transfer-Encoding: 7bit

----==_mimepart_6335c3388b140_114924286ed
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit

[the text version of the email here]

----==_mimepart_6335c3388b140_114924286ed
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit

[the HTML version of the email here]

----==_mimepart_6335c3388b140_114924286ed
Content-Type: multipart/alternative; boundary="--==_mimepart_6335c3389bc30_114924287a3"; charset=UTF-8
Content-Transfer-Encoding: 7bit

----==_mimepart_6335c3389bc30_114924287a3
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit

[the text version of the email AGAIN]

----==_mimepart_6335c3389bc30_114924287a3
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit

[the HTML version of the email AGAIN]

----==_mimepart_6335c3389bc30_114924287a3--
----==_mimepart_6335c3388b140_114924286ed
Content-Type: text/calendar; charset=UTF-8
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=20Liters_filterbuild_20221011T0900.ical
Content-ID: <[email protected]>

[numbers and letters]
----==_mimepart_6335c3388b140_114924286ed--

It's a weird tree suddenly:

1. Content-Type: multipart/mixed
    A. Content-Type: text/plain
    B. Content-Type: text/html
    C. Content-Type: multipart/alternative
        i. Content-Type: text/plain
        ii. Content-Type: text/html
    D. Content-Type: text/calendar

Rails' mailer preview doesn't reproduce this issue, nor does using Litmus' email client previews (because it seems to remove the text part and attachments), but I'm assuming with the deformed structure of content-types this isn't just a client-specific rendering issue.

I'm thinking this is coming from the Mail gem underneath ActionMailer structuring the content-types oddly, but I'm a bit out of my depth here. It could be ActionMailer, I really don't know how to tell.

I'm not very well versed in this, but I think I want this structure:

1. Content-Type: multipart/mixed
    A. Content-Type: multipart/alternative
        i. Content-Type: text/plain
        ii. Content-Type: text/html
    B. Content-Type: text/calendar

So, two questions:

1. If it's my code, what am I doing wrong?

2. If it's not my code, can I force the structure I want?

I've been combing through ActionMailer and Mail code bases, but haven't found a way to manually form my email to this level.

CodePudding user response:

After more digging, I'm blaming ActionMailer, though I'm still not sure why text and html parts are getting added twice.

A monkey patch for my specific use was to let ActionMailer and Mail build the mail object and then just manually remove the unwanted parts:

# app/mailers/registration_mailer.rb

...

mail.attachments[@attachment_title] = { mime_type: 'text/calendar', content: cal.to_ical }

mail(to: @recipient.email, subject: "[20 Liters] You registered for a filter build on #{@event.mailer_time}")

# PATCH: text and html parts are getting inserted in multipart/mixed (top level) as well as multipart/alternative (2nd level)
mail.parts.reject! { |part| !part.attachment? && !part.multipart? }

This only works for my specific case. If the nesting that mail creates for you is different, your reject! statement will need to be different.

In my case, mail builds this structure:

1. Content-Type: multipart/mixed
    A. Content-Type: text/plain
    B. Content-Type: text/html
    C. Content-Type: multipart/alternative
        i. Content-Type: text/plain
        ii. Content-Type: text/html
    D. Content-Type: text/calendar

So I step into the first level (A. - D.) and reject any parts that are not multipart and not an attachment:

1. Content-Type: multipart/mixed
    A. Content-Type: text/plain <-- not multipart, not attachment = rejected
    B. Content-Type: text/html <-- not multipart, not attachment = rejected
    C. Content-Type: multipart/alternative  <-- is multipart, not attachment = kept
        i. Content-Type: text/plain
        ii. Content-Type: text/html
    D. Content-Type: text/calendar  <-- not multipart, is attachment = kept

If you are facing this issue, I recommend you use a debugger to inspect your mail object, specifically focusing on the parts. Keep in mind that parts can be deeply nested.

Mail::Part inherits from Mail::Message which has some helpful methods you can use to determine the "shape" of your message:

  • multipart?
  • attachment?
  • attachments
  • has_attachments?
  • boundary
  • parts
  • all_parts

And good luck.

  • Related