Packages
swoosh
1.17.10
1.26.3
1.26.2
1.26.1
1.26.0
1.25.3
1.25.2
1.25.1
1.25.0
1.24.0
1.23.1
1.23.0
1.22.1
1.22.0
1.21.0
1.20.1
1.20.0
1.19.9
1.19.8
1.19.7
1.19.6
1.19.5
1.19.4
1.19.3
1.19.2
1.19.1
1.19.0
1.18.4
1.18.3
1.18.2
1.18.1
1.18.0
1.17.10
1.17.9
1.17.8
retired
1.17.7
retired
1.17.6
1.17.5
1.17.4
retired
1.17.3
1.17.2
1.17.1
1.17.0
1.16.12
1.16.11
1.16.10
1.16.9
1.16.8
1.16.7
1.16.6
1.16.5
1.16.4
1.16.3
1.16.2
retired
1.16.1
retired
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.4
1.14.3
1.14.2
1.14.1
1.14.0
1.13.0
1.12.0
1.11.6
1.11.5
1.11.4
1.11.3
1.11.2
1.11.1
retired
1.11.0
1.10.3
1.10.2
1.10.1
1.10.0
1.9.1
1.9.0
1.8.3
1.8.2
1.8.1
1.8.0
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.2
1.5.1
1.5.0
1.4.0
1.3.11
1.3.10
1.3.9
1.3.8
retired
1.3.7
1.3.6
retired
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.9
1.0.8
retired
1.0.7
retired
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.25.6
0.25.5
0.25.4
0.25.3
retired
0.25.2
0.25.1
0.25.0
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.5
0.23.4
0.23.3
0.23.2
0.23.1
0.23.0
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.1
0.16.0
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.0
0.10.0
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Compose, deliver and test your emails easily in Elixir. Supports SMTP, Sendgrid, Mandrill, Postmark, Mailgun and many more out of the box. Preview your emails in the browser. Test your email sending code.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/swoosh/adapters/mua.ex
defmodule Swoosh.Adapters.Mua do
@moduledoc """
An adapter for sending emails using the SMTP protocol.
> ### Dependencies {: .info}
>
> This adapter relies on the [Mua](https://github.com/ruslandoga/mua) and
> [Mail](https://github.com/DockYard/elixir-mail) libraries.
> Ensure they are added to your mix.exs file:
# mix.exs
def deps do
[
{:swoosh, "~> 1.3"},
{:mua, "~> 0.2.0"},
{:mail, "~> 0.3.0"},
# if on OTP version below 25
# {:castore, "~> 1.0"}
]
end
## Configuration
For direct email sending:
# config/config.exs
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.Mua
For sending emails via a relay:
# config/config.exs
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.Mua,
relay: "smtp.matrix.com",
port: 587,
auth: [username: "neo", password: "one"]
Define your mailer module:
# lib/sample/mailer.ex
defmodule Sample.Mailer do
use Swoosh.Mailer, otp_app: :sample
end
For supported configuration options, please see [`option()`](#t:option/0)
## Sending Email Directly
When the relay option is omitted, the adapter sends emails directly to the recipients' mail servers.
All recipients must be on the same host; otherwise, a `Swoosh.Adapters.Mua.MultihostError` is raised.
Ensure your application can make outgoing connections to port 25 and
that your sender domain has appropriate DNS records (e.g. SPF, DKIM).
> #### Short-lived Connections {: .warning}
>
> Each `deliver/2` call results in a new connection to the recipient's email server.
## Sending Email via a Relay
When the relay option is set, emails are sent through the specified relay, typically requiring authentication.
For example, you can use your Gmail account with an app password.
> #### Short-lived Connections {: .warning}
>
> Each `deliver/2` call results in a new connection to the relay. This is less efficient than `gen_smtp` which reuses long-lived connections.
> Future versions of this adapter may address this issue.
## CA Certificates
Starting with OTP 25, [system cacerts](https://www.erlang.org/doc/apps/public_key/public_key.html#cacerts_get/0)
are used by default for the [cacerts](https://www.erlang.org/doc/apps/ssl/ssl.html#t:client_option_cert/0) option:
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.Mua,
# this happens by default
ssl: [cacerts: :public_key.cacerts_get()]
For OTP versions below 25, [`CAStore.file_path/0`](https://hexdocs.pm/castore/CAStore.html#file_path/0) is used for the [cacertfile](https://www.erlang.org/doc/apps/ssl/ssl.html#t:client_option_cert/0) option:
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.Mua,
# this happens by default
ssl: [cacertfile: CAStore.file_path()]
This means that for OTP versions below 25, you need to add [CAStore](https://hex.pm/packages/castore) to your project.
You can also use custom certificates:
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.Mua,
ssl: [cacertfile: System.fetch_env!("MY_OWN_SMTP_CACERTFILE")]
> #### CA Certfile Cache {: .warning}
>
> When using the `:cacertfile` option, certificates are decoded with each new connection.
> To cache the decoded certificates, set `:persistent_term` for `:mua` to true:
>
> config :mua, persistent_term: true
"""
use Swoosh.Adapter, required_deps: [mail: Mail, mua: Mua]
defmodule MultihostError do
@moduledoc """
Raised when no relay is used and recipients contain addresses across multiple hosts.
For example:
email =
Swoosh.Email.new(
to: {"Mua", "mua@github.com"},
cc: [{"Swoosh", "mua@swoosh.github.com"}]
)
Swoosh.Adapters.Mua.deliver(email, _no_relay_config = [])
Fields:
- `:hosts` - the hosts for the recipients, `["github.com", "swoosh.github.com"]` in the example above
"""
@type t :: %__MODULE__{hosts: [Mua.host()]}
defexception [:hosts]
def message(%__MODULE__{hosts: hosts}) do
"expected all recipients to be on the same host, got: " <> Enum.join(hosts, ", ")
end
end
@type option :: Mua.option() | {:relay, Mua.host()}
@spec deliver(Swoosh.Email.t(), [option]) ::
{:ok, Swoosh.Email.t()} | {:error, Mua.error() | MultihostError.t()}
def deliver(email, config) do
recipients = recipients(email)
relay = Keyword.get(config, :relay)
recipients_by_host =
if relay do
[{relay, recipients}]
else
recipients
|> Enum.group_by(&host/1)
|> Map.to_list()
end
# we don't perform MX lookup when relay is used
config =
if relay do
Keyword.put_new(config, :mx, false)
else
config
end
case recipients_by_host do
[{host, recipients}] ->
sender = address(email.from)
message = render(email)
with {:ok, _receipt} <- Mua.easy_send(host, sender, recipients, message, config) do
{:ok, email}
end
[_ | _] = multihost ->
{:error, MultihostError.exception(hosts: :proplists.get_keys(multihost))}
end
end
defp address({_, address}) when is_binary(address), do: address
defp address(address) when is_binary(address), do: address
defp host(address) do
[_username, host] = String.split(address, "@")
host
end
defp recipients(%Swoosh.Email{to: to, cc: cc, bcc: bcc}) do
(List.wrap(to) ++ List.wrap(cc) ++ List.wrap(bcc))
|> Enum.map(&address/1)
|> Enum.uniq()
end
defp render(email) do
Mail.build_multipart()
|> put_headers(email)
|> maybe(&Mail.put_from/2, email.from)
|> maybe(&Mail.put_to/2, email.to)
|> maybe(&Mail.put_cc/2, email.cc)
|> maybe(&Mail.put_bcc/2, email.bcc)
|> maybe(&Mail.put_subject/2, email.subject)
|> maybe(&Mail.put_text/2, email.text_body)
|> maybe(&Mail.put_html/2, email.html_body)
|> maybe(&put_attachments/2, email.attachments)
|> Mail.render()
end
defp maybe(mail, _fun, empty) when empty in [nil, [], %{}], do: mail
defp maybe(mail, fun, value), do: fun.(mail, value)
defp put_attachments(mail, attachments) do
Enum.reduce(attachments, mail, fn attachment, mail ->
%Swoosh.Attachment{filename: filename, content_type: content_type} = attachment
data = Swoosh.Attachment.get_content(attachment)
Mail.put_attachment(mail, {filename, data}, headers: [content_type: content_type])
end)
end
defp put_headers(mail, swoosh_email) do
%{from: from, headers: headers} = swoosh_email
utc_now = DateTime.utc_now()
keys = headers |> Map.keys() |> Enum.map(&String.downcase/1)
has_date? = "date" in keys
has_message_id? = "message-id" in keys
headers = if has_date?, do: headers, else: Map.put(headers, "Date", utc_now)
headers =
if has_message_id? do
headers
else
address = address(from)
host = host(address)
Map.put(headers, "Message-ID", message_id(host, utc_now))
end
Enum.reduce(headers, mail, fn {key, value}, mail ->
Mail.Message.put_header(mail, key, value)
end)
end
defp message_id(host, utc_now) do
date = Calendar.strftime(utc_now, "%Y%m%d")
time = DateTime.to_time(utc_now)
{seconds_after_midnight, _ms} = Time.to_seconds_after_midnight(time)
disambiguator =
Base.hex_encode32(
<<
seconds_after_midnight::17,
:erlang.phash2({node(), self()}, 8_388_608)::23,
:erlang.unique_integer()::24
>>,
case: :lower,
padding: false
)
# e.g. <20241211.7aqmq6bb022i8@example.com>
"<#{date}.#{disambiguator}@#{host}>"
end
end