Giter Site home page Giter Site logo

mail-builder's Introduction

mail-builder

crates.io build docs.rs crates.io

mail-builder is a flexible e-mail builder library written in Rust. It includes the following features:

  • Generates e-mail messages conforming to the Internet Message Format standard (RFC 5322).
  • Full MIME support (RFC 2045 - 2049) with automatic selection of the most optimal encoding for each message body part.
  • Fast Base64 encoding based on Chromium's decoder (the fastest non-SIMD encoder).
  • Minimal dependencies.

Please note that this library does not support sending or parsing e-mail messages as these functionalities are provided by the crates mail-send and mail-parser.

Usage Example

Build a simple e-mail message with a text body and one attachment:

    // Build a simple text message with a single attachment
    let eml = MessageBuilder::new()
        .from(("John Doe", "[email protected]"))
        .to("[email protected]")
        .subject("Hello, world!")
        .text_body("Message contents go here.")
        .attachment("image/png", "image.png", [1, 2, 3, 4].as_ref())
        .write_to_string()
        .unwrap();
        
    // Print raw message
    println!("{}", eml);

More complex messages with grouped addresses, inline parts and multipart/alternative sections can also be easily built:

    // Build a multipart message with text and HTML bodies,
    // inline parts and attachments.
    MessageBuilder::new()
        .from(("John Doe", "[email protected]"))

        // To recipients
        .to(vec![
            ("Antoine de Saint-Exupéry", "[email protected]"),
            ("안녕하세요 세계", "[email protected]"),
            ("Xin chào", "[email protected]"),
        ])

        // BCC recipients using grouped addresses
        .bcc(vec![
            (
                "My Group",
                vec![
                    ("ASCII name", "[email protected]"),
                    ("ハロー・ワールド", "[email protected]"),
                    ("áéíóú", "[email protected]"),
                    ("Γειά σου Κόσμε", "[email protected]"),
                ],
            ),
            (
                "Another Group",
                vec![
                    ("שלום עולם", "[email protected]"),
                    ("ñandú come ñoquis", "[email protected]"),
                    ("Recipient", "[email protected]"),
                ],
            ),
        ])

        // Set RFC and custom headers
        .subject("Testing multipart messages") 
        .in_reply_to(vec!["message-id-1", "message-id-2"])
        .header("List-Archive", URL::new("http://example.com/archive"))

        // Set HTML and plain text bodies
        .text_body("This is the text body!\n") 
        .html_body("<p>HTML body with <img src=\"cid:my-image\"/>!</p>") 

        // Include an embedded image as an inline part
        .inline("image/png", "cid:my-image", [0, 1, 2, 3, 4, 5].as_ref())
        .attachment("text/plain", "my fíle.txt", "Attachment contents go here.") 

        // Add text and binary attachments
        .attachment(
            "text/plain",
            "ハロー・ワールド",
            b"Binary contents go here.".as_ref(),
        )

        // Write the message to a file
        .write_to(File::create("message.eml").unwrap())
        .unwrap();

Nested MIME body structures can be created using the body method:

    // Build a nested multipart message
    MessageBuilder::new()
        .from(Address::new_address("John Doe".into(), "[email protected]"))
        .to(Address::new_address("Jane Doe".into(), "[email protected]"))
        .subject("Nested multipart message")

        // Define the nested MIME body structure
        .body(MimePart::new(
            "multipart/mixed",
            vec![
                MimePart::new("text/plain", "Part A contents go here...").inline(),
                MimePart::new(
                    "multipart/mixed",
                    vec![
                        MimePart::new(
                            "multipart/alternative",
                            vec![
                                MimePart::new(
                                    "multipart/mixed",
                                    vec![
                                        MimePart::new("text/plain", "Part B contents go here...").inline(),
                                        MimePart::new(
                                            "image/jpeg",
                                            "Part C contents go here...".as_bytes(),
                                        )
                                        .inline(),
                                        MimePart::new("text/plain", "Part D contents go here...").inline(),
                                    ],
                                ),
                                MimePart::new(
                                    "multipart/related",
                                    vec![
                                        MimePart::new("text/html", "Part E contents go here...").inline(),
                                        MimePart::new(
                                            "image/jpeg",
                                            "Part F contents go here...".as_bytes(),
                                        ),
                                    ],
                                ),
                            ],
                        ),
                        MimePart::new("image/jpeg", "Part G contents go here...".as_bytes())
                            .attachment("image_G.jpg"),
                        MimePart::new(
                            "application/x-excel",
                            "Part H contents go here...".as_bytes(),
                        ),
                        MimePart::new(
                            "x-message/rfc822",
                            "Part J contents go here...".as_bytes(),
                        ),
                    ],
                ),
                MimePart::new("text/plain", "Part K contents go here...").inline(),
            ],
        ))
        
        // Write the message to a file
        .write_to(File::create("nested-message.eml").unwrap())
        .unwrap();

Testing

To run the testsuite:

 $ cargo test --all-features

or, to run the testsuite with MIRI:

 $ cargo +nightly miri test --all-features

License

Licensed under either of

at your option.

Copyright

Copyright (C) 2020-2022, Stalwart Labs Ltd.

See COPYING for the license.

mail-builder's People

Contributors

mdecimus avatar mucinoab 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

mail-builder's Issues

Build email from parsed email

Is there a convenient way to use this crate in conjunction with your other crate (mail-parser) to

  1. read in an email,
  2. convert the Message to a MessageBuilder,
  3. make some modifications to the email
  4. then send it (using mail-send or, in my case, a third party library)

Current best solution I have is to copy over field by field. I'm hoping there's a built in way.... '

The use case for this is I'm building a marketing engine. The user sends an email to my app, the app reads the email and adds unsubscribe links, edits content data, etc.

Add ability to customize the Content-Transfer-Encoding header

When creating encrypted multipart, the 2 inner parts application/pgp-encrypted and application/octet-stream should not contain anything else. From the RFC:

The multipart/encrypted MIME body MUST consist of exactly two body
parts, the first with content type "application/pgp-encrypted". This
body contains the control information. A message complying with this
standard MUST contain a "Version: 1" field in this body. Since the
OpenPGP packet format contains all other information necessary for
decrypting, no other information is required here.

The second MIME body part MUST contain the actual encrypted data. It
MUST be labeled with a content type of "application/octet-stream".

The first inner part that should contain Version: 1 ends up being base64 encoded:

Content-Type: application/pgp-encrypted
Content-Transfer-Encoding: base64

VmVyc2lvbjogMQ==

Instead of:

Content-Type: application/pgp-encrypted

Version: 1

I guess adding a condition on encrypted (and signed) parts could be enough. Or adding the ability to override (or remove) the header.

Binary `text/plain` attachment considered as `PartType::Text` instead of `PartType::Binary`

Given the following builder:

let msg = MessageBuilder::new()
    .from("from@localhost")
    .to("to@localhost")
    .subject("subject")
    .text_body("Hello, world!")
    .binary_attachment("text/plain", "attachment.txt", "Hello, world!".as_bytes())
    .write_to_string()
    .unwrap();

Which compiles to:

From: <from@localhost>
To: <to@localhost>
Subject: subject
Message-ID: <176218bdb01133ec.2d80ee539da5279a.52ee893e4d3a9db2@soywod>
Date: Wed, 24 May 2023 13:53:33 +0000
Content-Type: multipart/mixed; 
	boundary="176218bdb01183df_cbb8680d1cefa3af_52ee893e4d3a9db2"


--176218bdb01183df_cbb8680d1cefa3af_52ee893e4d3a9db2
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit

Hello, world!
--176218bdb01183df_cbb8680d1cefa3af_52ee893e4d3a9db2
Content-Type: text/plain
Content-Disposition: attachment; filename="attachment.txt"
Content-Transfer-Encoding: 7bit

Hello, world!
--176218bdb01183df_cbb8680d1cefa3af_52ee893e4d3a9db2--

If I parse back this email with mail-parser, the body of the attachment is considered a PartType::Text. In my opinion it should be a PartType::Binary as it was intended with the builder.

[Request] New version

Hello @mdecimus.

First of all thank you for your work on this library.

I was trying to use this lib at the 0.1.2 version but i found too the bug you fixed here. Do you think that a new version of this lib could be available soon?

Thank you in advance.

Sending email - how?

Hi,
could you give example of what smtp client is the best for your lib to sending email?

Addition of BodyPart::Raw

I'd like to be able to write a custom encoding type, as allowed for by RFC 2045 p.15.

This body type would operate exactly like BodyType::Text/Binary, but not add any Content-Transfer-Encoding headers, and directly write a raw [u8] to the output.

Incorrect new lines in the header

I'm parsing an email with mail-parse and building a new mail that has truncated body content. What I would like to todo is reuse original headers.

========================================
Return-Path: Text("[email protected]")
Delivered-To: Text("[email protected]")
Received: Text("from 139-162-244-188.cprapid.com\n\tby 139-162-244-188.cprapid.com with LMTP\n\tid RQb5Ojfdd2O5LikAqCNTnA\n\t(envelope-from <[email protected]>)\n\tfor <[email protected]>; Fri, 18 Nov 2022 19:29:59 +0000")
Return-Path: Text("[email protected]")
Envelope-to: Text("[email protected]")
Delivery-date: Text("Fri, 18 Nov 2022 19:30:00 +0000")
Received: Text("from pajkovsky by 139-162-244-188.cprapid.com with local (Exim 4.95)\n\t(envelope-from <[email protected]>)\n\tid 1ov8pO-00A1jF-Dg\n\tfor [email protected];\n\tWed, 16 Nov 2022 03:11:06 +0000")
To: Address(Addr { name: None, address: Some("[email protected]") })
Subject: Text("[My Company] Your site has updated to WordPress 6.1.1")
X-PHP-Script: Text("cpanel.pajkovsky.io/wp-cron.php for 139.162.244.188\n X-PHP-Originating-Script: 1001:PHPMailer.php")
Date: DateTime(DateTime { year: 2022, month: 11, day: 16, hour: 3, minute: 11, second: 6, tz_before_gmt: false, tz_hour: 0, tz_minute: 0 })
From: Address(Addr { name: Some("WordPress"), address: Some("[email protected]") })
Message-ID: Text("[email protected]")
X-Mailer: Text("PHPMailer 6.6.5 (https://github.com/PHPMailer/PHPMailer)")
MIME-Version: Text("1.0")
Content-Type: ContentType(ContentType { c_type: "text", c_subtype: Some("plain"), attributes: Some([("charset", "UTF-8")]) })
[src/main.rs:33] &v = "from 139-162-244-188.cprapid.com\n\tby 139-162-244-188.cprapid.com with LMTP\n\tid RQb5Ojfdd2O5LikAqCNTnA\n\t(envelope-from <[email protected]>)\n\tfor <[email protected]>; Fri, 18 Nov 2022 19:29:59 +0000"
[src/main.rs:33] &v = "from pajkovsky by 139-162-244-188.cprapid.com with local (Exim 4.95)\n\t(envelope-from <[email protected]>)\n\tid 1ov8pO-00A1jF-Dg\n\tfor [email protected];\n\tWed, 16 Nov 2022 03:11:06 +0000"
========================================
Return-Path: [email protected]
Delivered-To: [email protected]
Received: from 139-162-244-188.cprapid.com
        by 139-162-244-188.cprapid.com with
         LMTP
        id RQb5Ojfdd2O5LikAqCNTnA
        (envelope-from <[email protected]>)

        for <[email protected]>; Fri, 18 Nov 2022 19:29:59 +0000
Return-Path: [email protected]
Envelope-to: [email protected]
Delivery-date: Fri, 18 Nov 2022 19:30:00 +0000
Received: from pajkovsky by 139-162-244-188.cprapid.com with local (Exim 4.95)

        (envelope-from <[email protected]>)
        id 1ov8pO-00A1jF-Dg

        for [email protected];
        Wed, 16 Nov 2022 03:11:06 +0000
To: <[email protected]>

But I'm seeing Received has an unexpected new line in the header. I am not convinced that's correct.

Custom Content-Transfer-Encoding leads to duplicated header

Let's say I have a MimePart. If I do sth like:

part.header("Content-Transfer-Encoding", Raw::new("8bit"));

then the final generated MIME message contains twice the Content-Transfer-Encoding header. What could be the best way to solve this matter?

binary_inline should generate a multipart/related container

Hi! I think this might be two bugs, one here with generation and one in mail-parser in understanding the result.

When using binary_inline to define an inline attachment, the html part and the attachment that is being referenced should belong in the same multipart/related container in order to be referenced correctly. https://stackoverflow.com/a/30424938/149111 has some practical examples.

That container is missing from the built message.

In addition, when parsing the result back with the mail-parser crate, it believes that there are multiple text and html parts.

You can see this if you modify one of the existing test cases:

diff --git a/src/lib.rs b/src/lib.rs
index a7a93e2..eaa9a45 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -613,6 +613,7 @@ mod tests {
             )
             .write_to_vec()
             .unwrap();
-        Message::parse(&output).unwrap();
+        let parsed = Message::parse(&output).unwrap();
+        panic!("{:#?}", parsed);
     }
 }

the top of the struct shows multiple parts for text and html:

---- tests::build_message stdout ----
thread 'tests::build_message' panicked at 'Message {
    html_body: [
        3,
        4,
    ],
    text_body: [
        2,
        4,
    ],
    attachments: [
        4,
        5,
        6,
    ],
    parts: [

it's interesting to see that both the html and text body list 4 as an additional part, which is also listed under attachments; I think that is probably the binary_inline part; it's the first attachment defined in the test and shows up first in the attachments array.

`BodyPart::Binary` and `BodyPart::Text` ignores existing `Content-Transfer-Encoding` header

let builder = MessageBuilder::new()
    .body(MimePart { 
        headers: vec![
            (
                Cow::Borrowed("Content-Transfer-Encoding"), Text {
                    text: Cow::Borrowed("7bit") 
                }.into()
            )
        ], 
        contents: BodyPart::Binary(Cow::Borrowed(b"Hello"))
    });
println!("{}", builder.write_to_string()?);

The above code produces:

Message-ID: <175a1d6d39c6b774.dd9fc513b5d45e31.fed9a95d3f7174ad@FA-0455>
Date: Fri, 28 Apr 2023 13:49:25 +0000
Content-Transfer-Encoding: 7bit
Content-Transfer-Encoding: base64

SGVsbG8=

Note that the "Content-Transfer-Encoding" header has been duplicated. Also note that the content has been encoded in base64, and there is no way to encode it in anything other than base64, when using non text content types.

I would expect it to produce:

Message-ID: <175a1d6d39c6b774.dd9fc513b5d45e31.fed9a95d3f7174ad@FA-0455>
Date: Fri, 28 Apr 2023 13:49:25 +0000
Content-Transfer-Encoding: 7bit

Hello

The same issue (but reversed) occurs when using BodyPart::Text

support format=flowed?

  1. I'm not sure whether this library should assume that a trailing space means that it should use quoted-printable instead of utf-7 -- what's the reasoning behind this?
  2. It would be nice to have a way to set format=flowed -- I put together some commits on https://github.com/alexwennerberg/mail-builder but I'm not sure if it's the best way to do it or something you want to support in this library

Multipart message has double line break after header

When building a multipart message, I receive two line breaks after the header. Possibly related, there are also two line breaks at the end of the message.

Here is what the output looks like:

Message-ID: <175a169b74fdbff2.dd9fc513b5d45e31.fed9a95d3f7174ad@FA-0455>
Date: Fri, 28 Apr 2023 11:44:27 +0000
Content-Type: multipart/mixed; 
	boundary="175a169b74fe1006_7bd73ecd351eda46_fed9a95d3f7174ad"


--175a169b74fe1006_7bd73ecd351eda46_fed9a95d3f7174ad
Content-Transfer-Encoding: 7bit

Hello
--175a169b74fe1006_7bd73ecd351eda46_fed9a95d3f7174ad


This is the code to build the above message:

let builder = MessageBuilder::new()
  .body(MimePart { 
      contents: BodyPart::Multipart(vec![
          MimePart { 
              contents: BodyPart::Text(Cow::Borrowed("Hello")), 
              headers: vec![] }
      ]), 
      headers: vec![]
  });

println!("{}", builder.write_to_string()?);

Support for multipart/signed & multipart/encrypted

I am working on a PGP compliant email application using this library and I've noticed however that adding a content_type to the headers of a message_builder such that

message_builder.header("Content-Type", "multipart/encrypted; protocol=\"application/pgp-encrypted\"")
(See https://datatracker.ietf.org/doc/html/rfc3156#section-4)
Doesn't do anything, that is because when the message_builder gets transformed into a string via write the function fn write_to(self, mut output: impl Write) -> io::Result<()> which after adding date and message-id fields, will proceed to. call fn write_body(self, output: impl Write) -> io::Result<()> which, before writing does set the Content-Type depending on the contents, which becomes an issue as for PGP messages it is required to have the correct multipart (see https://datatracker.ietf.org/doc/html/rfc3156#section-4 & https://datatracker.ietf.org/doc/html/rfc1847#section-2).

I however, see that Stalwartlabs has a PGP compliant email application (see https://stalw.art/) so maybe I am misusing the library and this is a non-issue. If not, and mail-builder indeed does not handle custom content-types, could it be possible to make a PR for it with a possible solution or the project isn't open to them?

Apologies if this isn't the proper way to ask this sort of questions/issues, it is my first time working with open source projects.

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.