defmodule Bamboo.SMTPAdapter do @moduledoc """ Sends email using SMTP. Use this adapter to send emails through SMTP. This adapter requires that some settings are set in the config. See the example section below. ## Example config # In config/config.exs, or config.prod.exs, etc. config :my_app, MyApp.Mailer, adapter: Bamboo.SMTPAdapter, server: "smtp.domain", port: 1025, username: "your.name@your.domain", password: "pa55word", tls: :if_available, # can be `:always` or `:never` ssl: :false, # can be `:true` retries: 1 # Define a Mailer. Maybe in lib/my_app/mailer.ex defmodule MyApp.Mailer do use Bamboo.Mailer, otp_app: :my_app end """ @behaviour Bamboo.Adapter require Logger @required_configuration [:server, :port, :username, :password] @default_configuration %{tls: :if_available, ssl: :false, retries: 1, transport: :gen_smtp_client} defmodule SMTPError do @moduledoc false defexception [:message] def exception({reason, detail}) do message = """ There was a problem sending the email through SMTP. The error is #{inspect reason} More detail below: #{inspect detail} """ %SMTPError{message: message} end end def deliver(email, config) do gen_smtp_config = config |> to_gen_smtp_server_config email |> Bamboo.Mailer.normalize_addresses |> to_gen_smtp_message |> config[:transport].send_blocking(gen_smtp_config) |> handle_response end @doc false def handle_config(config) do config |> check_required_configuration |> put_default_configuration end defp handle_response({:error, reason, detail}) do raise SMTPError, {reason, detail} end defp handle_response(_) do :ok end defp add_bcc(body, %Bamboo.Email{bcc: recipients}) do add_smtp_header_line(body, :bcc, format_email_as_string(recipients, :bcc)) end defp add_cc(body, %Bamboo.Email{cc: recipients}) do add_smtp_header_line(body, :cc, format_email_as_string(recipients, :cc)) end defp add_custom_header(body, {key, value}) do add_smtp_header_line(body, key, value) end defp add_custom_headers(body, %Bamboo.Email{headers: headers}) do Enum.reduce(headers, body, &add_custom_header(&2, &1)) end defp add_ending_header(body) do add_smtp_line(body, "") end defp add_ending_multipart(body, delimiter) do add_smtp_line(body, "--#{delimiter}--") end defp add_html_body(body, %Bamboo.Email{html_body: html_body}, _multi_part_delimiter) when html_body == nil do body end defp add_html_body(body, %Bamboo.Email{html_body: html_body}, multi_part_delimiter) do body |> add_multipart_delimiter(multi_part_delimiter) |> add_smtp_header_line("Content-Type", "text/html;charset=UTF-8") |> add_smtp_header_line("Content-ID", "html-body") |> add_smtp_line("") |> add_smtp_line(html_body) end defp add_from(body, %Bamboo.Email{from: from}) do add_smtp_header_line(body, :from, format_email_as_string(from, :from)) end defp add_mime_header(body) do add_smtp_header_line(body, "MIME-Version", "1.0") end defp add_multipart_delimiter(body, delimiter) do add_smtp_line(body, "--#{delimiter}") end defp add_multipart_header(body, delimiter) do add_smtp_header_line(body, "Content-Type", ~s(multipart/alternative; boundary="#{delimiter}")) end defp add_smtp_header_line(body, type, content) when is_list(content) do Enum.reduce(content, body, &add_smtp_header_line(&2, type, &1)) end defp add_smtp_header_line(body, type, content) when is_atom(type) do add_smtp_header_line(body, String.capitalize(to_string(type)), content) end defp add_smtp_header_line(body, type, content) when is_binary(type) do add_smtp_line(body, "#{type}: #{content}") end defp add_smtp_line(body, content), do: body <> content <> "\r\n" defp add_subject(body, %Bamboo.Email{subject: subject}) do add_smtp_header_line(body, :subject, subject) end defp add_text_body(body, %Bamboo.Email{text_body: text_body}, _multi_part_delimiter) when text_body == nil do body end defp add_text_body(body, %Bamboo.Email{text_body: text_body}, multi_part_delimiter) do body |> add_multipart_delimiter(multi_part_delimiter) |> add_smtp_header_line("Content-Type", "text/plain;charset=UTF-8") |> add_smtp_header_line("Content-ID", "text-body") |> add_smtp_line("") |> add_smtp_line(text_body) end defp add_to(body, %Bamboo.Email{to: recipients}) do add_smtp_header_line(body, :to, format_email_as_string(recipients, :to)) end defp aggregate_errors(config, key, errors) do config |> Map.fetch(key) |> build_error(key, errors) end defp apply_default_configuration({:ok, value}, _default, config) when value != nil do config end defp apply_default_configuration(_not_found_value, {key, default_value}, config) do Map.put_new(config, key, default_value) end defp generate_multi_part_delimiter, do: "----=_Part_123456789_987654321.192837465" defp body(%Bamboo.Email{} = email) do multi_part_delimiter = generate_multi_part_delimiter "" |> add_subject(email) |> add_from(email) |> add_bcc(email) |> add_cc(email) |> add_to(email) |> add_custom_headers(email) |> add_mime_header |> add_multipart_header(multi_part_delimiter) |> add_ending_header |> add_text_body(email, multi_part_delimiter) |> add_html_body(email, multi_part_delimiter) |> add_ending_multipart(multi_part_delimiter) end defp build_error({:ok, value}, _key, errors) when value != nil, do: errors defp build_error(_not_found_value, key, errors) do ["Key #{key} is required for SMTP Adapter" | errors] end defp check_required_configuration(config) do @required_configuration |> Enum.reduce([], &aggregate_errors(config, &1, &2)) |> raise_on_missing_configuration(config) end defp format_email({nil, email}), do: email defp format_email({name, email}), do: "<#{email}> #{name}" defp format_email(emails) when is_list(emails) do Enum.map(emails, &format_email/1) end defp format_email(email, type) do email |> Bamboo.Formatter.format_email_address(type) |> format_email end defp format_email_as_string(emails) when is_list(emails) do Enum.join(emails, ", ") end defp format_email_as_string(email) do email end defp format_email_as_string(email, type) do email |> format_email(type) |> format_email_as_string end defp from(%Bamboo.Email{from: from}) do from |> format_email(:from) end defp put_default_configuration(config) do @default_configuration |> Enum.reduce(config, &put_default_configuration(&2, &1)) end defp put_default_configuration(config, default = {key, _default_value}) do config |> Map.fetch(key) |> apply_default_configuration(default, config) end defp raise_on_missing_configuration([], config), do: config defp raise_on_missing_configuration(errors, config) do formatted_errors = errors |> Enum.map(&("* #{&1}")) |> Enum.join("\n") raise ArgumentError, """ The following settings have not been found in your settings: #{formatted_errors} They are required to make the SMTP adapter work. Here you configuration: #{inspect config} """ end defp to(%Bamboo.Email{} = email) do email |> Bamboo.Email.all_recipients |> format_email(:to) end defp to_gen_smtp_message(%Bamboo.Email{} = email) do {from(email), to(email), body(email)} end defp to_gen_smtp_server_config(config) do Enum.reduce(config, [], &to_gen_smtp_server_config/2) end defp to_gen_smtp_server_config({:server, value}, config) do [{:relay, value} | config] end defp to_gen_smtp_server_config({:username, value}, config) do [{:username, value} | config] end defp to_gen_smtp_server_config({:password, value}, config) do [{:password, value} | config] end defp to_gen_smtp_server_config({:tls, value}, config) do [{:tls, value} | config] end defp to_gen_smtp_server_config({:port, value}, config) do [{:port, value} | config] end defp to_gen_smtp_server_config({:ssl, value}, config) do [{:ssl, value} | config] end defp to_gen_smtp_server_config({:retries, value}, config) do [{:retries, value} | config] end defp to_gen_smtp_server_config({_key, _value}, config) do config end end