Giter Club home page Giter Club logo

elixir-mail's Introduction

Mail

Build Status

An RFC2822 implementation in Elixir, built for composability.

Mail is built and maintained by DockYard, contact us for expert Elixir and Phoenix consulting.

Installation

def deps do
  [
    # Get from hex
    {:mail, "~> 0.3"},

    # Or use the latest from master
    {:mail, github: "DockYard/elixir-mail"}
  ]
end

Building

You can quickly build an RFC2822 spec compliant message.

Single-Part

message =
  Mail.build()
  |> Mail.put_text("A great message")
  |> Mail.put_to("[email protected]")
  |> Mail.put_from("[email protected]")
  |> Mail.put_subject("Open me")

Multi-Part

message =
  Mail.build_multipart()
  |> Mail.put_text("Hello there!")
  |> Mail.put_html("<h1>Hello there!</h1>")
  |> Mail.put_attachment("path/to/README.md")
  |> Mail.put_attachment({"README.md", file_data})

Rendering

After you have built your message you can render it:

rendered_message = Mail.render(message)

Parsing

If you'd like to parse an already rendered message back into a data model:

%Mail.Message{} = message = Mail.parse(rendered_message)

There are more functions described in the docs

Authors

We are very thankful for the many contributors

Versioning

This library follows Semantic Versioning

Looking for help with your Elixir project?

At DockYard we are ready to help you build your next Elixir project. We have a unique expertise in Elixir and Phoenix development that is unmatched. Get in touch!

At DockYard we love Elixir! You can read our Elixir blog posts or come visit us at The Boston Elixir Meetup that we organize.

Want to help?

Please do! We are always looking to improve this library. Please see our Contribution Guidelines on how to properly submit issues and pull requests.

Legal

DockYard, Inc. © 2015

@dockyard

Licensed under the MIT license

elixir-mail's People

Contributors

andrew-lewin avatar andrewtimberlake avatar angelikatyborska avatar anthonator avatar bcardarella avatar bjfish avatar dalthon avatar daniel-xu avatar danxexe avatar fire-dragon-dol avatar icedragon200 avatar iwarshak avatar kalys avatar kianmeng avatar koleksiuk avatar kovus avatar marocchino avatar michallepicki avatar nitrino avatar princemaple avatar rafaeliga avatar rgreenjr avatar rgtjr avatar sam701 avatar serpent213 avatar shribe avatar trevoke avatar veverkap avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elixir-mail's Issues

MatchError when parsing attachments with a content disposition that does not contain a filename.

Version

0.2.3

Test Case

  test "parses an attachment with a minimal content-disposition" do
    message =
      parse_email("""
      To: Test User <[email protected]>, Other User <[email protected]>
      CC: The Dude <[email protected]>, Batman <[email protected]>
      From: Me <[email protected]>
      Content-Type: multipart/mixed; boundary="foobar"
      Date: Fri, 1 Jan
       2016 00:00:00 +0000

      --foobar
      Content-Type: multipart/alternative; boundary="bazqux"

      --bazqux
      Content-Type: text/plain

      This is some text

      --bazqux
      Content-Type: text/html

      <h1>This is the HTML</h1>

      --bazqux--
      --foobar
      Content-Type: text/markdown
      Content-Disposition: attachment
      Content-Transfer-Encoding: base64

      SGVsbG8gd29ybGQh
      --foobar--
      """)

    _attachments = Mail.get_attachments(message)
  end

This test will fail, it is a trimmed down version of the test defined at test/mail/parsers/rfc_2822_test.exs:218 - the only functional difference is that line 244 Content-Disposition: attachment; filename=README.md, we remove the portion ;filename=README.md.

Steps to reproduce

See above and run that test - not entirely sure what the spec says about this but I'm seeing instances in the wild of the disposition being minimally defined to just be "attachment"

Expected Behavior

Should be able to get attachments from such messages in some fashion.

Actual Behavior

Attempting to get attachments from such messages results in a MatchError, as the system properly picks out that an attachment is included, but then forces a content-disposition match in get_attachments/1 in lib/mail.ex:

def get_attachments(%Mail.Message{} = message) do
    walk_parts([message], {:cont, []}, fn message, acc ->
      case Mail.Message.is_attachment?(message) do
        true ->
          ["attachment", {"filename", filename} | _] =
            Mail.Message.get_header(message, :content_disposition)

          {:cont, List.insert_at(acc, -1, {filename, message.body})}

        false ->
          {:cont, acc}
      end
    end)
    |> elem(1)
  end

`email_regex` is invalid

Version

0.2.0

Test Case

The regular expression used to check email is wrong. It won't accept mail addresses with a domain part containing a non-word character.

Steps to reproduce

Mail.build() |> Mail.put_to("[email protected]") |> Mail.render()

Expected Behavior

The Mail.render fn should accept all valid email addresses (or at least all valid domain names).

Actual Behavior

All domains containing a non-word character (\w) are rejected.

Note that too strict email address validation is frowned upon. Email addresses are very hard to verify and very hard to get right. See https://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx/ for some inspiration.

In this case, the check is horribly wrong.

Can't pass charset to Content-Type headers

Omitting the charset in the Content-Type header makes some email clients use arbitrary encodings, for example Thunderbird seems to default to ISO-8859-1/15. I would like some input on what the API should look like to allow passing this, and to have it default to utf-8.

I'm thinking of adding a charset field to the Mail.Message struct and change build_html/build_text to also allow passing an optional charset param.

I can work on a PR once we decide if this is the right approach.

Thanks

Bug: Parsing of obsolete date format with timezone UT

Version

0.2.3

Test Case

Mail with the following date:

Date: Tue, 12 May 2020 12:08:24 UT

Expected Behavior

The date is a valid, obsolete version according to RFC2822, so it should parse.

Actual Behavior

The following error occurs:

** (FunctionClauseError) no function clause matching in Mail.Parsers.RFC2822.erl_from_timestamp/1
(mail 0.2.3) lib/mail/parsers/rfc_2822.ex:50: Mail.Parsers.RFC2822.erl_from_timestamp("12 May 2020 12:08:24 UT")
(mail 0.2.3) lib/mail/parsers/rfc_2822.ex:200: Mail.Parsers.RFC2822.parse_headers/2
(mail 0.2.3) lib/mail/parsers/rfc_2822.ex:21: Mail.Parsers.RFC2822.parse/1

Possible fix:

The following function is already present:

  # This adds support for a now obsolete format
  # https://tools.ietf.org/html/rfc2822#section-4.3
  def erl_from_timestamp(
        <<date::binary-size(2), " ", month::binary-size(3), " ", year::binary-size(4), " ",
          hour::binary-size(2), ":", minute::binary-size(2), ":", second::binary-size(2), " ",
          timezone::binary-size(3), _rest::binary>>
      ) do
    erl_from_timestamp("#{date} #{month} #{year} #{hour}:#{minute}:#{second} (#{timezone})")
  end

However, it expects a timezone with exactly three letters, which is true for all timezones except UT for the "regular" timezones.

Regarding the value for obsolete timezones, specified via obs-zone in the RFC2822, also military timezones with one letter are described and the RFC states that other timezones with three to five letters had been used as well. Therefore, I propose to just skip any potential trailing letters and set the actual timezone to +00:00.

The following function might be placed right under the one mentioned above:

  # additional fix for obsolete, non-three-letter-timezones
  def erl_from_timestamp(
        <<date::binary-size(2), " ", month::binary-size(3), " ", year::binary-size(4), " ",
          hour::binary-size(2), ":", minute::binary-size(2), ":", second::binary-size(2), " ",
         _rest::binary>>
      ) do
    erl_from_timestamp("#{date} #{month} #{year} #{hour}:#{minute}:#{second} (+00:00)")
  end

no function clause matching in Mail.put_attachment/2 -when Adding Attachment from data, fails on put_attachment guard

1.0

Test Case

https://gist.github.com/misaelpc/40e58a98c30de2bd22678b9416675264

Steps to reproduce

When memory attachment is added

|> Mail.put_attachment({"README.md", file_data})

The guard for function:

def put_attachment(%Mail.Message{multipart: true} = message, path) when is_binary(path),
    do: Mail.Message.put_part(message, Mail.Message.build_attachment(path))

Fails because we are sending a tuple

Expected Behavior

Another guard should be added:

def put_attachment(%Mail.Message{multipart: true} = message, path) when is_tuple(path),
    do: Mail.Message.put_part(message, Mail.Message.build_attachment(path))

Actual Behavior

no function clause matching in Mail.put_attachment/2 raise when adding in memory attachment

Charset handling?

It looks like this library handles base64 etc. encoding of the mail but not the charset of decoded mail, for example ASCII, SHIFT-JIS, ISO-2022-JP, etc. (Please correct if I'm wrong)

Ruby's Mail gem handles this nicely, refer to the
#pick_encoding method

Extract address from "to", "from", "cc", etc.

I've run into an issue when using gen_smtp where providing an address that's a tuple or contains a name causes the SMTP server (AWS SES in this case) to return an invalid email error. The fix is to extract the address from the tuple/string with name. I'm curious if introducing something like get_address(message, header) makes sense for this project. I'd expect this to be a common problem for folks.

Here's an example of the problem I'm referring to.

message =
  Mail.build()
  |> Mail.put_from("Me <[email protected]>")
  |> Mail.put_to("You <[email protected]>")
  |> Mail.put_subject("Hello, you!")
  |> Mail.put_text("Sup")

from = Mail.get_from(message)  # returns "Me <[email protected]>"
to = Mail.get_to(message) # returns "You <[email protected]>"
body = Mail.render(message)

:gen_smtp_client({ from, to, body }, config) # returns a 533 invalid address 😵

You can fix the the problem by making both from and to plain addresses instead of the name/address combo. However, the From and To headers now won't contain the name. This puts the burden on the users of mail to figure this out.

My proposal is to provide a get_address/2 function that would provide this functionality.

message =
  Mail.build()
  |> Mail.put_from("Me <[email protected]>")
  |> Mail.put_to("You <[email protected]>")
  |> Mail.put_subject("Hello, you!")
  |> Mail.put_text("Sup")

from = Mail.get_address(message, :from) # returns "[email protected]"
to = Mail.get_address(message, :to) # returns "[email protected]"
body = Mail.render(message)

:gen_smtp_client({ from, to, body }, config) # it works 🎉

We've implemented something similar in our project and I'd be happy to provide a PR.

parse the envelope as well

I try to use elixir-mail to parse incoming mails. But they always start with the From-address of the envelope:

From [email protected]  Sat Mar 19 09:08:14 2016
Return-Path: <[email protected]>
X-Original-To: [email protected]
Delivered-To: [email protected]
Received: by example.com (Postfix, from userid 1000)
    id F2B491A5F34; Sat, 19 Mar 2016 09:08:13 +0100 (CET)
To: [email protected]
Subject: say it loud
Message-Id: <[email protected]>
Date: Sat, 19 Mar 2016 09:08:13 +0100 (CET)
From: [email protected] (foo)

hello world

Currently elixir-mail parses 'only' the inside the the mail, not the envelope.

Do you plan to deal with this case as well? I would like to send you a pull request but first I would like to know whether you are interested?

3.6.7. Trace fields parsing feature request

Wondering if it is possible to parse out the trace fields as a feature of this library.

below is an example of a pesky mailing list email that needs to be parsed into an elixir data structure.

Received: from o2.sendgrid.meetup.com (o2.sendgrid.meetup.com [167.89.52.198])
  by stink.net (Postfix) with ESMTP id DD3A24082C
  for <[email protected]>; Wed, 17 May 2017 19:33:37 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; d=meetup.com;
  h=from:sender:to:subject:mime-version:content-type:content-transfer-encoding:reply-to:list-id;
  s=s1; bh=piU8Iw5s8gvft3sBDhNC2brnqOM=; b=Nwcb6ftfe8OOepzB6+/OGOR
  VDgsaHhrWgjBuCMVA7akYBWBIspqmmmvdRgQa91byXLCkJOG5V2ji8k/jPWiCiwx
  pnaWzs7ahqSZnpzIUTVkoiGE3vHXyG8PpAdu9eiqyP/drCbdhvq5E+qCL8h1BK8h
  +LPen3N1vyicoJMtYv5U=
Received: by filter0981p1mdw1.sendgrid.net with SMTP id filter0981p1mdw1-12567-591CA591-12
        2017-05-17 19:33:37.126823786 +0000 UTC
Received: from mail.sendgrid.meetup.com (ec2-34-196-236-240.compute-1.amazonaws.com [34.196.236.240])
  by ismtpd0003p1iad1.sendgrid.net (SG) with ESMTP id 3N3kGOcpQzeoR1k1xtPE2A
  for <[email protected]>; Wed, 17 May 2017 19:33:37.149 +0000 (UTC)
Received: from mail.meetup.com (ip-10-192-12-125.ec2.internal [10.192.12.125])
  by mail.sendgrid.meetup.com (Postfix) with ESMTP id 1D90B40734
  for <[email protected]>; Wed, 17 May 2017 19:33:37 +0000 (UTC)
From: Anita Elder <[email protected]>
Sender: [email protected]
To: [email protected]
Subject: [Seattle-Photography-Club] Canon Explorers of Light: Stephen
 Johnson
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
X-MEETUP-TRACK: bc2
Reply-To: [email protected]
Mailing-List: list [email protected]; contact [email protected]
List-Id: <Seattle-Photography-Club-list.meetup.com>
Errors-To: [email protected]
Precedence: list
X-MEETUP-RECIP-ID: 213196484
X-MEETUP-MESG-ID: 80062686
Message-ID: <1621765400.1495049617110.JavaMail.nobody@9634c533c129>
Date: Wed, 17 May 2017 19:33:37 +0000 (UTC)
X-SG-EID: NxWPY2qdeEpd+G0Jh0zN3ogQlnZvFkv70eOuyeI5GmC4YpepTsxJ8KHY65q4kVGds4Ihwxx3IfqFOn
 Bs3ro1Oir7Ji2CS6cCcdMUcJryaHoibYhqElAtEl7bUXYkzzESH3zg0GZWmeOrmjqDgIKmaxULqYNw
 6q1xW1Lpfd1cJJXpbd26dE4eLrCe8rbAu3zPvcMJD+BF7FHw4OuVI16PETchca7JjjCDoPvctAUk6u
 0HMxZGdGbO7wSwQVo6uW3rHpxxnNkugXEg2uPVqV0Npg==

<html><body><div style=3D"color:#333;font-family:Verdana,Arial,sans-serif;f=
ont-size:12px;"><font face=3D"verdana,arial,sans-serif"><p>Tickets are now =
on sale for the November 17, 2017 speaker series featuring Stephen John, a =
Canon Explorers of Light.</p>
<p>More information and to purchase tickets (modest prices), go to=C2=A0<a =
href=3D"https://www.eventbrite.com/e/canon-explorer-of-light-speaker-ticket=
s-32421102390">https://www.eventbrite.com/e/canon-explorer-of-light-speaker=
-tickets-32421102390</a></p>
<p>Anita Elder,</p>
<p>Chair, Seattle Mountaineers Photography Committee</p>
<p>=C2=A0</p>
<br /><br />
<br />
<br />
--<br />
Please Note: If you hit "<strong>REPLY</strong>", your message will be sent=
 to <strong>everyone</strong> on this mailing list (<a href=3D"mailto:Seatt=
[email protected]">[email protected]=
om</a>)<br />
This message was sent by Meetup on behalf of <a href=3D"https://www.meetup.=
com/Seattle-Photography-Club/members/8681207/">Anita Elder</a> from <a href=
=3D"https://www.meetup.com/Seattle-Photography-Club/">Seattle Photography C=
lub</a>.
<br />
To report this message, please <a href=3D"https://www.meetup.com/report_abu=
se/mailing_list/80062686/">click here</a><br>
To block the sender of this message, please <a href=3D"https://www.meetup.c=
om/members/8681207/">click here</a><br>
To unsubscribe from special announcements from your Organizer(s), <a href=
=3D"https://www.meetup.com/Seattle-Photography-Club/optout/?submit=3Dtrue&_=
ms_unsub=3Dtrue&email=3DorgBdcst">click here</a>
<br /><br />
<small style=3D"color:gray">
<a href=3D"#" style=3D"color:#ccc;text-decoration:none;cursor:default;">Mee=
tup, POB 4668 #37895 NY NY USA 10163</a> | [email protected]
</small>
</div>
<img src=3D"https://u3922699.ct.sendgrid.net/wf/open?upn=3Dcn-2FFNZ-2Bh3bzG=
fqDtr0yClboILZ3qImoeGLyEBqXj5t1f7PnU24gu-2FM1Uh9nUBh4kPfURrKJNFU93d9YUf-2BE=
I2LiIM3KGCXYM0cnUb4f0Ut3VEa6i2Erl7ISMal4m-2Fn-2FZJIOw24cCHl8dnkBTUK85VrlOPd=
L22XgrHod6WZJbPMPLa3FcXw2bWhC8nK-2BKtj-2BG3HSFjcUTdK5BOkUe20nc0v7cC8bCavU1H=
tdzZxtzwh0DxjEUUabDuE4mc9AfpEWK" alt=3D"" width=3D"1" height=3D"1" border=
=3D"0" style=3D"height:1px !important;width:1px !important;border-width:0 !=
important;margin-top:0 !important;margin-bottom:0 !important;margin-right:0=
 !important;margin-left:0 !important;padding-top:0 !important;padding-botto=
m:0 !important;padding-right:0 !important;padding-left:0 !important;"/>
</body></html>

without a reply_to or return_path "for" trace needs to be utilized to know the intended recipient.

Parsing removes newline(\n) characters

Version

0.2.3

Test Case

eml = "Delivered-To: [email protected]\nReceived: by 2002:a05:6520:444e:b0:157:ec88:5685 with SMTP id r14csp744771lkv;\n        Fri, 3 Dec 2021 04:42:53 -0800 (PST)\nX-Smtp-Source: ABdhPJymCb004FLGPspwKq6mQcFTkQI+6hALJlF13shK+ntIeX8h0SWQqagautTMbkvoX79lLC3q\nX-Received: by 2002:a17:907:3d9e:: with SMTP id he30mr23373073ejc.177.1638535373767;\n        Fri, 03 Dec 2021 04:42:53 -0800 (PST)\nARC-Seal: i=2; a=rsa-sha256; t=1638535373; cv=pass;\n        d=example.com; s=arc-20160816;\n        b=USf28Lo91ATsFeazTbuMiNCB3WbhZEw8x9MTs4+siqtT2zySPFK6O4ToLy5cbp8WCO\n         4D4dXOj8DPnh/NIlPCMw3l5Id9cPWIQmKsIUT9/8uw95dJ4FFprAC5Jkl8aw4m9YRs/V\n         ys1g05LSCEgA7YLqTWSIJ2Ppm7W4QoZLQ8ceVPnphji9ebfScow6P4/JCz62Zvi8ctEp\n         fESH67rTdvicZdOLNUt4XtfZrgX91SFbQdJv3aJPvxqb41IJNBbTjNzK81sbcRSzkR4n\n         +gMEt+JWRR8yqJwzsdYZoBgucuF3t/02X0wiz6+EPfdgZfXxhfppc+e43eMFq6uJvPzz\n         TFCQ==\nARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20160816;\n        h=date:message-id:to:from:subject:mime-version:dkim-signature;\n        bh=j4xQykyD/StFXkM8uzUR3OggqUVrMAzwjuBrPmXIV+Q=;\n        b=QqaNZkGYgwYl8fWD+cgJj0kpQ0m+LufCCh9qEl+uEUQZQp1JNqxfdvqj96Hpi/oFAG\n         qUgY/Vn8jaw2E6opLDayuBXdLPrU7Rwe/r/+YwNlG5nKWlFAN7QBKf3FpJn/p58p8GTZ\n         mjGc5bi409SWtqYbt2Y5GrmbtGo88IrBUFBT/7WtcBoUXJplx/89w8RtHyiYVCzVU47J\n         xUYgMdcSLkEny1Nt7k6fXjACCWKABQPRunF8gRITbpTiCfGqdeVPbbVaH0ZY9khgI8QS\n         TICsIuB7nLRd/I4jKbhs38/QVP5Kc2NyGjRT8/kWHTRoHCLrmy0eLIitPBYaJzydJ31l\n         /T9g==\nARC-Authentication-Results: i=2; mx.example.com;\n       dkim=pass [email protected] header.s=zoho header.b=DXQqHBHo;\n       arc=pass (i=1 spf=pass spfdomain=example.com dkim=pass dkdomain=example.com dmarc=pass fromdomain=example.com>);\n       spf=pass (example.com: domain of [email protected] designates 31.186.226.225 as permitted sender) [email protected]\nReturn-Path: <[email protected]>\nReceived: from sender11-op-o11.zoho.eu (sender11-op-o11.zoho.eu. [31.186.226.225])\n        by mx.example.com with ESMTPS id h15si5379724ede.136.2021.12.03.04.42.53\n        for <[email protected]>\n        (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);\n        Fri, 03 Dec 2021 04:42:53 -0800 (PST)\nReceived-SPF: pass (example.com: domain of [email protected] designates 31.186.226.225 as permitted sender) client-ip=31.186.226.225;\nAuthentication-Results: mx.example.com;\n       dkim=pass [email protected] header.s=zoho header.b=DXQqHBHo;\n       arc=pass (i=1 spf=pass spfdomain=example.com dkim=pass dkdomain=example.com dmarc=pass fromdomain=example.com>);\n       spf=pass (example.com: domain of [email protected] designates 31.186.226.225 as permitted sender) [email protected]\nARC-Seal: i=1; a=rsa-sha256; t=1638535373; cv=none; \n\td=zohomail.eu; s=zohoarc; \n\tb=g14LDqq+UFBerNOKaKqcDxFLTLU3WdJgmJiBt4rSR5b1kj940eMOwkavflRdJztgi+Exc3pbDleEcPnS4W+bb7vDsJsVPnU1yOx5UsgQI1Nq+StNMNgV0bGaVegRW0G79YWOeJuGC0+YOz68cLOU7fAexOF9s1rxmuHKbqfZQVI=\nARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.eu; s=zohoarc; \n\tt=1638535373; h=Content-Type:Date:From:MIME-Version:Message-ID:Subject:To; \n\tbh=j4xQykyD/StFXkM8uzUR3OggqUVrMAzwjuBrPmXIV+Q=; \n\tb=jen7TS3hQnIbAHgUP+iqROwzOwteVov4mcNDJWln0tGSPQPa+gShAcR3hYW3kAimwzOKpHeSpm20COVg54fcgM6KRcB2AAz0mvH4j7E6KZbz7hR7JNzTqrd8KJ5ID6rfJJGGFNEsnoXysHL7TRlC7+QJceyG+IPmcbSnDhUA7l4=\nARC-Authentication-Results: i=1; mx.zohomail.eu;\n\tdkim=pass  header.i=example.com;\n\tspf=pass  [email protected];\n\tdmarc=pass header.from=<[email protected]>\nDKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; t=1638535373;\n\ts=zoho; d=example.com; [email protected];\n\th=Content-Type:MIME-Version:Subject:From:To:Message-ID:Date;\n\tbh=j4xQykyD/StFXkM8uzUR3OggqUVrMAzwjuBrPmXIV+Q=;\n\tb=DXQqHBHovFQJQDtWcmjSPIm/j6SEgTMuJ5ber2toXh87MCLo2YOczMN95BmrPndO\n\t4fq5Kf8spohIrmlVtN/Lhhut9+WmvsQO2OUkNyEl6aXL+dMbLxUzhJ9y3YRX93Jv6WB\n\t1KTlh6hECpM/CHRZF2jr100/rjAtOTrjuVkLnRxo=\nReceived: from [127.0.1.1] (vsb47.miramo.cz [93.95.33.47]) by mx.zoho.eu\n\twith SMTPS id 1638535371878118.3658187522883; Fri, 3 Dec 2021 13:42:51 +0100 (CET)\nContent-Type: multipart/alternative; boundary="===============1926786547036323748=="\nMIME-Version: 1.0\nSubject: Stuff\nFrom: [email protected]\nTo: [email protected]\nMessage-ID: <[email protected]>\nDate: Fri, 3 Dec 2021 13:42:51 +0100 (CET)\nX-ZohoMailClient: External\n\n--===============1926786547036323748==\nContent-Type: text/plain; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\n\nHello Joe\nThis is a receipt from ACME incorporated\n\nDate: 10/21/2021\n\nTotal: $128.53\n\nPayment Method\n------------------------------------------------------\nCredit Card\n\nVisa 7139\n\n\nThanks for your purchase Joe\n\nACME inc.\n\n--===============1926786547036323748==--\n"
parsed = RFC2822.parse(eml)
Mail.get_text(parsed)

Steps to reproduce

Email was created in a python script. This is nothing unusual about this. It can be done by any online service using python in a backend system. Parsing this email results in lost newline characters. I've tried to send this email with \r\n and \n newline characters but it didn't work in both cases. Since I've been using Python standard library my guess is your library has an issue.

Email content has been downloaded from gmail and thunderbird interfce. Both times it caused the same error. Email is showing correctly in gmail, thunderbird and zoho web interface.

Expected Behavior

%Mail.Message{
  body: <String with newline(\n) characters>
 ...
}

Actual Behavior

%Mail.Message{
  body: <String without newline(\n) characters>
 ...
}

newlines not in text_body

Version

0.3.1

Test Case

I've got a part that looks like this:

--64d3a96e_e3a6714_fa43
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

ok well I will

My Name
On Aug 9, 2023 at 8:57 AM -0600, Some Name <[email protected]>, wrote:
> let us know any time
>
> My Name
> On Aug 9, 2023 at 8:51 AM -0600, My Name <[email protected]>, wrote:
> > ok that sounds good
> >
> > My Name
> > On Aug 9, 2023 at 8:49 AM -0600, My Name <[email protected]>, wrote:
> > > We had a follow-up question about pricing. If we get 200 seats and then add 10 more half way through the month, is that prorated or what?
> > >
> > > My Name

--64d3a96e_e3a6714_fa43

Steps to reproduce

{:ok, rfc2822} <- Base.url_decode64(gmail_raw)
%Mail.Message{} = message <- Mail.parse(rfc2822),
Mail.get_text(message).body |> dbg()

Expected Behavior

I would expect the body of the text part to be:

ok well I will

My Name
On Aug 9, 2023 at 8:57 AM -0600, Some Name <[email protected]>, wrote:
> let us know any time
>
> My Name
> On Aug 9, 2023 at 8:51 AM -0600, My Name <[email protected]>, wrote:
> > ok that sounds good
> >
> > My Name
> > On Aug 9, 2023 at 8:49 AM -0600, My Name <[email protected]>, wrote:
> > > We had a follow-up question about pricing. If we get 200 seats and then add 10 more half way through the month, is that prorated or what?
> > >
> > > My Name

Actual Behavior

Instead, it has no newlines:

ok well I willMy NameOn Aug 9, 2023 at 8:57 AM -0600, Some Name <[email protected]>, wrote:> let us know any time>> My Name> On Aug 9, 2023 at 8:51 AM -0600, My Name <[email protected]>, wrote:> > ok that sounds good> >> > My Name> > On Aug 9, 2023 at 8:49 AM -0600, My Name <[email protected]>, wrote:> > > We had a follow-up question about pricing. If we get 200 seats and then add 10 more half way through the month, is that prorated or what?> > >> > > My Name

Subject not properly encoded to printable character

Version

0.3.1

Test Case

Subject header:

Subject: =?UTF-8?Q?Read_the_latest_posts_-_"Hyperdrive:_ma?= =?UTF-8?Q?king_databases_feel_like_they=E2=80=99re_global"?=

Expected Behavior

Subject parsed as:

"subject" => "Read the latest posts - \"Hyperdrive: making databases feel like they’re global\""

Actual Behavior

Mail parses the subject as:

"subject" => "Read_the_latest_posts_-_\"Hyperdrive:_ma king_databases_feel_like_they’re_global\"",

UTF-8 in subject

The library does not encode UTF-8 characters in the (subject) header. This test fails:

  test "subject" do
    import Mail
    subject = "test üä test"
    txt = Mail.build()
      |> put_subject(subject)
      |> render()
    encoded_subject = "=?UTF-8?Q?" <> Mail.Encoders.QuotedPrintable.encode(subject) <> "?="

    assert String.contains?(txt, encoded_subject)
  end

Parse errors

Version

0.2.0

Test Case

File.read!("/tmp/815.txt") |> Mail.Parsers.RFC2822.parse

Steps to reproduce

Download examples (there are three) from here:
https://gist.github.com/cjbottaro/62e8a4e037d015c2996259ff56888256

Then run code above.

Expected Behavior

To not error.

Actual Behavior

** (MatchError) no match of right hand side value: ["by filter0844p1mdw1.sendgrid.net with SMTP id filter0844p1mdw1-23096-5AF1DA68-1E        2018-05-08 17:12:08.410314999 +0000 UTC"]
    (mail) lib/mail/parsers/rfc_2822.ex:165: Mail.Parsers.RFC2822.parse_received_value/1
    (mail) lib/mail/parsers/rfc_2822.ex:107: Mail.Parsers.RFC2822.parse_headers/2
    (mail) lib/mail/parsers/rfc_2822.ex:19: Mail.Parsers.RFC2822.parse/1

Thank for the help!

Provide typespecs

Would be nice to have typespecs on this project. I'd specifically like to see something on Mail.Message. Would you accept a pull request for this?

String header keys instead of atom keys for sub-types

Atoms are not GC'd, and inbound emails being parsed could cause a DOS attack on an elixir app by hammering with infinitely unique header keys.

However, the current data model using keyword lists for the header values. Keyword values map very nicely to header values and subtypes:

Content-Type: multipart/mixed; boundary="foobar"

maps well to:

%{content_type: ["multipart/mixed", boundary: "foobar"]}

However, using string keys:

headers = %{"Content-Type" => ["multipart/mixed", {"boundary", "foobar"}]}

It seems that the boundary subtype value cannot be fetched:

iex(4)> Keyword.fetch(headers["Content-Type"], "boundary")
** (FunctionClauseError) no function clause matching in Keyword.fetch/2
    (elixir) lib/keyword.ex:288: Keyword.fetch(["multipart/mixed", {"boundary", "foobar"}], "boundary")
iex(4)>

Have body be anything that uses String.Chars

In order to make things as flexible as possible, have the body of a Mail message be anything that implements String.Chars. This lets each message type know how to encode itself, maximizing future extensibility.

multi-part bodies

Currently the body is a map field on the Mail struct. Each "part" of the body is intended to be associated with a key. After some consideration I believe this is a poor design approach. An email can have any number of "parts" which means that the body should really be a list. Each member in the list should be a part.

I believe a good direction here is the following api:

Mail.put_part(mail, part)
Mal.delete_part(mail, part)
Mail.render_body(mail)
Mail.put_attachment(mail, file)

Removing the body field and replacing with a parts field that is a list. New parts are appended onto the list. The parts themselves should be their own struct:

defmodule Mail.Part do
  defstruct data: nil
            headers: %{}
end

This would allow for any part types to be added.

getter functions?

Currently to get most of the data you have to go through the headers on the struct:

mail = put_subject(%Mail{}, "Spoooon!")

assert mail.headers.subject == "Spoooon!"

this might be annoying, and potentially problematic for certain headers. For example, the recipient headers like to can hold a list, but the value defaults to nil. In most cases you have to wrap the header value to guarantee a list:

List.wrap(mail.headers[:to])

It might be more convenient to have certain getter functions:

mail = put_to(%Mail{}, "[email protected]")
assert Mail.to(mail) == ["[email protected]"]

assert Mail.bcc(mail) == []

I am unsure if the functions should be Mail.get_to/1 or just Mail.to/1

Encoders needed

Elixir comes with a Base64 encoder, but to be compliant with the spec we need the following:

Incorrect Test Assertion

While I was working on #116, I noticed the following test:

test "content-type with implicit charset" do
message =
parse_email("""
Content-Type: text/html; us-ascii
""")
assert message.headers["content-type"] == ["text/html", "us-ascii"]
end

If I understand this test correctly, it is meant to test that if you omit the charset= parameter prefix that we will still parse the charset. Thus, shouldn't the assertion statement look like this?

assert message.headers["content-type"] == ["text/html", {"charset", "us-ascii"}]

Recipient value parsing doesn't parse names where content of name is an email address

Version

v0.2.1

Test Case

Mail.Parsers.RFC2822.parse_recipient_value("\"[email protected]\" <[email protected]>")

The return value is ["[email protected]\"", "[email protected]"]
While it should have been [{"[email protected]", "[email protected]"}] since, yes this is terrible, the "name" for that person is the email address.
This is from a real email (I did alter the address) so it's a real issue.

Steps to reproduce

Invoke

Mail.Parsers.RFC2822.parse_recipient_value("\"[email protected]\" <[email protected]>")

Inside an IEx session

Expected Behavior

I'd expect "\"[email protected]\" <[email protected]>" to be parsed as a name + email address, not as 2 email addresses.
A list of two elements (two strings) was returned, while the expected behavior is that a list of one element (a tuple of name+email address) is returned

Actual Behavior

Two emails were found (a list of two elements is returned) ["[email protected]\"", "[email protected]"]

An empty header field causes an infinite loop on render

Putting an empty or nil value into a header will cause an infinite loop in the render_header_value
Since there isn't a function for handling empty input.

Version
Commit 99facb9

Steps to Reproduce

mail =
  Mail.build
  |> Mail.Message.put_header("x-mms-message-id", nil)
  |> Mail.render

Test Case
What test case? This thing just loops like nobody's business, you figure it out.

Expected Behaviour
Doesn't infinitely loop.
Jokes aside, it should either raise an error, or discard the header.

Actual Behaviour
It loops.

Fails to parse multipart message due to Content-Type header case sensitivity

Version

0.3.1

Test Case

  test "parses multipart emails with case insensitive Content-Type header" do
    raw =
      """
      Content-type: multipart/alternative; boundary=banana
      Mime-Version: 1.0

      --banana
      Content-Type: text/plain; charset=utf-8

      Hi

      --banana--
      """

    message =
      raw
      |> String.replace("\n", "\r\n")
      |> Mail.Parsers.RFC2822.parse()

    refute message.parts == [] # <-- I'd expect this to have one plain text part
  end

Expected Behavior

I can parse a multipart email whose content type header's case is Content-type.

Actual Behavior

I can't. Only emails where the header case is exactly Content-Type will be parsed.

Email headers aren't required to be case sensitive, and indeed I've observed mail servers in the wild sending messages with headers like Content-type instead of Content-Type. When this happens, elixir-mail fails to parse the message as multipart.

This relates to #148

7bit encoded email with line break

Hello. Maybe a dumb question but I can't see how the lib can successfully parse a 7bit encoded email that contain line breaks. By successfully I mean without losing line breaks.

Version

mail 0.2.3
Erlang/OTP 24 [erts-12.3.2.6] [source] [64-bit] [smp:5:5] [ds:5:5:10] [async-threads:1]
Elixir 1.14.0 (compiled with Erlang/OTP 24)

Test Case

Using the parse_email function defined in parser test:

parse_email("""
    To: [email protected]
    From: [email protected]
    Subject: Test Email
    Content-Transfer-Encoding: 7bit

    This is the body!
    It has more than one line
    """)

Steps to reproduce

Run the above code

Expected Behavior

The returned body should have some sort of line breaks:
This is the body!\nIt has more than one line

Actual Behavior

The returned body no longer have line breaks:
This is the body!It has more than one line

Reading at the code, I see that the lib joins the body lines using \r\n but the SevenBit parser called just after drops them. Am I missing something ?

Joining the body lines with \n instead of \r\n seems to fix the issue.

Thanks in advance

PS: I saw the previous issue on the matter but could not find an answer there so I allowed myself to repost a new issue.

CC parsing presumes the header is capitalized as "CC", though being defined as "Cc" is at least somewhat of an occurence.

Version

0.2.3

Test Case

test "parses a multipart message" do
  message =
    parse_email("""
    To: Test User <[email protected]>, Other User <[email protected]>
    Cc: The Dude <[email protected]>, Batman <[email protected]>
    From: Me <[email protected]>
    Reply-To: OtherMe <[email protected]>
    Subject: Test email
    Mime-Version: 1.0
    Content-Type: multipart/alternative; boundary=foobar

    --foobar
    Content-Type: text/plain

    This is some text

    --foobar
    Content-Type: text/html

    <h1>This is some HTML</h1>
    --foobar--
    """)

  assert message.headers["cc"] == [
           {"The Dude", "[email protected]"},
           {"Batman", "[email protected]"}
         ]
end

This test will fail, it is a trimmed down version of the test defined at test/mail/parsers/rfc_2822_test.exs:42 - the only functional difference is that line 46 CC: The Dude <[email protected]>, Batman <[email protected]> is that the header is defined as "Cc" as opposed to "CC"

Steps to reproduce

See above and run that test - not entirely sure what the spec says about this but email from gmail routed through AWS SES -> SNS -> S3 has the header defined as Cc.

Expected Behavior

Should be able to parse the cc header properly.

Actual Behavior

Cannot properly parse cc recipients if the header is defined with "Cc"

Mail.put_text can delete plain text attachments

Currently Mail.put_text/2 tries to replace a plain text part, but doesn't differentiate between normal message parts and attachments.

Version

0.2.1

Steps to reproduce

in the following snippet the first attachment will be replaced with the text from the put_text-statement.

Mail.build_multipart()
|> Mail.put_attachment({"Attachment.txt", "I am the first attachment"})
|> Mail.put_attachment({"Attachment2.txt", "I am the second attachment"})
|> Mail.put_text("I am the message body")

Expected Behavior

Both attachments remain untouched and the text from the put_text-statement is added.

Actual Behavior

The first attachment is replaced with the text from the put_text-statement.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.