Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle commas appropriately #23

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion lib/mimemail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule MimeMail do
end
body = case headers[:'content-type'] do
{"multipart/"<>_,%{boundary: bound}}->
body |> String.split(~r"\s*--#{bound}\s*") |> Enum.slice(1..-2) |> Enum.map(&from_string/1) |> Enum.map(&decode_body/1)
body |> String.split(~r"\s*--#{Regex.escape(bound)}\s*") |> Enum.slice(1..-2) |> Enum.map(&from_string/1) |> Enum.map(&decode_body/1)
{"text/"<>_,%{charset: charset}} ->
body |> Iconv.conv(charset,"utf8") |> ok_or(ensure_ascii(body)) |> ensure_utf8
_ -> body
Expand Down
35 changes: 22 additions & 13 deletions lib/mimemail_headers.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
defmodule MimeMail.Address do
defstruct name: nil, address: ""

@behaviour Access
defdelegate get_and_update(dict,k,v), to: Map
defdelegate fetch(dict,k), to: Map
defdelegate get(dict,k,v), to: Map
defdelegate pop(dict,k), to: Map

def decode(addr_spec) do
case Regex.run(~r/^([^<]*)<([^>]*)>/,addr_spec) do
[_,desc,addr]->%MimeMail.Address{name: MimeMail.Words.word_decode(desc), address: addr}
_ -> %MimeMail.Address{name: nil, address: String.strip(addr_spec)}
case Regex.run(~r/^([^<]*)<([^>]*)>/, addr_spec) do
[_, desc, addr] ->
name = desc |> MimeMail.Words.word_decode() |> String.trim("\"")
%MimeMail.Address{name: name, address: addr}
_ ->
%MimeMail.Address{name: nil, address: String.strip(addr_spec)}
end
end

Expand All @@ -17,7 +26,7 @@ end

defmodule MimeMail.Emails do
def parse_header(data) do
data |> String.strip |> String.split(~r/\s*,\s*/) |> Enum.map(&MimeMail.Address.decode/1)
data |> String.strip |> String.split(~r/(?!\B"[^"]*),(?![^"]*"\B)/) |> Enum.map(&MimeMail.Address.decode/1)
end
def decode_headers(%MimeMail{headers: headers}=mail) do
parsed=for {k,{:raw,v}}<-headers, k in [:from,:to,:cc,:cci,:'delivered-to'] do
Expand All @@ -27,7 +36,7 @@ defmodule MimeMail.Emails do
end
defimpl MimeMail.Header, for: List do # a list header is a mailbox spec list
def to_ascii(mail_list) do # a mail is a struct %{name: nil, address: ""}
mail_list
mail_list
|> Enum.filter(&match?(%MimeMail.Address{},&1))
|> Enum.map(&MimeMail.Header.to_ascii/1) |> Enum.join(", ")
end
Expand All @@ -37,13 +46,13 @@ end
defmodule MimeMail.Params do
def parse_header(bin), do: parse_kv(bin<>";",:key,[],[])

def parse_kv(<<c,rest::binary>>,:key,keyacc,acc) when c in [?\s,?\t,?\r,?\n,?;], do:
def parse_kv(<<c,rest::binary>>,:key,keyacc,acc) when c in [?\s,?\t,?\r,?\n,?;], do:
parse_kv(rest,:key,keyacc,acc) # not allowed characters in key, skip
def parse_kv(<<?=,?",rest::binary>>,:key,keyacc,acc), do:
def parse_kv(<<?=,?",rest::binary>>,:key,keyacc,acc), do:
parse_kv(rest,:quotedvalue,[],[{:"#{keyacc|>Enum.reverse|>to_string|>String.downcase}",""}|acc]) # enter in a quoted value, save key in res acc
def parse_kv(<<?=,rest::binary>>,:key,keyacc,acc), do:
def parse_kv(<<?=,rest::binary>>,:key,keyacc,acc), do:
parse_kv(rest,:value,[],[{:"#{keyacc|>Enum.reverse|>to_string|>String.downcase}",""}|acc]) # enter in a simple value, save key in res acc
def parse_kv(<<c,rest::binary>>,:key,keyacc,acc), do:
def parse_kv(<<c,rest::binary>>,:key,keyacc,acc), do:
parse_kv(rest,:key,[c|keyacc],acc) # allowed char in key, add to key acc
def parse_kv(<<?\\,?",rest::binary>>,:quotedvalue,valueacc,acc), do:
parse_kv(rest,:quotedvalue,[?"|valueacc],acc) # \" in quoted value is "
Expand All @@ -69,7 +78,7 @@ defmodule MimeMail.CTParams do
[value] -> {value,%{}}
end
end
def normalize({value,m},k) when k in
def normalize({value,m},k) when k in
[:"content-type",:"content-transfer-encoding",:"content-disposition"], do: {String.downcase(value),m}
def normalize(h,_), do: h
def decode_headers(%MimeMail{headers: headers}=mail) do
Expand All @@ -91,7 +100,7 @@ defmodule MimeMail.Words do
def word_encode(line) do
if is_ascii(line) do line else
for <<char::utf8<-line>> do
case char do
case char do
?\s -> ?_
c when c < 127 and c > 32 and c !== ?= and c !== ?? and c !== ?_-> c
c -> for(<<a,b<-Base.encode16(<<c::utf8>>)>>,into: "",do: <<?=,a,b>>)
Expand Down Expand Up @@ -121,9 +130,9 @@ defmodule MimeMail.Words do
end
def single_word_decode(str), do: "#{str} "

def q_to_binary("_"<>rest,acc), do:
def q_to_binary("_"<>rest,acc), do:
q_to_binary(rest,[?\s|acc])
def q_to_binary(<<?=,x1,x2>><>rest,acc), do:
def q_to_binary(<<?=,x1,x2>><>rest,acc), do:
q_to_binary(rest,[<<x1,x2>> |> String.upcase |> Base.decode16! | acc])
def q_to_binary(<<c,rest::binary>>,acc), do:
q_to_binary(rest,[c | acc])
Expand Down
8 changes: 4 additions & 4 deletions test/mails/free.eml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ From: Assistance Free <[email protected]>
ID-Courrier: 28601953
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="_NextPart_529786cba680f4cda8c47a92f2d7e09d"
boundary="_NextPart_(529786cba680f4cda8c47a92f2d7e09d)"
Message-Id: <[email protected]>
Date: Fri, 7 Nov 2014 10:15:04 +0100 (CET)




--_NextPart_529786cba680f4cda8c47a92f2d7e09d
--_NextPart_(529786cba680f4cda8c47a92f2d7e09d)
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: 8bit

Expand All @@ -55,7 +55,7 @@ Depuis un autre num
Web : http://assistance.free.fr/
Adresse : Free Haut D�bit 75371 PARIS CEDEX 08Free � 75371 Paris Cedex 08 � http://www.free.fr/
S.A.S au capital de 3.441.812 Euros � R.C.S. Paris : B 421 938 861 � N� TVA intra communautaire : FR 604 219 388 61
--_NextPart_529786cba680f4cda8c47a92f2d7e09d
--_NextPart_(529786cba680f4cda8c47a92f2d7e09d)
Content-Type: multipart/related;
boundary="_MixedPart_12c208616ad3684897f2a5137a7d00b1"

Expand Down Expand Up @@ -296,5 +296,5 @@ D8QoG6Tj7wNzAAAAAElFTkSuQmCC


--_MixedPart_12c208616ad3684897f2a5137a7d00b1--
--_NextPart_529786cba680f4cda8c47a92f2d7e09d--
--_NextPart_(529786cba680f4cda8c47a92f2d7e09d)--

19 changes: 16 additions & 3 deletions test/mime_headers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule MimeHeadersTest do
end

test "decode addresses headers" do
mail = File.read!("test/mails/encoded.eml")
mail = File.read!("test/mails/encoded.eml")
|> MimeMail.from_string
|> MimeMail.Emails.decode_headers
assert [%MimeMail.Address{name: "Jérôme Nicolle", address: "[email protected]"}]
Expand All @@ -22,6 +22,19 @@ defmodule MimeHeadersTest do
= mail.headers[:to]
end

test "decode addresses headers with quoted-commas" do
mail = %MimeMail{
headers: [
from: {:raw, "From: \"LastName, FirstName\" <[email protected]>"}
]
}

%MimeMail{headers: headers} = MimeMail.Emails.decode_headers(mail)
assert headers == [
from: [%MimeMail.Address{address: "[email protected]", name: "LastName, FirstName"}]
]
end

test "encode addresses headers" do
mail=%MimeMail{headers: [
to: [%MimeMail.Address{address: "[email protected]"},
Expand All @@ -33,7 +46,7 @@ defmodule MimeHeadersTest do
= (headers[:to] |> MimeMail.header_value |> String.replace(~r/\s+/," "))
assert "[email protected]" = MimeMail.header_value(headers[:from])
end

test "round trip encoded-words" do
assert "Jérôme Nicolle gave me €"
= ("Jérôme Nicolle gave me €" |> MimeMail.Words.word_encode |> MimeMail.Words.word_decode)
Expand All @@ -45,7 +58,7 @@ defmodule MimeHeadersTest do
end

test "decode str from q-encoded-word" do
assert "[FRnOG] [TECH] ToS implémentée chez certains transitaires"
assert "[FRnOG] [TECH] ToS implémentée chez certains transitaires"
= MimeMail.Words.word_decode("[FRnOG] =?UTF-8?Q?=5BTECH=5D_ToS_impl=C3=A9ment=C3=A9e_chez_certa?=\r\n =?UTF-8?Q?ins_transitaires?=")
end

Expand Down