defmodule Swoosh.Adapters.Postmark do @moduledoc ~S""" An adapter that sends email using the Postmark API. For reference: [Postmark API docs](http://developer.postmarkapp.com/developer-send-api.html) ## Example # config/config.exs config :sample, Sample.Mailer, adapter: Swoosh.Adapters.Postmark, api_key: "my-api-key" # lib/sample/mailer.ex defmodule Sample.Mailer do use Swoosh.Mailer, otp_app: :sample end ## Example of sending emails using templates This will use Postmark's `withTemplate` endpoint. import Swoosh.Email new() |> from({"T Stark", "tony.stark@example.com"}) |> to({"Steve Rogers", "steve.rogers@example.com"}) |> put_provider_option(:template_id, "123456") |> put_provider_option(:template_model, %{name: "Steve", email: "steve@avengers.com"}) You can also use `template_alias` instead of `template_id`, if you use Postmark's [TemplateAlias](https://postmarkapp.com/developer/api/templates-api#email-with-template) feature. Note that you must include the `:template_model` provider option even if your template has no variables to interpolate. In this case you can pass an empty map: put_provider_option(email, :template_model, %{}) When sending batch emails using `:deliver_many` do not mix emails using templates with non-template emails. The use of templates impacts the API endpoint used and so the batch email collection should be of the same format. ## Example of sending emails with a tag This will add a tag to the sent Postmark's email. import Swoosh.Email new() |> from({"T Stark", "tony.stark@example.com"}) |> to({"Steve Rogers", "steve.rogers@example.com"}) |> subject("Hello, Avengers!") |> put_provider_option(:tag, "some tag") ## Provider Options * `:message_stream` (string) – `MessageStream`, configure the message stream for the email * `:metadata` (map) - `Metadata`, add metadata to an email * `:tag` (string) - `Tag`, to categorize outgoing email * `:template_id` (string) - `TemplateId`, the template used when sending email and only required if `:template_alias` is not specified * `:template_alias` (string), `TemplateAlias`, the alias of a template used when sending email and only required if `:template_id` is not specified * `:template_model` (map), `TemplateModel`, a map of key/value field to be used in the `HtmlBody`, `TextBody`, and `Subject` field in the template, required alongside `:template_id`/`:template_alias` * `:track_opens` (boolean) - `TrackOpens`, specify if open tracking needs to be enabled for this email. * `:track_links` (string) - `TrackOpens`, specify if link tracking needs to be enabled for this email. Valid values are: `None`, `HtmlAndText`, `HtmlOnly`, `TextOnly` * `:inline_css` (boolean) - `InlineCss`, specify if Postmark should apply the style blocks as inline attributes to the rendered HTML content. Default is true. """ use Swoosh.Adapter, required_config: [:api_key] alias Swoosh.Email import Swoosh.Email.Render @base_url "https://api.postmarkapp.com" def deliver(%Email{} = email, config \\ []) do headers = prepare_headers(config) params = email |> prepare_body() |> Swoosh.json_library().encode! url = [base_url(config), api_endpoint(email)] case Swoosh.ApiClient.post(url, headers, params, email) do {:ok, 200, _headers, body} -> {:ok, %{id: Swoosh.json_library().decode!(body)["MessageID"]}} {:ok, code, _headers, body} when code > 399 -> case Swoosh.json_library().decode(body) do {:ok, error} -> {:error, {code, error}} {:error, _} -> {:error, {code, body}} end {:error, reason} -> {:error, reason} end end def deliver_many(emails, config \\ []) def deliver_many([], _config) do {:ok, []} end def deliver_many(emails, config) when is_list(emails) do headers = prepare_headers(config) body = emails |> prepare_body() |> Swoosh.json_library().encode! url = [base_url(config), api_endpoint(List.first(emails), true)] case Swoosh.ApiClient.post(url, headers, body, List.first(emails)) do {:ok, 200, _headers, body} -> results = Enum.map(Swoosh.json_library().decode!(body), fn email_result -> %{ id: email_result["MessageID"], error_code: email_result["ErrorCode"], message: email_result["Message"], to: email_result["To"], submitted_at: email_result["SubmittedAt"] } end) {:ok, results} {:ok, code, _headers, body} when code > 399 -> case Swoosh.json_library().decode(body) do {:ok, error} -> {:error, {code, error}} {:error, _} -> {:error, {code, body}} end {:error, reason} -> {:error, reason} end end defp base_url(config), do: config[:base_url] || @base_url defp prepare_headers(config) do [ {"User-Agent", "swoosh/#{Swoosh.version()}"}, {"X-Postmark-Server-Token", config[:api_key]}, {"Content-Type", "application/json"}, {"Accept", "application/json"} ] end defp api_endpoint(email, batch \\ false) do case {email_uses_template?(email), batch} do {true, true} -> "/email/batchWithTemplates" {true, false} -> "/email/withTemplate" {false, true} -> "/email/batch" {false, false} -> "/email" end end defp email_uses_template?(%Email{} = email) do case email do %{provider_options: %{template_id: _, template_model: _}} -> true %{provider_options: %{template_alias: _, template_model: _}} -> true _ -> false end end defp prepare_body(emails) when is_list(emails) do if email_uses_template?(List.first(emails)) do %{"Messages" => Enum.map(emails, &prepare_body/1)} else Enum.map(emails, &prepare_body/1) end end defp prepare_body(email) do %{} |> prepare_from(email) |> prepare_to(email) |> prepare_subject(email) |> prepare_html(email) |> prepare_text(email) |> prepare_cc(email) |> prepare_bcc(email) |> prepare_attachments(email) |> prepare_reply_to(email) |> prepare_template(email) |> prepare_custom_headers(email) |> prepare_tag(email) |> prepare_metadata(email) |> prepare_message_stream(email) |> prepare_track_opens(email) |> prepare_track_links(email) |> prepare_inline_css(email) end defp prepare_from(body, %{from: from}), do: Map.put(body, "From", render_recipient(from)) defp prepare_to(body, %{to: to}), do: Map.put(body, "To", render_recipient(to)) defp prepare_cc(body, %{cc: []}), do: body defp prepare_cc(body, %{cc: cc}), do: Map.put(body, "Cc", render_recipient(cc)) defp prepare_bcc(body, %{bcc: []}), do: body defp prepare_bcc(body, %{bcc: bcc}), do: Map.put(body, "Bcc", render_recipient(bcc)) defp prepare_attachments(body, %{attachments: []}), do: body defp prepare_attachments(body, %{attachments: attachments}) do Map.put(body, "Attachments", Enum.map(attachments, &prepare_attachment/1)) end defp prepare_attachment(attachment) do attachment_data = %{ "Name" => attachment.filename, "ContentType" => attachment.content_type, "Content" => Swoosh.Attachment.get_content(attachment, :base64) } case attachment.type do :attachment -> attachment_data :inline -> Map.put(attachment_data, "ContentID", "cid:#{attachment.cid}") end end defp prepare_reply_to(body, %{reply_to: nil}), do: body defp prepare_reply_to(body, %{reply_to: {_name, address}}), do: Map.put(body, "ReplyTo", address) defp prepare_subject(body, %{subject: ""}), do: body defp prepare_subject(body, %{subject: subject}), do: Map.put(body, "Subject", subject) defp prepare_text(body, %{text_body: nil}), do: body defp prepare_text(body, %{text_body: text_body}), do: Map.put(body, "TextBody", text_body) defp prepare_html(body, %{html_body: nil}), do: body defp prepare_html(body, %{html_body: html_body}), do: Map.put(body, "HtmlBody", html_body) # example custom vars # # %{ # "template_id" => 123, # "template_model" => %{"name": 1, "company": 2} # } # # Or, using template_alias # # %{ # "template_alias" => "welcome", # "template_model" => %{"name": 1, "company": 2} # } defp prepare_template(body, %{provider_options: provider_options}), do: Enum.reduce(provider_options, body, &put_in_body/2) defp prepare_template(body, _email), do: body defp put_in_body({:template_model, val}, body_acc), do: Map.put(body_acc, "TemplateModel", val) defp put_in_body({:template_id, val}, body_acc), do: Map.put(body_acc, "TemplateId", val) defp put_in_body({:template_alias, val}, body_acc), do: Map.put(body_acc, "TemplateAlias", val) defp put_in_body(_, body_acc), do: body_acc defp prepare_custom_headers(body, %{headers: headers}) when map_size(headers) == 0, do: body defp prepare_custom_headers(body, %{headers: headers}) do custom_headers = Enum.map(headers, fn {k, v} -> %{"Name" => k, "Value" => v} end) Map.put(body, "Headers", custom_headers) end defp prepare_tag(body, %{provider_options: %{tag: tag_value}}) do Map.put(body, "Tag", tag_value) end defp prepare_tag(body, _), do: body defp prepare_metadata(body, %{provider_options: %{metadata: metadata}}) do Map.put(body, "Metadata", metadata) end defp prepare_metadata(body, _), do: body defp prepare_message_stream(body, %{provider_options: %{message_stream: value}}), do: Map.put(body, "MessageStream", value) defp prepare_message_stream(body, _), do: body defp prepare_track_opens(body, %{provider_options: %{track_opens: value}}), do: Map.put(body, "TrackOpens", value) defp prepare_track_opens(body, _), do: body defp prepare_track_links(body, %{provider_options: %{track_links: value}}), do: Map.put(body, "TrackLinks", value) defp prepare_track_links(body, _), do: body defp prepare_inline_css(body, %{provider_options: %{inline_css: value}}), do: Map.put(body, "InlineCss", value) defp prepare_inline_css(body, _), do: body end