Giter Club home page Giter Club logo

mail-send's Introduction

mail-send

crates.io build docs.rs crates.io

mail-send is a Rust library to build, sign and send e-mail messages via SMTP. 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.
  • DomainKeys Identified Mail (DKIM) Signatures (RFC 6376) with ED25519-SHA256, RSA-SHA256 and RSA-SHA1 support.
  • Simple Mail Transfer Protocol (SMTP; RFC 5321) delivery.
  • SMTP Service Extension for Secure SMTP over TLS (RFC 3207).
  • SMTP Service Extension for Authentication (RFC 4954) with automatic mechanism negotiation (from most secure to least secure):
    • CRAM-MD5 (RFC 2195)
    • DIGEST-MD5 (RFC 2831; obsolete but still supported)
    • XOAUTH2 (Google proprietary)
    • LOGIN
    • PLAIN
  • Full async (requires Tokio).

Usage Example

Send a message via an SMTP server that requires authentication:

    // Build a simple multipart message
    let message = MessageBuilder::new()
        .from(("John Doe", "[email protected]"))
        .to(vec![
            ("Jane Doe", "[email protected]"),
            ("James Smith", "[email protected]"),
        ])
        .subject("Hi!")
        .html_body("<h1>Hello, world!</h1>")
        .text_body("Hello world!");

    // Connect to the SMTP submissions port, upgrade to TLS and
    // authenticate using the provided credentials.
    SmtpClientBuilder::new("smtp.gmail.com", 587)
        .implicit_tls(false)
        .credentials(("john", "p4ssw0rd"))
        .connect()
        .await
        .unwrap()
        .send(message)
        .await
        .unwrap();

Sign a message with DKIM and send it via an SMTP relay server:

    // Build a simple text message with a single attachment
    let message = MessageBuilder::new()
        .from(("John Doe", "[email protected]"))
        .to("[email protected]")
        .subject("Howdy!")
        .text_body("These pretzels are making me thirsty.")
        .attachment("image/png", "pretzels.png", [1, 2, 3, 4].as_ref());

    // Sign an e-mail message using RSA-SHA256
    let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(TEST_KEY).unwrap();
    let signer = DkimSigner::from_key(pk_rsa)
        .domain("example.com")
        .selector("default")
        .headers(["From", "To", "Subject"])
        .expiration(60 * 60 * 7); // Number of seconds before this signature expires (optional)

    // Connect to an SMTP relay server over TLS.
    // Signs each message with the configured DKIM signer.
    SmtpClientBuilder::new("smtp.gmail.com", 465)
        .connect()
        .await
        .unwrap()
        .send_signed(message, &signer)
        .await
        .unwrap();

More examples of how to build messages are available in the mail-builder crate. Please note that this library does not support parsing e-mail messages as this functionality is provided separately by the mail-parser crate.

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.

mail-send's People

Contributors

janis-ab avatar leoniephiline avatar mdecimus avatar soywod 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

mail-send's Issues

Elastic template

I want to send a message containing an email in a template, but I don't want to call SmtpClient Builder::new every time for each email.
Maybe you know how to make it more elastic?

let template = format!("Dear {email}, You are now change your email.");
let emails = vec![...];
let message = MessageBuilder::new()
            .from((username, email))
            .to(emails)
            .subject("Subject")
            .text_body(&template);
 
 SmtpClientBuilder::new(self.hostname.clone(), self.port)
            .implicit_tls(false)
            .credentials((self.from.clone(), self.password.clone()))
            .connect()
            .await
            .expect("connect to send email")
            .send(message)
            .await
            .unwrap();

As a solution maybe need implement .send_many(message) where we can create message for every individual user?

Add an `impl std::error::Error` for `mail_send::Error`

Hello!

I've been using thiserror and stumbled upon this when trying to integrate mail_send:

mail_send::Error (and also mail_auth::Error) do not implement the std::error::Error trait. That's why they cannot be simply integrated into other error-frameworks which all build on the std lib's Error trait.

mail_send and mail_auth could also opt into using the thiserror crate.
thiserror::Error is entirely transparent to the library user, and takes some boilerplate away from you.

Here's how this could look like: https://github.com/jolimail/mrml-core/pull/38/files#diff-c08afca406e15cd665ef6c443014a8472bb79435231a21463239333886aed304

A pure std::error::Error implementation, without #[derive(thiserror::Error)] might look like this: https://docs.rs/askama_shared/0.12.2/src/askama_shared/error.rs.html#43-54

Disable SSL/TLS

Is there a way to totally disable SSL/TLS? I am replacing lettre by mail-send, and I don't see how to do it. The equivalent with lettre is:

SmtpTransport::relay(&config.host)?.tls(Tls::None);

Fails to compile (Even in hello world)

Adding this crate to even cargo's default hello world program causes it to fail to compile:

error[E0599]: &HeaderType<'_> is not an iterator
--> /home/westly/.cargo/registry/src/github.com-1ecc6299db9ec823/mail-send-0.2.0/src/smtp/message.rs:160:91
|
160 | if let Some(HeaderType::Address(address::Address::Address(addr))) = value.last() {
| ^^^^ &HeaderType<'_> is not an iterator
|
::: /home/westly/.cargo/registry/src/github.com-1ecc6299db9ec823/mail-builder-0.2.1/src/headers/mod.rs:32:1
|
32 | pub enum HeaderType<'x> {
| ----------------------- doesn't satisfy HeaderType<'_>: Iterator
|
= note: the following trait bounds were not satisfied:
&HeaderType<'_>: Iterator
which is required by &mut &HeaderType<'_>: Iterator
HeaderType<'_>: Iterator
which is required by &mut HeaderType<'_>: Iterator

error[E0277]: &HeaderType<'_> is not an iterator
--> /home/westly/.cargo/registry/src/github.com-1ecc6299db9ec823/mail-send-0.2.0/src/smtp/message.rs:170:29
|
170 | for addr in value {
| ^^^^^ &HeaderType<'_> is not an iterator
|
= help: the trait Iterator is not implemented for &HeaderType<'_>
= note: required because of the requirements on the impl of IntoIterator for &HeaderType<'_>

Some errors have detailed explanations: E0277, E0599.
For more information about an error, try rustc --explain E0277.
error: could not compile mail-send due to 2 previous errors
warning: build failed, waiting for other jobs to finish...

Make the API more flexible and document parameters.

Trying to move away from lettre in favor of an actively maintained crate. I immediately noticed that there's no documentation for the API and there's a lot of implementations missing that one would expect to be present such as impl From<EmailAddress> for Address.
Great job so far though, wish I had the time to offer a helping hand.

โœจ Feature request: OpenPGP encryption support

Introduction

Hi! I love this project idea and really appreciate the dedication to modern security standards (such as DKIM support) as well as mail-send's RFC compliance. The current feature set would serve as a fantastic base for a security-oriented Rust-written mail client if that is something Stalwart Labs would be interested in pursuing.

The final component of mail-send that would make it a serious security contender (in terms of client features) to the likes of ProtonMail and Tutanota would be support for OpenPGP, the open standard for email encryption.

Proposal

Standards

mail-send could implement OpenPGP support by following these Internet Message Format standards:

  • RFC 4880 OpenPGP Message Format
  • RFC 3156 MIME* Security with OpenPGP

There are two strong libraries that have implemented RFC 4880: rpgp and sequoia.

*Comparison of OpenPGP vs. PGP/MIME and a table of major email client support for them

Implementation

The loading of a public PGP key could be done in a similar syntax to the implementation of DKIM in this crate:

// Set up DKIM signer
let pgp_key = PGP::from_pkcs1_asc_file("./key.asc")
    .unwrap()

^ I'm not very familiar with PGP so not sure if any other inputs besides the file path should be provided

// Build a encrypted text message with a single attachment
let message = MessageBuilder::new()
    .from(("John Doe", "[email protected]"))
    .to("[email protected]")
    .subject("Howdy!")
    .text_body("These pretzels are making me thirsty.")
    .binary_attachment("image/png", "pretzels.png", [1, 2, 3, 4].as_ref())
    .pgp_encrypt(pgp_key);

Interested to hear your thoughts! Let me know if this issue would be better suited at mail-builder.

Feature request: capture message queue ID

It'd be useful if SmtpClient.send() returned the reported queue ID for that particular email, to help diagnose delivery problems by identifying a particular send of an email.

DATA
354 End data with <CR><LF>.<CR><LF>
email body here
.
250 2.0.0 Ok: queued as 224B824BD40

Integration with mail-parser

Hi, it's me again

What I'm trying to build requires to store mails on disk, before reading them back and sending them.

Storing them on disk using mail-parser is easy thanks to serde. An alternative could be to store them raw which is trivial too.

However, sending them through mail-send seems to be a bit of a pain point. Indeed, the IntoMessage trait that Transport.send expects is implemented for mail-parser's Message and mail-builder MessageBuilder.

However, neither mail-parser::Message nor mail-builder::MessageBuilder support building the structure from the raw text or through Deserialize.

I guess implementing IntoMessage for mail-parser::Message could work, but it feels like both mail-parser::Message and mail-send::smtp::message::Message could be the same structure, that would implement Serialize/Deserialize?

Thanks!

`UnparseableReply`: Bug in `impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T>`. (Fixed by #13)

Hi!

I am experiencing some weird SMTP failures.

I have a mailhog (https://github.com/mailhog/MailHog / https://registry.hub.docker.com/r/mailhog/mailhog/) set up behind a traefik (https://registry.hub.docker.com/_/traefik) reverse proxy. The entire thing is happening within a docker environment on my local machine - no remote network failures.

The setup is simple: MailHog just exposes port 1025 for plain SMTP; Traefik does the TLS and exposes port 587. (TLS is self-signed; using allow_invalid_certs().)

The Traefik config for the TLS & TCP setup is basically the same as the one shown here: https://docker-mailserver.github.io/docker-mailserver/edge/examples/tutorials/mailserver-behind-proxy/#configuration-of-the-used-proxy-software

Most of the time, I can send mail just fine.
However, every now and then I receive an UnparseableReply error.

I can reproduce it by just running the test again and again. It does not matter if I run it in quick succession or wait for a second or more before retrying.

In these cases, mailhog logs:

Starting session
[PROTO: INVALID] Started session, switching to ESTABLISH state
Sent 45 bytes: '220 mail.some-host.berlin ESMTP MailHog\r\n'
Received 15 bytes: 'EHLO felicity\r\n'
[PROTO: ESTABLISH] Processing line: EHLO felicity
[PROTO: ESTABLISH] In state 1, got command 'EHLO', args 'felicity'
[PROTO: ESTABLISH] In ESTABLISH state
[PROTO: ESTABLISH] Got EHLO command, switching to MAIL state
Sent 20 bytes: '250-Hello felicity\r\n'
Sent 16 bytes: '250-PIPELINING\r\n'
Sent 16 bytes: '250 AUTH PLAIN\r\n'
Connection closed by remote host
Session ended

Note: Connection closed by remote host (by mail-send.)

The SmtpClientBuilder's timeout is set to 30 seconds. The test sending, however, fails immediately.

Note that this test is done without Credentials.

Where Connection closed by remote host occurs, commonly MailHog should receive the first header: Received 30 bytes: 'MAIL FROM:<[email protected]>\r\n'. See below for a full (elided message body) example of a successful send.

And my test shows:

running 2 tests
test tests::renders_full_newsletter ... ok
test mail::tests::mailer_sends_smtp_tls_mail ... FAILED

failures:

---- mail::tests::mailer_sends_smtp_tls_mail stdout ----
Error: SendMail(UnparseableReply)


failures:
    mail::tests::mailer_sends_smtp_tls_mail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

error: test failed, to rerun pass `--lib`

SendMail is my own error enum variant. UnparseableReply is mail_send::Error.

Traefik writes nothing special to the log (log level Debug):

Handling connection from 172.254.1.1:48226

In contrast, here's the MailHog logs from a good mail send:

Starting session
[PROTO: INVALID] Started session, switching to ESTABLISH state
Sent 45 bytes: '220 mail.some-host.berlin ESMTP MailHog\r\n'
Received 15 bytes: 'EHLO felicity\r\n'
[PROTO: ESTABLISH] Processing line: EHLO felicity
[PROTO: ESTABLISH] In state 1, got command 'EHLO', args 'felicity'
[PROTO: ESTABLISH] In ESTABLISH state
[PROTO: ESTABLISH] Got EHLO command, switching to MAIL state
Sent 20 bytes: '250-Hello felicity\r\n'
Sent 16 bytes: '250-PIPELINING\r\n'
Sent 16 bytes: '250 AUTH PLAIN\r\n'
Received 30 bytes: 'MAIL FROM:<[email protected]>\r\n'
[PROTO: MAIL] Processing line: MAIL FROM:<[email protected]>
[PROTO: MAIL] In state 6, got command 'MAIL', args 'FROM:<[email protected]>'
[PROTO: MAIL] In MAIL state
[PROTO: MAIL] Got MAIL command, switching to RCPT state
Sent 32 bytes: '250 Sender [email protected] ok\r\n'
Received 26 bytes: 'RCPT TO:<[email protected]>\r\n'
[PROTO: RCPT] Processing line: RCPT TO:<[email protected]>
[PROTO: RCPT] In state 7, got command 'RCPT', args 'TO:<[email protected]>'
[PROTO: RCPT] In RCPT state
[PROTO: RCPT] Got RCPT command
Sent 33 bytes: '250 Recipient [email protected] ok\r\n'
Received 6 bytes: 'DATA\r\n'
[PROTO: RCPT] Processing line: DATA
[PROTO: RCPT] In state 7, got command 'DATA', args ''
[PROTO: RCPT] In RCPT state
[PROTO: RCPT] Got DATA command, switching to DATA state
Sent 37 bytes: '354 End data with <CR><LF>.<CR><LF>\r\n'
Received 1024 bytes: 'From: <[email protected]>\r\nTo: <[email protected]>\r\nSubject: Test mail\r\nReply-To: <[email protected]>\r\nMessage-ID: <174421ede9d619d9.fbb0693af3efc5c7.146ed3f42bb856b@felicity>\r\nDate: Wed, 15 Feb 2023 23:04:27 +0000\r\nContent-Type: text/html; charset="utf-8"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n<!doctype html><html xmlns=3D"http://www.w3.org/1999/xhtml" xmlns:v=3D"urn:s=\r\nchemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsoft-com:office:office=\r\n"> [... AND SO ON, receiving the entire mail in chunks of up to 1024 B]'
[PROTO: DATA] Got EOF, storing message and switching to MAIL state
Parsing Content from string: 'From: <[email protected]>
To: <[email protected]>
Subject: Test mail
Reply-To: <[email protected]>
Message-ID: <174421ede9d619d9.fbb0693af3efc5c7.146ed3f42bb856b@felicity>
Date: Wed, 15 Feb 2023 23:04:27 +0000
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable

<!doctype html><html xmlns=3D"http://www.w3.org/1999/xhtml" xmlns:v=3D"urn:s=
chemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsoft-com:office:office=
">[AND SO ON... rest of the mail - not relevant to the bug report]'
Storing message ZjsPQpZCLXvLR44_7SWRBJpCTy9AfPV4JL_CvweyeoU=@mail.some-host.berlin
Sent 90 bytes: '250 Ok: queued as ZjsPQpZCLXvLR44_7SWRBJpCTy9AfPV4JL_CvweyeoU=@mail.some-host.berlin\r\n'
Got message in APIv2 websocket channel
[APIv2] BROADCAST /api/v2/websocket
Got message in APIv1 event stream
Connection closed by remote host
Session ended

PS: "Jim, the Chaos Monkey" (a MailHog feature intended to test application resilience in the face of failure) is disabled.

Sample Usage using XOAuth2 for SMTP Sending of Email

Hello

Do you have a sample in your code that will successfully email using SMPT XOAUTH2?

Url and scopes used to get token.

    AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string())?,
    TokenUrl::new("https://oauth2.googleapis.com/token".to_string())?,

    Scope::new("https://www.googleapis.com/auth/gmail.send".to_string()),
    Scope::new("email".to_string()),
    Scope::new("profile".to_string()),

This is the sample code I created

let auth_code_grant = AuthCodeGrant::new(
    ClientId::new(client_id.to_string()),
    client_secret,
let message = MessageBuilder::new()
    .from((sender_name.as_ref(), sender_email.as_ref()))
    .to(vec![(receiver_name.as_ref(), receiver_email.as_ref())])
    .subject("Microsoft - Test XOAUTH2 SMTP!")
    .html_body("<h1>Hello, world!</h1>")
    .text_body("Hello world!");

let credentials =
    Credentials::new_xoauth2(sender_email.as_ref(), access_token.secret().as_str());
log::info!("Authenticating SMTP XOAUTH2 Credentials....");
let email_connect = SmtpClientBuilder::new("smtp.gmail.com", 587)
    .implicit_tls(false)
    .credentials(credentials)
    .connect()
    .await;

match email_connect {
    Ok(mut result) => {
        log::info!("Sending SMTP XOAUTH2 Email....");
        let send = result.send(message).await;
        match send {
            Ok(_result) => {}
            Err(err) => {
                log::error!("SMTP Sending Error: {err:?}");
            }
        }
    }
    Err(err) => {
        log::error!("SMTP Connecting Error: {err:?}");
    }
}

But I got this error.
AuthenticationFailed(Response { code: 535, esc: [5, 7, 8], message: "Username and Password not accepted. Learn more at\n https://support.google.com/mail/?p=BadCredentials p5-20020a1709028a8500b001ab0d815dbbsm2483183plo.23 - gsmtp" })

I there any lacking in building the email?

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: MissingStartTls

version: mail-send = "0.3.0"

when not connect to stmp with tls:
match SmtpClientBuilder::new(email_host,25) .implicit_tls(false) .credentials((email_username, email_password)) .timeout(Duration::from_secs(3)) .connect() .await .unwrap() .send(message) .await { Ok(_) => println!("Email sent successfully!"), Err(e) => panic!("Could not send email: {:?}", e), }

thread 'main' panicked at 'called Result::unwrap() on an Err value: MissingStartTls',

MailSend violates the license of its dependency smtp-proto

Mail-send is licensed under mit/apache, while it's dependency smtp-proto - under AGPL3.0.

AGPL requires all derived work to be licensed under AGPL. Thus mail_send must be AGPL3.0 too.

Violation of the license of its dependency, makes the mail_send crate unusable.

impl IntoMessage for Message

I need the exact conversion method as defined here:

#[cfg(feature = "builder")]
impl<'x, 'y> IntoMessage<'x> for MessageBuilder<'y> {
fn into_message(self) -> crate::Result<Message<'x>> {
but from a mail_parser::Message. Would you accept a PR based on the same logic, behind a mail-parser cargo feature?

Non-async support?

Hi,

Thanks for publishing this crate and all the other, related, ones.

I'd like to use it in a small program that wouldn't use async or tokio, and it's not clear to me whether it can be done.

The doc says:

Full async (requires Tokio).

which is a bit ambiguous to me. Does it support async with a dependency on tokio if it is enabled, or is the crate only designed to support async?

If the latter, do you have any plans on supporting non-async?

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.