defmodule BambooSes.Encoding do
@moduledoc """
Encoding module contains various helper methods related to encoding of a strings and an email addresses
"""
@rfc1342_maximum_encoded_word_length 75
@doc """
Encodes an email address.
Returns encoded email address.
## Example
prepare_address({"", "john.doe@example.com"})
prepare_address({"John Doe", "john.doe@example.com"})
"""
@spec prepare_address(Bamboo.Email.address()) :: String.t()
def prepare_address({nil, address}), do: encode_address(address)
def prepare_address({"", address}), do: encode_address(address)
def prepare_address({name, address}) do
"\"#{maybe_rfc1342_encode(name)}\" <#{encode_address(address)}>"
end
@doc """
Encodes string to rfc1342 if needed
Returns encoded string
"""
@spec maybe_rfc1342_encode(String.t()) :: String.t()
def maybe_rfc1342_encode(string) when is_binary(string) do
should_encode? = not ascii?(string) || String.contains?(string, ["\"", "?", "\\"])
if should_encode? do
rfc1342_encode(string)
else
string
end
end
def maybe_rfc1342_encode(_), do: ""
@doc """
Checks if string contains only ASCII characters
Returns boolean value
"""
@spec ascii?(String.t()) :: boolean()
def ascii?(string) do
string
|> String.to_charlist()
|> Enum.all?(&(&1 < 127))
end
defp encode_address(address) do
[local_part, domain_part] = String.split(address, "@")
Enum.join([local_part, :idna.utf8_to_ascii(domain_part)], "@")
end
defp rfc1342_encode(string) when is_binary(string), do: rfc1342_encode(string, [])
defp rfc1342_encode("", acc), do: acc |> Enum.reverse() |> Enum.join(" ")
defp rfc1342_encode(string, acc) do
# https://tools.ietf.org/html/rfc1342
# > An encoded-word may not be more than 75 characters long, including
# > charset, encoding, encoded-text, and delimiters. If it is desirable
# > to encode more text than will fit in an encoded-word of 75
# > characters, multiple encoded-words (separated by SPACE or newline)
# > may be used.
maximum_possible_text_length =
@rfc1342_maximum_encoded_word_length - String.length(encode_word(""))
{encoded, rest} =
maximum_possible_text_length..1
|> Enum.reduce_while(nil, fn n, _ ->
{word, rest} = String.split_at(string, n)
encoded = encode_word(word)
if String.length(encoded) <= @rfc1342_maximum_encoded_word_length do
{:halt, {encoded, rest}}
else
{:cont, nil}
end
end)
rfc1342_encode(rest, [encoded | acc])
end
defp encode_word(word) do
"=?utf-8?B?#{Base.encode64(word)}?="
end
end