diff --git a/lib/mimemail.ex b/lib/mimemail.ex index a723dc5..de78f0e 100644 --- a/lib/mimemail.ex +++ b/lib/mimemail.ex @@ -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 diff --git a/lib/mimemail_headers.ex b/lib/mimemail_headers.ex index 3ea7608..0cd638b 100644 --- a/lib/mimemail_headers.ex +++ b/lib/mimemail_headers.ex @@ -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 @@ -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 @@ -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 @@ -37,13 +46,13 @@ end defmodule MimeMail.Params do def parse_header(bin), do: parse_kv(bin<>";",:key,[],[]) - def parse_kv(<>,:key,keyacc,acc) when c in [?\s,?\t,?\r,?\n,?;], do: + def parse_kv(<>,: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(<>,:key,keyacc,acc), do: + def parse_kv(<>,: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(<>,:key,keyacc,acc), do: + def parse_kv(<>,: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(<>,:key,keyacc,acc), do: + def parse_kv(<>,:key,keyacc,acc), do: parse_kv(rest,:key,[c|keyacc],acc) # allowed char in key, add to key acc def parse_kv(<>,:quotedvalue,valueacc,acc), do: parse_kv(rest,:quotedvalue,[?"|valueacc],acc) # \" in quoted value is " @@ -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 @@ -91,7 +100,7 @@ defmodule MimeMail.Words do def word_encode(line) do if is_ascii(line) do line else for <> do - case char do + case char do ?\s -> ?_ c when c < 127 and c > 32 and c !== ?= and c !== ?? and c !== ?_-> c c -> for(<>)>>,into: "",do: <>) @@ -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(<><>rest,acc), do: + def q_to_binary(<><>rest,acc), do: q_to_binary(rest,[<> |> String.upcase |> Base.decode16! | acc]) def q_to_binary(<>,acc), do: q_to_binary(rest,[c | acc]) diff --git a/test/mails/free.eml b/test/mails/free.eml index 0d424ea..70801ab 100644 --- a/test/mails/free.eml +++ b/test/mails/free.eml @@ -26,14 +26,14 @@ From: Assistance Free ID-Courrier: 28601953 MIME-Version: 1.0 Content-Type: multipart/alternative; - boundary="_NextPart_529786cba680f4cda8c47a92f2d7e09d" + boundary="_NextPart_(529786cba680f4cda8c47a92f2d7e09d)" Message-Id: <20141107091504.E2BA4958020@grosminet-cron.centrapel.com> 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 @@ -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" @@ -296,5 +296,5 @@ D8QoG6Tj7wNzAAAAAElFTkSuQmCC --_MixedPart_12c208616ad3684897f2a5137a7d00b1-- ---_NextPart_529786cba680f4cda8c47a92f2d7e09d-- +--_NextPart_(529786cba680f4cda8c47a92f2d7e09d)-- diff --git a/test/mime_headers_test.exs b/test/mime_headers_test.exs index 18cc106..7f7ae11 100644 --- a/test/mime_headers_test.exs +++ b/test/mime_headers_test.exs @@ -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: "jerome@ceriz.fr"}] @@ -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\" "} + ] + } + + %MimeMail{headers: headers} = MimeMail.Emails.decode_headers(mail) + assert headers == [ + from: [%MimeMail.Address{address: "mailbox@domain.tld", name: "LastName, FirstName"}] + ] + end + test "encode addresses headers" do mail=%MimeMail{headers: [ to: [%MimeMail.Address{address: "frnog@frnog.org"}, @@ -33,7 +46,7 @@ defmodule MimeHeadersTest do = (headers[:to] |> MimeMail.header_value |> String.replace(~r/\s+/," ")) assert "frnog@frnog.org" = 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) @@ -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