Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions lib/mail/renderers/rfc_2822.ex
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,16 @@ defmodule Mail.Renderers.RFC2822 do
end
end

defp render_address({name, email}),
do: "#{encode_header_value(~s("#{name}"), :quoted_printable)} <#{validate_address(email)}>"
defp render_address({name, email}) do
address = validate_address(email)
encoded = encode_header_value(name, :quoted_printable)

if encoded == name do
~s("#{name}" <#{address}>)
else
"#{encoded} <#{address}>"
end
end

defp render_address(email), do: validate_address(email)

Expand Down Expand Up @@ -229,10 +237,24 @@ defmodule Mail.Renderers.RFC2822 do

defp wrap_encoded_words(value) do
:binary.split(value, "=\r\n", [:global])
|> Enum.map(fn chunk -> <<"=?UTF-8?Q?", chunk::binary, "?=">> end)
|> Enum.map(fn chunk ->
chunk = encode_encoded_word_text(chunk)
<<"=?UTF-8?Q?", chunk::binary, "?=">>
end)
|> Enum.join()
end

# Per RFC 2047 §5, encoded-words must be recognizable as atoms (RFC 2822 §3.2.4).
# Spaces become underscores per RFC 2047 §4.2(2).
# All other non-atext characters are QP-encoded.
defp encode_encoded_word_text(chunk) do
chunk
|> String.replace(" ", "_")
|> String.replace(~r/[^a-zA-Z0-9!#$%&'*+\-\/=?^_`{|}~]/, fn <<byte>> ->
"=" <> Base.encode16(<<byte>>)
end)
end

@doc """
Builds a RFC2822 timestamp from an Erlang timestamp

Expand Down
57 changes: 49 additions & 8 deletions test/mail/message_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ defmodule Mail.MessageTest do
|> Mail.put_subject(subject)
|> Mail.render()

encoded_subject = "=?UTF-8?Q?" <> Mail.Encoders.QuotedPrintable.encode(subject) <> "?="
encoded_subject = "=?UTF-8?Q?" <> encode_rfc2047(subject) <> "?="

assert String.contains?(txt, encoded_subject)
assert %Mail.Message{headers: %{"subject" => ^subject}} = Mail.Parsers.RFC2822.parse(txt)
Expand All @@ -244,14 +244,46 @@ defmodule Mail.MessageTest do
|> Mail.put_to(to)
|> Mail.render()

encoded_from =
~s(From: =?UTF-8?Q?"#{Mail.Encoders.QuotedPrintable.encode(elem(from, 0))}"?= <#{elem(from, 1)}>)

encoded_to =
~s(To: =?UTF-8?Q?"#{Mail.Encoders.QuotedPrintable.encode(elem(to, 0))}"?= <#{elem(to, 1)}>)
encoded_from = "From: =?UTF-8?Q?#{encode_rfc2047(elem(from, 0))}?= <#{elem(from, 1)}>"
encoded_to = "To: =?UTF-8?Q?#{encode_rfc2047(elem(to, 0))}?= <#{elem(to, 1)}>"

assert txt =~ encoded_from
assert txt =~ encoded_to

parsed = Mail.Parsers.RFC2822.parse(txt)
assert {elem(from, 0), elem(from, 1)} == parsed.headers["from"]
assert [{elem(to, 0), elem(to, 1)}] == parsed.headers["to"]
end

test "UTF-8 in addresses round-trips with spaces" do
from = {"Mäx Müstermann", "max@example.com"}

txt =
Mail.build()
|> Mail.put_from(from)
|> Mail.render()

refute txt =~ ~r/=\?UTF-8\?Q\?[^?]* [^?]*\?=/
parsed = Mail.Parsers.RFC2822.parse(txt)
assert from == parsed.headers["from"]
end

test "UTF-8 in addresses with comma in name" do
from = {"Max, Müstermänn", "max@example.com"}
to = {"Other User", "other@example.com"}

txt =
Mail.build()
|> Mail.put_from(from)
|> Mail.put_to(to)
|> Mail.render()

# Comma must be encoded as =2C, not literal, to avoid being parsed as recipient separator
refute txt =~ ~r/=\?UTF-8\?Q\?[^?]*,[^?]*\?=/
assert txt =~ "=2C"

parsed = Mail.Parsers.RFC2822.parse(txt)
assert from == parsed.headers["from"]
end

test "UTF-8 in other header" do
Expand All @@ -263,7 +295,7 @@ defmodule Mail.MessageTest do
|> Mail.render()

encoded_header_value =
"=?UTF-8?Q?" <> Mail.Encoders.QuotedPrintable.encode("READMEüä.md") <> "?="
"=?UTF-8?Q?" <> encode_rfc2047("READMEüä.md") <> "?="

assert String.contains?(message, encoded_header_value)

Expand All @@ -282,9 +314,18 @@ defmodule Mail.MessageTest do
|> Mail.render()

encoded_subject =
"=?UTF-8?Q?=C3=BCber alles=0Anew =3F=3D line some =D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C long line?="
"=?UTF-8?Q?=C3=BCber_alles=0Anew_=3F=3D_line_some_=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD?==?UTF-8?Q?=D1=8C-=D0=BE=D1=87=D0=B5=D0=BD=D1=8C_long_line?="

assert String.contains?(txt, encoded_subject)
assert %Mail.Message{headers: %{"subject" => ^subject}} = Mail.Parsers.RFC2822.parse(txt)
end

defp encode_rfc2047(string) do
string
|> Mail.Encoders.QuotedPrintable.encode()
|> String.replace(" ", "_")
|> String.replace(~r/[^a-zA-Z0-9!#$%&'*+\-\/=?^_`{|}~]/, fn <<byte>> ->
"=" <> Base.encode16(<<byte>>)
end)
end
end
10 changes: 5 additions & 5 deletions test/mail/renderers/rfc_2822_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,27 @@ defmodule Mail.Renderers.RFC2822Test do

assert Mail.Renderers.RFC2822.render_header("Subject", [
"Hello World 😀"
]) == "Subject: =?UTF-8?Q?Hello World =F0=9F=98=80?="
]) == "Subject: =?UTF-8?Q?Hello_World_=F0=9F=98=80?="

assert Mail.Renderers.RFC2822.render_header("Subject", [
"Café résumé"
]) == "Subject: =?UTF-8?Q?Caf=C3=A9 r=C3=A9sum=C3=A9?="
]) == "Subject: =?UTF-8?Q?Caf=C3=A9_r=C3=A9sum=C3=A9?="

assert Mail.Renderers.RFC2822.render_header("Subject", [
"Hello 世界 World"
]) == "Subject: =?UTF-8?Q?Hello =E4=B8=96=E7=95=8C World?="
]) == "Subject: =?UTF-8?Q?Hello_=E4=B8=96=E7=95=8C_World?="

assert Mail.Renderers.RFC2822.render_header("Subject", [
"normal subject\r\nReply-To: cleverhacker@example.com"
]) == "Subject: =?UTF-8?Q?normal subject=0D=0AReply-To: cleverhacker@example.com?="
]) == "Subject: =?UTF-8?Q?normal_subject=0D=0AReply-To=3A_cleverhacker=40example=2Ecom?="

assert Mail.Renderers.RFC2822.render_header("Subject", [
"tabs\t\t and spaces"
]) == "Subject: tabs\t\t and spaces"

assert Mail.Renderers.RFC2822.render_header("Subject", [
"delete \x7f vertical tab \x0b"
]) == "Subject: =?UTF-8?Q?delete =7F vertical tab =0B?="
]) == "Subject: =?UTF-8?Q?delete_=7F_vertical_tab_=0B?="
end

test "address headers renders list of recipients" do
Expand Down
Loading