defmodule Swoosh.Adapters.SMTP.Helpers do @moduledoc false alias Swoosh.Email import Swoosh.Email.Render @doc false def sender(%Email{} = email) do email.headers["Sender"] || elem(email.from, 1) end @doc false def body(email, config) do {message_config, config} = Keyword.split(config, [:transfer_encoding, :keep_bcc]) {type, subtype, headers, parts} = prepare_message(email, message_config) {encoding_config, _config} = Keyword.split(config, [:dkim]) mime_encode(type, subtype, headers, parts, encoding_config) end # TODO: Remove conditional handling when going 2.0 gen_smtp_major = if Code.ensure_loaded?(:gen_smtp_client) do Application.load(:gen_smtp) :gen_smtp |> Application.spec(:vsn) |> to_string() |> Version.parse!() |> Map.get(:major) else 0 end @parameters if(gen_smtp_major >= 1, do: %{}, else: []) defp mime_encode(type, subtype, headers, parts, encoding_config) do :mimemail.encode({type, subtype, headers, @parameters, parts}, encoding_config) end @doc false def prepare_message(email, config) do email |> prepare_headers(config) |> prepare_parts(email, config) end defp prepare_headers(email, config) do [] |> prepare_additional_headers(email) |> prepare_mime_version() |> prepare_reply_to(email) |> prepare_subject(email) |> prepare_cc(email) |> then(fn headers -> # bcc is deliberately omitted unless explicitly asked to keep, for service providers # https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 if config[:keep_bcc] do prepare_bcc(headers, email) else headers end end) |> prepare_to(email) |> prepare_from(email) end defp prepare_subject(headers, %{subject: subject}) when is_binary(subject), do: [{"Subject", subject} | headers] defp prepare_from(headers, %{from: from}), do: [{"From", render_recipient(from)} | headers] defp prepare_to(headers, %{to: []}), do: headers defp prepare_to(headers, %{to: to}), do: [{"To", render_recipient(to)} | headers] defp prepare_cc(headers, %{cc: []}), do: headers defp prepare_cc(headers, %{cc: cc}), do: [{"Cc", render_recipient(cc)} | headers] defp prepare_bcc(headers, %{bcc: []}), do: headers defp prepare_bcc(headers, %{bcc: bcc}), do: [{"Bcc", render_recipient(bcc)} | headers] defp prepare_reply_to(headers, %{reply_to: nil}), do: headers defp prepare_reply_to(headers, %{reply_to: reply_to}), do: [{"Reply-To", render_recipient(reply_to)} | headers] defp prepare_mime_version(headers), do: [{"MIME-Version", "1.0"} | headers] defp prepare_additional_headers(headers, %{headers: additional_headers}) do Map.to_list(additional_headers) ++ headers end defp prepare_parts( headers, %{ attachments: [], html_body: html_body, text_body: text_body }, config ) do case {text_body, html_body} do {text_body, nil} -> {"text", "plain", add_content_type_header(headers, "text/plain; charset=\"utf-8\""), text_body} {nil, html_body} -> {"text", "html", add_content_type_header(headers, "text/html; charset=\"utf-8\""), html_body} {text_body, html_body} -> parts = [ prepare_part(:plain, text_body, config), prepare_part(:html, html_body, config) ] {"multipart", "alternative", headers, parts} end end defp prepare_parts( headers, %{ attachments: attachments, html_body: html_body, text_body: text_body }, config ) do {inline_attachments, attachments} = Enum.split_with(attachments, &(&1.type == :inline)) content_part = case {prepare_part(:plain, text_body, config), prepare_part(:html, html_body, config)} do {text_part, nil} -> text_part {nil, html_part} -> html_with_line_attachments(html_part, inline_attachments) {text_part, html_part} -> html_part = html_with_line_attachments(html_part, inline_attachments) {"multipart", "alternative", [], %{}, [text_part, html_part]} end attachment_parts = Enum.map(attachments, &prepare_attachment(&1)) {"multipart", "mixed", headers, [content_part | attachment_parts]} end defp prepare_part(_subtype, nil, _config), do: nil @content_params if(gen_smtp_major >= 1, do: %{ content_type_params: [{"charset", "utf-8"}], disposition: "inline", disposition_params: [] }, else: [ {"content-type-params", [{"charset", "utf-8"}]}, {"disposition", "inline"}, {"disposition-params", []} ] ) defp prepare_part(subtype, content, config) do subtype_string = to_string(subtype) transfer_encoding = Keyword.get(config, :transfer_encoding, "quoted-printable") {"text", subtype_string, [ {"Content-Type", "text/#{subtype_string}; charset=\"utf-8\""}, {"Content-Transfer-Encoding", transfer_encoding} ], @content_params, content} end defp html_with_line_attachments(html_part, []), do: html_part defp html_with_line_attachments(html_part, inline_attachments) do attachment_parts = Enum.map(inline_attachments, &prepare_attachment(&1)) {"multipart", "related", [], %{}, [html_part | attachment_parts]} end defp add_content_type_header(headers, value) do if Enum.find(headers, fn {header_name, _} -> String.downcase(header_name) == "content-type" end) do headers else [{"Content-Type", value} | headers] end end defp prepare_attachment( %Swoosh.Attachment{ cid: cid, filename: filename, content_type: content_type, type: attachment_type, headers: custom_headers } = attachment ) do [type, format] = String.split(content_type, "/") content = Swoosh.Attachment.get_content(attachment) transfer_encoding = transfer_encoding_for_content_type(content_type) case attachment_type do :attachment -> { type, format, [ {"Content-Transfer-Encoding", transfer_encoding} | custom_headers ], attachment_content_params(:attachment, filename), content } :inline -> { type, format, [ {"Content-Transfer-Encoding", transfer_encoding}, {"Content-Id", "<#{cid || filename}>"} | custom_headers ], attachment_content_params(:inline, filename), content } end end # RFC 2046 Section 5.2: message/* types must not use base64 encoding # - message/rfc822 allows "7bit", "8bit", or "binary" (Section 5.2.1) # - message/partial must be "7bit" (Section 5.2.2) # - message/external-body must be "7bit" (Section 5.2.3) defp transfer_encoding_for_content_type("message/rfc822"), do: "8bit" defp transfer_encoding_for_content_type("message/" <> _), do: "7bit" defp transfer_encoding_for_content_type(_), do: "base64" if gen_smtp_major >= 1 do defp attachment_content_params(:attachment, filename) do %{ disposition: "attachment", disposition_params: [{"filename", filename}] } end else defp attachment_content_params(:attachment, filename) do [ {"disposition", "attachment"}, {"disposition-params", [{"filename", filename}]} ] end end if gen_smtp_major >= 1 do defp attachment_content_params(:inline, filename) do %{ content_type_params: [], disposition: "inline", disposition_params: [{"filename", filename}] } end else defp attachment_content_params(:inline, filename) do [ {"content-type-params", []}, {"disposition", "inline"}, {"disposition-params", [{"filename", filename}]} ] end end end