defmodule Mail do
@moduledoc """
Mail primitive for composing messages.
Build a mail message with the `Mail` struct
mail =
Mail.build_multipart()
|> put_subject("How is it going?")
|> Mail.put_text("Just checking in")
|> Mail.put_to("joe@example.com")
|> Mail.put_from("brian@example.com")
"""
@doc """
Build a single-part mail
"""
def build(),
do: %Mail.Message{}
@doc """
Build a multi-part mail
"""
def build_multipart,
do: %Mail.Message{multipart: true}
@doc """
Primary hook for parsing
You can pass in your own custom parse module. That module
must have a `parse/1` function that accepts a string or list of lines
By default the `parser` will be `Mail.Parsers.RFC2822`
"""
def parse(message, parser \\ Mail.Parsers.RFC2822) do
parser.parse(message)
end
@doc """
Add a plaintext part to the message
Shortcut function for adding plain text part
Mail.put_text(%Mail.Message{}, "Some plain text")
If a text part already exists this function will replace that existing
part with the new part.
## Options
* `:charset` - The character encoding standard for content type
"""
def put_text(message, body, opts \\ [])
def put_text(%Mail.Message{multipart: true} = message, body, opts) do
message =
case Enum.find(message.parts, &Mail.Message.match_body_text/1) do
%Mail.Message{} = part -> Mail.Message.delete_part(message, part)
_ -> message
end
Mail.Message.put_part(message, Mail.Message.build_text(body, opts))
end
def put_text(%Mail.Message{} = message, body, opts) do
content_type =
case opts do
charset: charset ->
["text/plain", {"charset", charset}]
_else ->
"text/plain"
end
Mail.Message.put_body(message, body)
|> Mail.Message.put_header(:content_transfer_encoding, :quoted_printable)
|> Mail.Message.put_content_type(content_type)
end
@doc """
Find the text part of a given mail
If single part with `content-type` "text/plain", returns itself
If single part without `content-type` "text/plain", returns `nil`
If multipart with part having `content-type` "text/plain" will return that part
If multipart without part having `content-type` "text/plain" will return `nil`
"""
def get_text(%Mail.Message{multipart: true} = message) do
Enum.reduce_while(message.parts, nil, fn sub_message, acc ->
text_part = get_text(sub_message)
if text_part, do: {:halt, text_part}, else: {:cont, acc}
end)
end
def get_text(%Mail.Message{headers: %{"content-type" => "text/plain" <> _}} = message),
do: message
def get_text(%Mail.Message{headers: %{"content-type" => ["text/plain" | _]}} = message),
do: message
def get_text(%Mail.Message{}), do: nil
@doc """
Add an HTML part to the message
Mail.put_html(%Mail.Message{}, "<span>Some HTML</span>")
If a text part already exists this function will replace that existing
part with the new part.
## Options
* `:charset` - The character encoding standard for content type
"""
def put_html(message, body, opts \\ [])
def put_html(%Mail.Message{multipart: true} = message, body, opts) do
message =
case Enum.find(message.parts, &Mail.Message.match_content_type?(&1, "text/html")) do
%Mail.Message{} = part -> Mail.Message.delete_part(message, part)
_ -> message
end
Mail.Message.put_part(message, Mail.Message.build_html(body, opts))
end
def put_html(%Mail.Message{} = message, body, opts) do
content_type =
case opts do
charset: charset ->
["text/html", {"charset", charset}]
_else ->
"text/html"
end
Mail.Message.put_body(message, body)
|> Mail.Message.put_header(:content_transfer_encoding, :quoted_printable)
|> Mail.Message.put_content_type(content_type)
end
@doc """
Find the html part of a given mail
If single part with `content-type` "text/html", returns itself
If single part without `content-type` "text/html", returns `nil`
If multipart with part having `content-type` "text/html" will return that part
If multipart without part having `content-type` "text/html" will return `nil`
"""
def get_html(%Mail.Message{multipart: true} = message) do
Enum.reduce_while(message.parts, nil, fn sub_message, acc ->
html_part = get_html(sub_message)
if html_part, do: {:halt, html_part}, else: {:cont, acc}
end)
end
def get_html(%Mail.Message{headers: %{"content-type" => "text/html"}} = message), do: message
def get_html(%Mail.Message{headers: %{"content-type" => ["text/html", _]}} = message),
do: message
def get_html(%Mail.Message{}), do: nil
@doc """
Add an attachment part to the message
Mail.put_attachment(%Mail.Message{}, "README.md")
Mail.put_attachment(%Mail.Message{}, {"README.md", data})
Each call will add a new attachment part.
"""
def put_attachment(message, path_or_file_tuple, opts \\ [])
def put_attachment(%Mail.Message{multipart: true} = message, path, opts) when is_binary(path),
do: Mail.Message.put_part(message, Mail.Message.build_attachment(path, opts))
def put_attachment(%Mail.Message{multipart: true} = message, {filename, data}, opts),
do: Mail.Message.put_part(message, Mail.Message.build_attachment({filename, data}, opts))
def put_attachment(%Mail.Message{} = message, path, opts) when is_binary(path),
do: Mail.Message.put_attachment(message, path, opts)
def put_attachment(%Mail.Message{} = message, {filename, data}, opts),
do: Mail.Message.put_attachment(message, {filename, data}, opts)
@doc """
Determines the message has any attachment parts
Returns a `Boolean`
"""
def has_attachments?(%Mail.Message{} = message) do
walk_parts([message], {:cont, false}, fn message, _acc ->
case Mail.Message.is_attachment?(message) do
true -> {:halt, true}
false -> {:cont, false}
end
end)
|> elem(1)
end
@doc """
Determines the message has any text parts
Returns a `Boolean`
"""
def has_text_parts?(%Mail.Message{} = message) do
walk_parts([message], {:cont, false}, fn message, _acc ->
case Mail.Message.is_text_part?(message) do
true -> {:halt, true}
false -> {:cont, false}
end
end)
|> elem(1)
end
@doc """
Walks the message parts and collects all attachments
Each member in the list is `{filename, content}`
"""
def get_attachments(%Mail.Message{} = message) do
walk_parts([message], {:cont, []}, fn message, acc ->
case Mail.Message.is_attachment?(message) do
true ->
filename =
case Mail.Message.get_header(message, :content_disposition) do
["attachment" | properties] ->
Enum.find_value(properties, "Unknown", fn {key, value} ->
key == "filename" && value
end)
end
{:cont, List.insert_at(acc, -1, {filename, message.body})}
false ->
{:cont, acc}
end
end)
|> elem(1)
end
defp walk_parts(_parts, {:halt, acc}, _fun), do: {:halt, acc}
defp walk_parts([], {:cont, acc}, _fun), do: {:cont, acc}
defp walk_parts([message | parts], {:cont, acc}, fun) do
{tag, acc} = fun.(message, acc)
{tag, acc} = walk_parts(message.parts, {tag, acc}, fun)
walk_parts(parts, {tag, acc}, fun)
end
@doc """
Add a new `subject` header
Mail.put_subject(%Mail.Message{}, "Welcome to DockYard!")
%Mail.Message{headers: %{subject: "Welcome to DockYard!"}}
"""
def put_subject(message, subject),
do: Mail.Message.put_header(message, "subject", subject)
@doc ~S"""
Retrieve the `subject` header
"""
def get_subject(message),
do: Mail.Message.get_header(message, "subject")
@doc """
Add new recipients to the `to` header
Recipients can be added as a single string or a list of strings.
The list of recipients will be concated to the previous value.
Mail.put_to(%Mail.Message{}, "one@example.com")
%Mail.Message{headers: %{to: ["one@example.com"]}}
Mail.put_to(%Mail.Message{}, ["one@example.com", "two@example.com"])
%Mail.Message{headers: %{to: ["one@example.com", "two@example.com"]}}
Mail.put_to(%Mail.Message{}, "one@example.com")
|> Mail.put_to(["two@example.com", "three@example.com"])
%Mail.Message{headers: %{to: ["one@example.com", "two@example.com", "three@example.com"]}}
The value of a recipient must conform to either a string value or a tuple with two elements,
otherwise an `ArgumentError` is raised.
Valid forms:
* `"user@example.com"`
* `"Test User <user@example.com>"`
* `{"Test User", "user@example.com"}`
"""
def put_to(message, recipients)
def put_to(message, recipients) when is_list(recipients) do
validate_recipients(recipients)
Mail.Message.put_header(message, "to", (get_to(message) || []) ++ recipients)
end
def put_to(message, recipient),
do: put_to(message, [recipient])
@doc ~S"""
Retrieves the list of recipients from the `to` header
"""
def get_to(message),
do: Mail.Message.get_header(message, "to")
@doc """
Add new recipients to the `cc` header
Recipients can be added as a single string or a list of strings.
The list of recipients will be concated to the previous value.
Mail.put_cc(%Mail.Message{}, "one@example.com")
%Mail.Message{headers: %{cc: ["one@example.com"]}}
Mail.put_cc(%Mail.Message{}, ["one@example.com", "two@example.com"])
%Mail.Message{headers: %{cc: ["one@example.com", "two@example.com"]}}
Mail.put_cc(%Mail.Message{}, "one@example.com")
|> Mail.put_cc(["two@example.com", "three@example.com"])
%Mail.Message{headers: %{cc: ["one@example.com", "two@example.com", "three@example.com"]}}
The value of a recipient must conform to either a string value or a tuple with two elements,
otherwise an `ArgumentError` is raised.
Valid forms:
* `"user@example.com"`
* `"Test User <user@example.com>"`
* `{"Test User", "user@example.com"}`
"""
def put_cc(message, recipients)
def put_cc(message, recipients) when is_list(recipients) do
validate_recipients(recipients)
Mail.Message.put_header(message, "cc", (get_cc(message) || []) ++ recipients)
end
def put_cc(message, recipient),
do: put_cc(message, [recipient])
@doc ~S"""
Retrieves the recipients from the `cc` header
"""
def get_cc(message),
do: Mail.Message.get_header(message, "cc")
@doc """
Add new recipients to the `bcc` header
Recipients can be added as a single string or a list of strings.
The list of recipients will be concated to the previous value.
Mail.put_bcc(%Mail.Message{}, "one@example.com")
%Mail.Message{headers: %{bcc: ["one@example.com"]}}
Mail.put_bcc(%Mail.Message{}, ["one@example.com", "two@example.com"])
%Mail.Message{headers: %{bcc: ["one@example.com", "two@example.com"]}}
Mail.put_bcc(%Mail.Message{}, "one@example.com")
|> Mail.put_bcc(["two@example.com", "three@example.com"])
%Mail.Message{headers: %{bcc: ["one@example.com", "two@example.com", "three@example.com"]}}
The value of a recipient must conform to either a string value or a tuple with two elements,
otherwise an `ArgumentError` is raised.
Valid forms:
* `"user@example.com"`
* `"Test User <user@example.com>"`
* `{"Test User", "user@example.com"}`
"""
def put_bcc(message, recipients)
def put_bcc(message, recipients) when is_list(recipients) do
validate_recipients(recipients)
Mail.Message.put_header(message, "bcc", (get_bcc(message) || []) ++ recipients)
end
def put_bcc(message, recipient),
do: put_bcc(message, [recipient])
@doc ~S"""
Retrieves the recipients from the `bcc` header
"""
def get_bcc(message),
do: Mail.Message.get_header(message, "bcc")
@doc """
Add a new `from` header
Mail.put_from(%Mail.Message{}, "user@example.com")
%Mail.Message{headers: %{from: "user@example.com"}}
"""
def put_from(message, sender),
do: Mail.Message.put_header(message, "from", sender)
@doc ~S"""
Retrieves the `from` header
"""
def get_from(message),
do: Mail.Message.get_header(message, "from")
@doc """
Add a new `reply-to` header
Mail.put_reply_to(%Mail.Message{}, "user@example.com")
%Mail.Message{headers: %{reply_to: "user@example.com"}}
"""
def put_reply_to(message, reply_address),
do: Mail.Message.put_header(message, "reply-to", reply_address)
@doc ~S"""
Retrieves the `reply-to` header
"""
def get_reply_to(message),
do: Mail.Message.get_header(message, "reply-to")
@doc """
Returns a unique list of all recipients
Will collect all recipients from `to`, `cc`, and `bcc`
and returns a unique list of recipients.
"""
def all_recipients(message) do
(List.wrap(Mail.get_to(message)) ++
List.wrap(Mail.get_cc(message)) ++ List.wrap(Mail.get_bcc(message)))
|> Enum.uniq()
end
@doc """
Primary hook for rendering
You can pass in your own custom render module. That module
must have `render/1` function that accepts a `Mail.Message` struct.
By default the `renderer` will be `Mail.Renderers.RFC2822`
"""
def render(message, renderer \\ Mail.Renderers.RFC2822) do
renderer.render(message)
end
defp validate_recipients([]), do: nil
defp validate_recipients([recipient | tail]) do
case recipient do
{name, address} when is_binary(name) and is_binary(address) ->
validate_recipients(tail)
address when is_binary(address) ->
validate_recipients(tail)
other ->
raise ArgumentError,
message: """
The recipient `#{inspect(other)}` is invalid.
Recipients must be in the format of either a string,
or a tuple with two elements `{name, address}`
"""
end
end
end