lib/swoosh/attachment.ex

defmodule Swoosh.Attachment do
  @moduledoc ~S"""
  Struct representing an attachment in an email.

  ## Usage

  For all usecases of `new/2` see the function documentation.

  ## Inline Example

      new()
      |> to({avenger.name, avenger.email})
      |> from({"Red Skull", "red_skull@villains.org"})
      |> subject("End Game invitation QR Code")
      |> html_body(~s|<h1>Hello #{avenger.name}</h1> Here is your QR Code <img src="cid:qrcode.png">|)
      |> text_body("Hello #{avenger.name}. Please find your QR Code attached.\n")
      |> attachment(
        Swoosh.Attachment.new(
          {:data, invitation_qr_code_binary},
          filename: "qrcode.png",
          content_type: "image/png",
          type: :inline)
      )
      |> VillainMailer.deliver()
  """

  defstruct filename: nil,
            content_type: nil,
            path: nil,
            data: nil,
            type: :attachment,
            cid: nil,
            headers: []

  @type t :: %__MODULE__{
          filename: String.t(),
          content_type: String.t(),
          path: String.t() | nil,
          data: binary | nil,
          type: :inline | :attachment,
          cid: String.t() | nil,
          headers: [{String.t(), String.t()}]
        }

  @doc ~S"""
  Creates a new Attachment

  Examples:

      Attachment.new("/path/to/attachment.png")
      Attachment.new("/path/to/attachment.png", filename: "image.png")
      Attachment.new("/path/to/attachment.png", filename: "image.png", content_type: "image/png")
      Attachment.new(params["file"]) # Where params["file"] is a %Plug.Upload
      Attachment.new({:data, File.read!("/path/to/attachment.png")}, filename: "image.png", content_type: "image/png")

  Examples with inline-attachments:

      Attachment.new("/path/to/attachment.png", type: :inline)
      Attachment.new("/path/to/attachment.png", filename: "image.png", type: :inline)
      Attachment.new("/path/to/attachment.png", filename: "image.png", content_type: "image/png", type: :inline)
      Attachment.new(params["file"], type: :inline) # Where params["file"] is a %Plug.Upload
      Attachment.new({:data, File.read!("/path/to/attachment.png")}, filename: "image.png", content_type: "image/png", type: :inline)

  Inline attachments by default use their filename
  (or basename of the path if filename is not specified) as cid,
  in relevant adapters.

      Attachment.new("/data/file.png", type: :inline)

  Gives you something like this:

      <img src="cid:file.png">

  You can optionally override this default by passing in the cid option:

      Attachment.new("/data/file.png", type: :inline, cid: "custom-cid")
  """
  @spec new(binary | struct | {:data, binary}, Keyword.t() | map) :: %__MODULE__{}
  def new(path, opts \\ [])

  if Code.ensure_loaded?(Plug) do
    def new(
          %Plug.Upload{
            filename: filename,
            content_type: content_type,
            path: path
          },
          opts
        ) do
      new(
        path,
        Keyword.merge(
          [filename: filename, content_type: content_type],
          opts
        )
      )
    end
  end

  def new({:data, data}, opts) do
    %{struct!(__MODULE__, opts) | data: data}
  end

  def new(path, opts) do
    attachment = struct!(__MODULE__, opts)
    filename = attachment.filename || Path.basename(path)
    cid = if attachment.type == :inline, do: attachment.cid || filename, else: nil

    %{
      attachment
      | path: path,
        filename: filename,
        content_type: attachment.content_type || MIME.from_path(path),
        cid: cid
    }
  end

  @type content_encoding :: :raw | :base64
  @spec get_content(%__MODULE__{}, content_encoding) :: binary | no_return
  def get_content(%__MODULE__{data: nil, path: nil}) do
    raise Swoosh.AttachmentContentError, message: "No path or data is provided"
  end

  def get_content(%__MODULE__{data: data, path: path}, encoding \\ :raw) do
    encode(data || File.read!(path), encoding)
  end

  defp encode(content, :raw), do: content
  defp encode(content, :base64), do: Base.encode64(content)
end