Use Inline Email Attachments for Images

When Rails developers need to include an image in an Action Mailer message, we’ll often use image_tag in the same way that we’d use it in the rest of the application:

<%= image_tag('logo.png', alt: 'Example Company logo') %><%# Image from `app/assets/images/` %>
<%= image_tag(@user.avatar, alt: "#{@user.name} avatar") %><%# Image from ActiveStorage %>

These generate <img> tags whose src points to the image hosted on the Rails server (or its content delivery network) or its object storage, known usually as “external images” or “remote content”. These are adequate for most purposes, but we can improve privacy, resilience, and security by using inline email attachments instead.

What?

Inline attachments are sent as part of the email message itself (an attachment) and are also displayed in the body of the message (inline; as opposed to having to select the attachment to open it in its own window). You’ve likely used them before while composing messages yourself: in your email client (also known as a mail user agent or MUA; which may be your email provider’s web application), you can usually drag-and-drop images onto the message or use an “Insert Image” button to create an inline attachment.

Why?

There are 3 main advantages of inline attachments: privacy, resilience, and security.

Respect users’ privacy

In the privacy settings of almost every MUA, there is a toggle for automatically displaying external images; if disabled, it will instead offer the user the choice to do so when viewing individual messages. It’s categorized as a privacy setting because external images are fetched when a user views a message, which allows the host of that content to know exactly when the message was viewed, how many times it was viewed, the user’s approximate geolocation (via IP address), and information about the device on which it’s being viewed (via its user agent).

By using inline attachments, the user can trust that you’re not harvesting that information. They also don’t need to choose between viewing the full design of the message and protecting their privacy.

Resilience

With external images, if your site (or, in rarer circumstances, your content delivery network) is experiencing an outage, users will not be able to view images in the email messages that you’ve sent. Inline attachments are immune to this.

External images also must be hosted forever in order for them to be viewable in old messages, meaning that you need to be cautious when deleting files from app/assets/images/ or ActiveStorage attachments. Inline attachments do not have this issue.

Security & reputation

With external images, a certain type of attack is possible: if your domain expires (or someone otherwise takes over your domain registration), someone else can easily register that domain and replace the images in emails you’ve already sent with something nefarious. All of a sudden, your images in messages from years ago that once showed your logo now show instructions for a phishing scam. Additionally, they can harvest your users’ information as described above.

Why not?

There are some reasons why you might want to use external images instead of inline attachments: message size, open tracking, and control.

Message size

Most email providers will reject an incoming message if it exceeds a certain size, often in the range of 20–50 MB. Inline attachments increase the size of the message, so it’s more necessary to ensure that the images’ file sizes aren’t very large.

However, even with external images, it’s polite to your users to be conscious of the file sizes anyway, so that they’re more usable on a slow internet connection or a limited data plan.

Open tracking

“Open tracking” is the harvesting of users’ information as described above. Many email marketing campaigns (for example) will utilize open tracking to measure their effectiveness. This can be accomplished by tracking or proxying requests for external images.

However, visible external images are not necessary for this; open-tracking services will almost always insert a “tracking pixel” (an invisible image within the email body, usually a 1×1 px transparent image) into the email body. By using inline attachments for visible content and an external image for a tracking pixel, users who have disabled external images in their MUAs can more easily protect their privacy. Of course, that requires those users to be informed of that setting and its rationale; the majority of users likely aren’t aware of it, and they may have an implicit expectation of privacy.

Control

Perhaps you want the ability to make certain images inaccessible in the future or to replace them with different images; for example, you might want to display a countdown widget made possible by dynamically generating an animated image showing the days & hours remaining until an event.

How?

The official Action Mailer guide contains instructions for creating inline attachments.

For images in app/assets/images/, read the file from disk in the mailer method:

class BillingMailer < ApplicationMailer
  def invoice
    # ⋮
    attachments.inline['logo.png'] = Rails.root.join('app', 'assets', 'images', 'logo.png').read
    # ⋮
  end
end

For images hosted via Active Storage, read the file from storage in the mailer method:

class BillingMailer < ApplicationMailer
  def invoice
    # ⋮
    attachments.inline['avatar.png'] = @user.avatar.open(&:read)
    # ⋮
  end
end

Now that the image has been attached, display it in the view:

<%= image_tag(attachments['logo.png'].url), alt: 'Example Company logo' %>
<%= image_tag(attachments['avatar.png'].url), alt: "#{@user.name} avatar" %>