Monday, March 20, 2017

The Trouble With Mail Attachments in Rails

I ran into a problem with a Rails ActionMailer at work recently, and I thought I'd share my findings. Attachments require a multipart mail message -- but adding an attachment does not cause the message to be multipart.

So I had an HTML-formatted email that was working just fine. The Mailer basically looked like this in the action, with only an .html template was in the views directory.

 class DdosReportMailer < ActionMailer::Base  
  layout 'layouts/mailer'  
  def daily_report(rcpts:)  
   mail(  
    to: EnvVar.get('NOREPLY_EMAIL'),  
    from: EnvVar.get('NOREPLY_EMAIL'),  
    bcc: rcpts,  
    subject: I18n.t("ddos_report_mailer.daily_report.heading")  
   )
  end  
 end  

My story required that I add attachments, but the Rails guide is not exactly clear about when and where to call the 'attachments' method. First, I tried calling it on the mail instance after it was created:

 class DdosReportMailer < ActionMailer::Base  
  layout 'layouts/mailer'  
  def daily_report(rcpts:, reports:)  
   mail(  
    to: EnvVar.get('NOREPLY_EMAIL'),  
    from: EnvVar.get('NOREPLY_EMAIL'),  
    bcc: rcpts,  
    subject: I18n.t("ddos_report_mailer.daily_report.heading")  
   ).tap do |m|  
    reports.each { |k,v| m.attachments[k] = v }  
   end  
  end  
 end  

In the Rails previewer, this looked good. HTML formatting, check; attachment, check. Tests which verified the mail body and attachments passed, so I figured the work was done.  

But when I sent the actual mail, it looked like this:


I immediately lay this problem at the feet of the MailSafe gem, which we use to prevent outbound mail in staging. The MailSafe modification seems so close to the attachment boundary, it must be causing a parsing error! Not to worry -- in production, we won't have this problem. Just to be sure, I whitelisted the original recipient address so there was no MailSafe intervention. Surprisingly, that did not help. 

I went back to the Rails guide and decided the 'attachments' call should be sent to the mailer, not the mail.

 class DdosReportMailer < ActionMailer::Base  
  layout 'layouts/mailer'  
  def daily_report(rcpts:, reports:)  
   reports.each { |k,v| attachments[k] = v }  
   mail(  
    to: EnvVar.get('NOREPLY_EMAIL'),  
    from: EnvVar.get('NOREPLY_EMAIL'),  
    bcc: rcpts,  
    subject: I18n.t("ddos_report_mailer.daily_report.heading")  
   )
  end  
 end  

Now the delivered message looked even worse! Something is wrong with the message parts.


After a bit of Googling I found a discussion about a similar multi-part problem that was solved by passing a block to the 'mail' method and using the format object which is yielded. This did the trick. Although attachments require a multipart message, adding an attachment does not cause the message to be multipart. Using a format block will. Here is the final code.

 class DdosReportMailer < ActionMailer::Base
  layout 'layouts/mailer'
  def daily_report(rcpts:, reports:)
    reports.each { |k, v| attachments[k] = v }
    mail(
      to: EnvVar.get('NOREPLY_EMAIL'),
      from: EnvVar.get('NOREPLY_EMAIL'),
      bcc: rcpts,
      subject: I18n.t("ddos_report_mailer.daily_report.heading")
    ) do |format|
      format.html { render 'daily_report' }
    end
   end
  end

No comments: