diff --git a/config/config.exs b/config/config.exs index 408948a..ed1c192 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,3 +1,7 @@ import Config +if Mix.env() == :test do + config :logger, level: :info +end + # import_config "#{Mix.env}.exs" diff --git a/lib/bamboo/adapters/render/raw.ex b/lib/bamboo/adapters/render/raw.ex index 1f3d840..b72c9ac 100644 --- a/lib/bamboo/adapters/render/raw.ex +++ b/lib/bamboo/adapters/render/raw.ex @@ -3,83 +3,195 @@ defmodule BambooSes.Render.Raw do Functions for rendering email messages into strings. """ + # TODO render according to logic table: text, html, has_attachments, has_content_id_attachments @doc """ Returns a tuple with all data needed for the underlying adapter to send. """ def render(email, extra_headers \\ []) do - email - # Returns a list of tuples - |> compile_parts() - # Nests the tuples and attaches necessary metadata - |> nest_parts(email, extra_headers) + has_text = String.length(email.text_body) > 0 + has_html = String.length(email.html_body) > 0 + has_attachments = length(filter_regular_attachments(email)) > 0 + has_inline_attachments = length(filter_inline_attachments(email)) > 0 + + headers = headers_for(email) ++ extra_headers + + build_parts( + has_text, + has_html, + has_attachments, + has_inline_attachments, + email, + headers + ) + # |> IO.inspect() |> :mimemail.encode() + + # has_attachments = + # email + # # Returns a list of tuples + # |> compile_parts() + # # Nests the tuples and attaches necessary metadata + # |> nest_parts(email, extra_headers) + # |> :mimemail.encode() + end + + defp build_parts(false, false, _, _, email, headers) do + { + "multipart", + "mixed", + headers, + %{}, + prepare_attachments(email.attachments) + } end - defp nest_parts(parts, email, extra_headers) do - {top_mime_type, top_mime_sub_type, _, _, top_content_part} = nested_content_part_tuples(parts) + defp build_parts(false, true, false, false, email, headers) do + { + "text", + "html", + headers, + parameters_for(nil), + email.html_body + } + end + defp build_parts(false, true, false, true, email, headers) do { - top_mime_type, - top_mime_sub_type, - headers_for(email) ++ extra_headers, + "multipart", + "related", + headers, %{}, - top_content_part + [ + # generates html + build_parts(false, true, false, false, email, []) + ] ++ prepare_attachments(filter_inline_attachments(email)) } end - defp nested_content_part_tuples(parts) do - plain_part_tuple = body_part_tuple(parts, :plain) - html_part_tuple = body_part_tuple(parts, :html) - # attachment_part_tuples(parts) - inline_attachment_part_tuples = [] - attached_attachment_part_tuples = attachment_part_tuples(parts) - - related_or_html_part_tuple = - if Enum.empty?(inline_attachment_part_tuples) do - html_part_tuple - else - if is_nil(html_part_tuple), - do: nil, - else: - {"multipart", "related", [], %{}, [html_part_tuple | inline_attachment_part_tuples]} - end - - alternative_or_plain_tuple = - if is_nil(related_or_html_part_tuple) do - plain_part_tuple - else - {"multipart", "alternative", [], %{}, [plain_part_tuple, related_or_html_part_tuple]} - end - - mixed_or_alternative_tuple = - if Enum.empty?(attached_attachment_part_tuples) do - alternative_or_plain_tuple - else - if is_nil(alternative_or_plain_tuple), - do: nil, - else: - {"multipart", "mixed", [], %{}, - [alternative_or_plain_tuple | attached_attachment_part_tuples]} - end - - mixed_or_alternative_tuple - end - - @spec body_part_tuple([tuple()], atom()) :: nil | tuple() - defp body_part_tuple(parts, type) do - part = Enum.find(parts, &(elem(&1, 0) == type)) - - if is_nil(part) do - nil - else - { - mime_type_for(part), - mime_subtype_for(part), - headers_for(part), - parameters_for(part), - elem(part, 1) - } - end + defp build_parts(false, true, true, false, email, headers) do + { + "multipart", + "mixed", + headers, + %{}, + [ + # generates html + build_parts(false, true, false, false, email, []) + ] ++ prepare_attachments(email.attachments) + } + end + + defp build_parts(false, true, true, true, email, headers) do + { + "multipart", + "mixed", + headers, + %{}, + [ + # generates html + build_parts(false, true, false, true, email, []) + ] ++ prepare_attachments(filter_regular_attachments(email)) + } + end + + defp build_parts(true, false, false, false, email, headers) do + { + "text", + "plain", + headers, + parameters_for(nil), + email.text_body + } + end + + defp build_parts(true, false, _, _, email, headers) do + { + "multipart", + "mixed", + headers, + %{}, + [ + # generates text + build_parts(true, false, false, false, email, []) + ] ++ prepare_attachments(email.attachments) + } + end + + defp build_parts(true, true, false, false, email, headers) do + { + "multipart", + "alternative", + headers, + %{}, + [ + # generates text + build_parts(true, false, false, false, email, []), + # generates html + build_parts(false, true, false, false, email, []) + ] + } + end + + defp build_parts(true, true, false, true, email, headers) do + { + "multipart", + "related", + headers, + %{}, + [ + # generates alternative + build_parts(true, true, false, false, email, []) + ] ++ prepare_attachments(filter_inline_attachments(email)) + } + end + + defp build_parts(true, true, true, false, email, headers) do + { + "multipart", + "mixed", + headers, + %{}, + [ + # generates alternative + build_parts(true, true, false, false, email, []) + ] ++ prepare_attachments(email.attachments) + } + end + + defp build_parts(true, true, true, true, email, headers) do + { + "multipart", + "mixed", + headers, + %{}, + [ + # generates related with alternative + build_parts(true, true, false, true, email, []) + ] ++ prepare_attachments(filter_regular_attachments(email)) + } + end + + defp prepare_attachments(attachments) do + attachments + |> Enum.map(fn attachment -> {:attachment, attachment.data, attachment} end) + |> attachment_part_tuples() + end + + def filter_inline_attachments(email) do + Enum.filter(email.attachments, fn + attachment -> + !is_nil(attachment) && + !is_nil(attachment.content_id) && + String.length(attachment.content_id) > 0 + end) + end + + def filter_regular_attachments(email) do + Enum.filter(email.attachments, fn + attachment -> + !is_nil(attachment) && + (is_nil(attachment.content_id) || String.length(attachment.content_id) == 0) + end) end @spec attachment_part_tuples([tuple()]) :: list(tuple()) @@ -171,21 +283,4 @@ defmodule BambooSes.Render.Raw do end defp mailbox({name, address}), do: "#{name} <#{address}>" - - defp compile_parts(email) do - [ - {:plain, email.text_body}, - {:html, email.html_body}, - Enum.map(email.attachments, fn attachment -> - {:attachment, attachment.data, attachment} - end) - ] - |> List.flatten() - |> Enum.filter(¬_empty_tuple_value(&1)) - end - - defp not_empty_tuple_value(tuple) when is_tuple(tuple) do - value = elem(tuple, 1) - value != nil && value != [] && value != "" - end end diff --git a/test/lib/bamboo/adapters/content_raw_parts_text.exs b/test/lib/bamboo/adapters/content_raw_parts_text.exs new file mode 100644 index 0000000..a440ef4 --- /dev/null +++ b/test/lib/bamboo/adapters/content_raw_parts_text.exs @@ -0,0 +1,354 @@ +defmodule BambooSes.ContentRawPartsTest do + use ExUnit.Case + alias BambooSes.Message.Content + alias BambooSes.{EmailParser, TestHelpers} + alias Bamboo.Email + + @moduledoc """ + + TEXT | HTML | ATTACHMENTS | INLINE ATTACHMENTS | RESULT + f | f | f | f | NOT VALID + f | f | f | t | mixed(attachments[]) + f | f | t | f | mixed(attachments[]) + f | f | t | t | mixed(attachments[]) + f | t | f | f | text/html + f | t | f | t | related(html,inline_attachments[]) + f | t | t | f | mixed(html,attachments[]) + f | t | t | t | mixed(related(html,inline_attachments[]),attachments[]) + t | f | f | f | text/plain + t | f | f | t | mixed(text,attachments[]) + t | f | t | f | mixed(text,attachments[]) + t | f | t | t | mixed(text,attachments[]) + t | t | f | f | alternative(text,html) + t | t | f | t | related(alternative(text,html),inline_attachments[]) + t | t | t | f | mixed(alternative(text,html),attachments[]) + t | t | t | t | mixed(related(alternative(text,html),inline_attachments[]),attachments[]) + + """ + + @doc "f f f t" + test "generates multipart/mixed when only inline attachments are provided; no text, no html" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [png]} = EmailParser.parse(raw_data) + assert {"image", "png", _, _, _} = png + end + + @doc "f f t f" + test "generates multipart/mixed when only regular attachments are provided; no text, no html" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [pdf]} = EmailParser.parse(raw_data) + assert {"application", "pdf", _, _, _} = pdf + end + + @doc "f f t t" + test "generates multipart/mixed when inline and regular attachments are provided; no text, no html" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [pdf, png]} = EmailParser.parse(raw_data) + assert {"application", "pdf", _, _, _} = pdf + assert {"image", "png", _, _, _} = png + end + + @doc "f t f f" + test "generates text/html when only html is provided" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("Email body") + |> Email.put_header("X-Custom-Header", "custom-value") + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"text", "html", _, _, "Email body"} = EmailParser.parse(raw_data) + end + + @doc "f t f t" + test "generates multipart/related when html and attachments with content_id are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("Email body") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "related", _, _, [html, png]} = EmailParser.parse(raw_data) + assert {"text", "html", _, _, "Email body"} = html + assert {"image", "png", _, _, _} = png + end + + @doc "f t t f" + test "generates multipart/mixed when html and attachments are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("Email body") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [html, pdf]} = EmailParser.parse(raw_data) + assert {"text", "html", _, _, "Email body"} = html + assert {"application", "pdf", _, _, _} = pdf + end + + @doc "f t t t" + test "generates multipart/mixed with multipart/related" do + content = + TestHelpers.new_email() + |> Email.text_body("") + |> Email.html_body("Email body") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [related, pdf]} = EmailParser.parse(raw_data) + assert {"multipart", "related", _, _, [html, png]} = related + assert {"application", "pdf", _, _, _} = pdf + assert {"text", "html", _, _, "Email body"} = html + assert {"image", "png", _, _, _} = png + end + + @doc "t f f f" + test "generates text/plain when only text is provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("") + |> Email.put_header("X-Custom-Header", "custom-value") + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"text", "plain", _, _, "Email text body"} = EmailParser.parse(raw_data) + end + + @doc "t f f t" + test "generates multipart/mixed when text and inline attathments are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [text, png]} = EmailParser.parse(raw_data) + assert {"text", "plain", _, _, "Email text body"} = text + assert {"image", "png", _, _, _} = png + end + + @doc "t f t f" + test "generates multipart/mixed when text and attachments are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [text, pdf]} = EmailParser.parse(raw_data) + assert {"text", "plain", _, _, "Email text body"} = text + assert {"application", "pdf", _, _, _} = pdf + end + + @doc "t f t t" + test "generates multipart/mixed when text and both inline and regular attachments are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [text, pdf, png]} = EmailParser.parse(raw_data) + assert {"text", "plain", _, _, "Email text body"} = text + assert {"image", "png", _, _, _} = png + assert {"application", "pdf", _, _, _} = pdf + end + + @doc "t t f f" + test "generates multipart/alternative when text and html are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("Email html body") + |> Email.put_header("X-Custom-Header", "custom-value") + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "alternative", _, _, [text, html]} = EmailParser.parse(raw_data) + assert {"text", "plain", _, _, "Email text body"} = text + assert {"text", "html", _, _, "Email html body"} = html + end + + @doc "t t f t" + test "generates multipart/related when text, html and inline attachments are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("Email html body") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "related", _, _, [alternative, png]} = EmailParser.parse(raw_data) + assert {"multipart", "alternative", _, _, [text, html]} = alternative + assert {"image", "png", _, _, _} = png + assert {"text", "plain", _, _, "Email text body"} = text + assert {"text", "html", _, _, "Email html body"} = html + end + + @doc "t t t f" + test "generates multipart/mixed when text, html and regular attachements are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("Email html body") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [alternative, pdf]} = EmailParser.parse(raw_data) + assert {"multipart", "alternative", _, _, [text, html]} = alternative + assert {"text", "plain", _, _, "Email text body"} = text + assert {"text", "plain", _, _, "Email text body"} = text + assert {"text", "html", _, _, "Email html body"} = html + assert {"application", "pdf", _, _, _} = pdf + end + + @doc "t t t t" + test "generates multipart/mixed when text, html and both inline and regular attachements are provided" do + content = + TestHelpers.new_email() + |> Email.text_body("Email text body") + |> Email.html_body("Email html body") + |> Email.put_attachment(Path.join(__DIR__, "../../../support/pole.png"), + content_id: "img-1" + ) + |> Email.put_attachment(Path.join(__DIR__, "../../../support/invoice.pdf")) + |> Content.build_from_bamboo_email() + + %Content{ + Raw: %{ + Data: raw_data + } + } = content + + assert {"multipart", "mixed", _, _, [related, pdf]} = EmailParser.parse(raw_data) + assert {"multipart", "related", _, _, [alternative, png]} = related + assert {"multipart", "alternative", _, _, [text, html]} = alternative + assert {"image", "png", _, _, _} = png + assert {"text", "plain", _, _, "Email text body"} = text + assert {"application", "pdf", _, _, _} = pdf + assert {"text", "html", _, _, "Email html body"} = html + end +end diff --git a/test/support/pole.png b/test/support/pole.png new file mode 100644 index 0000000..3fda0cc Binary files /dev/null and b/test/support/pole.png differ