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.