Replace email builder by a new implementation (#393)
* Update dependencies (#386) * Update dependencies and set MSRV to 1.40 * update hyperx * Use display instead of description for errors * Make hostname an optional feature * Envelope from headers * Update hyperx to 1.0 * rename builder to message * Cleanup and make Transport send Messages * Update rustls from 0.16 to 0.17 * Move transports into a common folder * Merge imports from same crate * Add message creation example to the site * Hide "extern crate" in doc examples * Add References and In-Reply-To methods * Add message-id header * Add blog posts and improve doc examples
This commit is contained in:
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -67,7 +67,6 @@ jobs:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
|
||||
4
.github/workflows/website.yml
vendored
4
.github/workflows/website.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- run: cargo install mdbook --no-default-features --features output,search
|
||||
- run: cd website && mdbook build
|
||||
- run: echo "lettre.at" > website/_book/html/CNAME
|
||||
- run: echo "lettre.at" > website/book/html/CNAME
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v2.5.1
|
||||
env:
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
PUBLISH_BRANCH: gh-pages
|
||||
PUBLISH_DIR: ./website/_book/html
|
||||
PUBLISH_DIR: ./website/book/html
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.vscode/
|
||||
.project/
|
||||
.idea/
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
61
Cargo.toml
61
Cargo.toml
@@ -6,9 +6,9 @@ readme = "README.md"
|
||||
homepage = "https://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
authors = ["Alexis Mousset <contact@amousset.me>", "Kayo <kayo@illumium.org>"]
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
@@ -17,41 +17,46 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "^0.11", optional = true }
|
||||
bufstream = { version = "^0.1", optional = true }
|
||||
email = { version = "^0.0.20", optional = true }
|
||||
fast_chemail = "^0.9"
|
||||
hostname = { version = "^0.3", optional = true }
|
||||
log = "^0.4"
|
||||
mime = { version = "^0.3", optional = true }
|
||||
native-tls = { version = "^0.2", optional = true }
|
||||
nom = { version = "^5.0", optional = true }
|
||||
r2d2 = { version = "^0.8", optional = true }
|
||||
rustls = { version = "^0.16", optional = true }
|
||||
serde = { version = "^1.0", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "^1.0", optional = true }
|
||||
time = { version = "^0.2", optional = true }
|
||||
uuid = { version = "^0.8", features = ["v4"], optional = true }
|
||||
webpki = { version = "^0.21", optional = true }
|
||||
base64 = { version = "0.12", optional = true }
|
||||
bufstream = { version = "0.1", optional = true }
|
||||
# TODO to 0.5
|
||||
bytes = { version = "0.4", optional = true }
|
||||
hostname = { version = "0.3", optional = true }
|
||||
hyperx = { version = "1", optional = true, features = ["headers"] }
|
||||
idna = "0.2"
|
||||
log = "0.4"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
mime = { version = "0.3", optional = true }
|
||||
native-tls = { version = "0.2", optional = true }
|
||||
nom = { version = "5", optional = true }
|
||||
once_cell = "1"
|
||||
quoted_printable = { version = "0.4", optional = true }
|
||||
r2d2 = { version = "0.8", optional = true }
|
||||
regex = "1"
|
||||
rustls = { version = "0.17", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
textnonce = { version = "0.7", optional = true }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "^0.3"
|
||||
env_logger = "^0.7"
|
||||
glob = "^0.3"
|
||||
walkdir = "^2"
|
||||
criterion = "0.3"
|
||||
env_logger = "0.7"
|
||||
glob = "0.3"
|
||||
walkdir = "2"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[features]
|
||||
builder = ["email", "mime", "time", "base64", "uuid"]
|
||||
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable", "bytes"]
|
||||
connection-pool = ["r2d2"]
|
||||
default = ["file-transport", "smtp-transport", "sendmail-transport", "native-tls", "builder"]
|
||||
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"]
|
||||
file-transport = ["serde", "serde_json"]
|
||||
rustls-tls = ["webpki", "rustls"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["bufstream", "base64", "nom", "hostname"]
|
||||
smtp-transport = ["bufstream", "base64", "nom"]
|
||||
unstable = []
|
||||
|
||||
[[example]]
|
||||
@@ -61,7 +66,3 @@ required-features = ["smtp-transport"]
|
||||
[[example]]
|
||||
name = "smtp_gmail"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "builder"
|
||||
required-features = ["builder"]
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -1,4 +1,5 @@
|
||||
Copyright (c) 2014-2020 Alexis Mousset
|
||||
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
||||
26
README.md
26
README.md
@@ -31,6 +31,11 @@ Lettre provides the following features:
|
||||
* Secure delivery with SMTP using encryption and authentication
|
||||
* Easy email builders
|
||||
|
||||
Lettre does not provide (for now):
|
||||
|
||||
* Async support
|
||||
* Email parsing
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.40 or newer.
|
||||
@@ -38,25 +43,25 @@ To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.9"
|
||||
lettre = "0.10"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{SmtpClient, Transport, Email, builder::mime::TEXT_PLAIN};
|
||||
use std::path::Path;
|
||||
use lettre::{SmtpClient, Transport, Message};
|
||||
use std::convert::TryInto;
|
||||
|
||||
fn main() {
|
||||
let email = Email::builder()
|
||||
let email = Message::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
.to(("user@example.org", "Firstname Lastname").try_into().unwrap())
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.from("user@example.com".parse().unwrap())
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.attachment_from_file(Path::new("Cargo.toml"), None, &TEXT_PLAIN)
|
||||
.unwrap()
|
||||
.body("Hello world.")
|
||||
//.attachment_from_file(Path::new("Cargo.toml"), None, &TEXT_PLAIN)
|
||||
// FIXME add back attachment example
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -88,4 +93,7 @@ this GitHub repository, must follow our [code of conduct](https://github.com/let
|
||||
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
The builder comes from [emailmessage-rs](https://github.com/katyo/emailmessage-rs) by
|
||||
Kayo, under MIT license.
|
||||
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{
|
||||
smtp::ConnectionReuseParameters, ClientSecurity, Email, EmailAddress, Envelope, SmtpClient,
|
||||
Transport,
|
||||
transport::smtp::ConnectionReuseParameters, ClientSecurity, Message, SmtpClient, Transport,
|
||||
};
|
||||
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
@@ -11,15 +10,13 @@ fn bench_simple_send(c: &mut Criterion) {
|
||||
|
||||
c.bench_function("send email", move |b| {
|
||||
b.iter(|| {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(email));
|
||||
assert!(result.is_ok());
|
||||
})
|
||||
@@ -33,15 +30,13 @@ fn bench_reuse_send(c: &mut Criterion) {
|
||||
.transport();
|
||||
c.bench_function("send email with connection reuse", move |b| {
|
||||
b.iter(|| {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(email));
|
||||
assert!(result.is_ok());
|
||||
})
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
use lettre::Email;
|
||||
use lettre::{SmtpClient, Transport};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let email = Email::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{Email, EmailAddress, Envelope, SmtpClient, Transport};
|
||||
use lettre::{Message, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre::{Email, EmailAddress, Envelope, SmtpClient, Transport};
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello example".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new(
|
||||
"example_username".to_string(),
|
||||
|
||||
282
src/address.rs
Normal file
282
src/address.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
//! Representation of an email address
|
||||
|
||||
use idna::domain_to_ascii;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
net::IpAddr,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Email address
|
||||
///
|
||||
/// This type contains email in canonical form (_user@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Address {
|
||||
/// User part
|
||||
pub user: String,
|
||||
/// Domain part
|
||||
pub domain: String,
|
||||
/// Complete address
|
||||
complete: String,
|
||||
}
|
||||
|
||||
impl<U, D> TryFrom<(U, D)> for Address
|
||||
where
|
||||
U: Into<String>,
|
||||
D: Into<String>,
|
||||
{
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(from: (U, D)) -> Result<Self, Self::Error> {
|
||||
let (user, domain) = from;
|
||||
Self::new(user, domain)
|
||||
}
|
||||
}
|
||||
|
||||
// Regex from the specs
|
||||
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
|
||||
// It will mark esoteric email addresses like quoted string as invalid
|
||||
static USER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
|
||||
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
|
||||
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
|
||||
|
||||
impl Address {
|
||||
/// Create email address from parts
|
||||
#[inline]
|
||||
pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
|
||||
let user = user.into();
|
||||
Address::check_user(&user)?;
|
||||
let domain = domain.into();
|
||||
Address::check_domain(&domain)?;
|
||||
let complete = format!("{}@{}", &user, &domain);
|
||||
Ok(Address {
|
||||
user,
|
||||
domain,
|
||||
complete,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_user(user: &str) -> Result<(), AddressError> {
|
||||
if USER_RE.is_match(user) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AddressError::InvalidUser)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_domain(domain: &str) -> Result<(), AddressError> {
|
||||
Address::check_domain_ascii(domain).or_else(|_| {
|
||||
domain_to_ascii(domain)
|
||||
.map_err(|_| AddressError::InvalidDomain)
|
||||
.and_then(|domain| Address::check_domain_ascii(&domain))
|
||||
})
|
||||
}
|
||||
|
||||
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
||||
if DOMAIN_RE.is_match(domain) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(caps) = LITERAL_RE.captures(domain) {
|
||||
if let Some(cap) = caps.get(1) {
|
||||
if cap.as_str().parse::<IpAddr>().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(AddressError::InvalidDomain)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
f.write_str(&self.complete)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, AddressError> {
|
||||
if val.is_empty() || !val.contains('@') {
|
||||
return Err(AddressError::MissingParts);
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = val.rsplitn(2, '@').collect();
|
||||
let user = parts[1];
|
||||
let domain = parts[0];
|
||||
|
||||
Address::check_user(user)
|
||||
.and_then(|_| Address::check_domain(domain))
|
||||
.map(|_| Address {
|
||||
user: user.into(),
|
||||
domain: domain.into(),
|
||||
complete: val.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Address {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.complete.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for Address {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
self.complete.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum AddressError {
|
||||
MissingParts,
|
||||
Unbalanced,
|
||||
InvalidUser,
|
||||
InvalidDomain,
|
||||
InvalidUtf8b,
|
||||
}
|
||||
|
||||
impl Error for AddressError {}
|
||||
|
||||
impl Display for AddressError {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
AddressError::MissingParts => f.write_str("Missing domain or user"),
|
||||
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
||||
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
||||
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
||||
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod serde {
|
||||
use crate::address::Address;
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
impl Serialize for Address {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Address {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
User,
|
||||
Domain,
|
||||
};
|
||||
|
||||
const FIELDS: &[&str] = &["user", "domain"];
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("'user' or 'domain'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"user" => Ok(Field::User),
|
||||
"domain" => Ok(Field::Domain),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddressVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AddressVisitor {
|
||||
type Value = Address;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("email address string or object")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut user = None;
|
||||
let mut domain = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::User => {
|
||||
if user.is_some() {
|
||||
return Err(DeError::duplicate_field("user"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_user(val).map_err(DeError::custom)?;
|
||||
user = Some(val);
|
||||
}
|
||||
Field::Domain => {
|
||||
if domain.is_some() {
|
||||
return Err(DeError::duplicate_field("domain"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_domain(val).map_err(DeError::custom)?;
|
||||
domain = Some(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
|
||||
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
||||
// FIXME avoid unwrap here
|
||||
Ok(Address::new(user, domain).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AddressVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//! Error and result type for emails
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Envelope error
|
||||
Envelope(crate::error::Error),
|
||||
/// Unparseable filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(&match *self {
|
||||
CannotParseFilename => "Could not parse attachment filename".to_owned(),
|
||||
Io(ref err) => err.to_string(),
|
||||
Envelope(ref err) => err.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
match *self {
|
||||
Envelope(ref err) => Some(err),
|
||||
Io(ref err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::error::Error> for Error {
|
||||
fn from(err: crate::error::Error) -> Error {
|
||||
Error::Envelope(err)
|
||||
}
|
||||
}
|
||||
@@ -1,762 +0,0 @@
|
||||
use crate::{error::Error as LettreError, Email, EmailAddress, Envelope};
|
||||
pub use email::{Address, Header, Mailbox as OriginalMailbox, MimeMessage, MimeMultipartType};
|
||||
use error::Error;
|
||||
pub use mime;
|
||||
use mime::Mime;
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
const DT_RFC822Z: &str = "%a, %d %b %Y %T %z";
|
||||
|
||||
// From rust-email, allows adding rfc2047 encoding
|
||||
|
||||
/// Represents an RFC 5322 mailbox
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct Mailbox {
|
||||
inner: OriginalMailbox,
|
||||
}
|
||||
|
||||
impl Mailbox {
|
||||
/// Create a new Mailbox without a display name
|
||||
pub fn new(address: String) -> Mailbox {
|
||||
Mailbox {
|
||||
inner: OriginalMailbox::new(address),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Mailbox with a display name
|
||||
pub fn new_with_name(name: String, address: String) -> Mailbox {
|
||||
Mailbox {
|
||||
inner: OriginalMailbox::new_with_name(encode_rfc2047(&name).to_string(), address),
|
||||
}
|
||||
}
|
||||
|
||||
fn original(self) -> OriginalMailbox {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mailbox {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Mailbox {
|
||||
fn from(mailbox: &'a str) -> Mailbox {
|
||||
Mailbox::new(mailbox.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Mailbox {
|
||||
fn from(mailbox: String) -> Mailbox {
|
||||
Mailbox::new(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> From<(S, T)> for Mailbox {
|
||||
fn from(header: (S, T)) -> Mailbox {
|
||||
let (address, alias) = header;
|
||||
Mailbox::new_with_name(alias.into(), address.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a UTF-8 string according to RFC 2047, if need be.
|
||||
///
|
||||
/// Currently, this only uses "B" encoding, when pure ASCII cannot represent the
|
||||
/// string accurately.
|
||||
///
|
||||
/// Can be used on header content.
|
||||
pub fn encode_rfc2047(text: &str) -> Cow<str> {
|
||||
if text.is_ascii() {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
Cow::Owned(
|
||||
base64::encode_config(text.as_bytes(), base64::STANDARD)
|
||||
// base64 so ascii
|
||||
.as_bytes()
|
||||
// Max length - wrapping chars
|
||||
.chunks(75 - 12)
|
||||
.map(|d| format!("=?utf-8?B?{}?=", std::str::from_utf8(d).unwrap()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailAddress> for OriginalMailbox {
|
||||
fn from(addr: EmailAddress) -> Self {
|
||||
OriginalMailbox::new(addr.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `MimeMessage` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct PartBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
}
|
||||
|
||||
impl Default for PartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a message id
|
||||
pub type MessageId = String;
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct EmailBuilder {
|
||||
/// Message
|
||||
message: PartBuilder,
|
||||
/// The recipients' addresses for the mail header
|
||||
to: Vec<Address>,
|
||||
/// The sender addresses for the mail header
|
||||
from: Vec<Address>,
|
||||
/// The Cc addresses for the mail header
|
||||
cc: Vec<Address>,
|
||||
/// The Bcc addresses for the mail header
|
||||
bcc: Vec<Address>,
|
||||
/// The Reply-To addresses for the mail header
|
||||
reply_to: Vec<Address>,
|
||||
/// The In-Reply-To ids for the mail header
|
||||
in_reply_to: Vec<MessageId>,
|
||||
/// The References ids for the mail header
|
||||
references: Vec<MessageId>,
|
||||
/// The sender address for the mail header
|
||||
sender: Option<OriginalMailbox>,
|
||||
/// The envelope
|
||||
envelope: Option<Envelope>,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
/// Message-ID
|
||||
message_id: Option<String>,
|
||||
}
|
||||
|
||||
impl PartBuilder {
|
||||
/// Creates a new empty part
|
||||
pub fn new() -> PartBuilder {
|
||||
PartBuilder {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
|
||||
self.message.headers.insert(header.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
|
||||
self.message.body = body.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines a `MimeMultipartType` value
|
||||
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
|
||||
self.message.message_type = Some(mime_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `ContentType` header with the given MIME type
|
||||
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
|
||||
self.header(("Content-Type", content_type.to_string()))
|
||||
}
|
||||
|
||||
/// Adds a child part
|
||||
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
|
||||
self.message.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets built `MimeMessage`
|
||||
pub fn build(mut self) -> MimeMessage {
|
||||
self.message.update_headers();
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: PartBuilder::new(),
|
||||
to: vec![],
|
||||
from: vec![],
|
||||
cc: vec![],
|
||||
bcc: vec![],
|
||||
reply_to: vec![],
|
||||
in_reply_to: vec![],
|
||||
references: vec![],
|
||||
sender: None,
|
||||
envelope: None,
|
||||
date_issued: false,
|
||||
message_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
|
||||
self.message = self.message.body(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
|
||||
self.message = self.message.header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.from.push(Address::Mailbox(mailbox.original()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.to.push(Address::Mailbox(mailbox.original()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.cc.push(Address::Mailbox(mailbox.original()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Bcc` header and stores the recipient address
|
||||
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.bcc.push(Address::Mailbox(mailbox.original()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.reply_to.push(Address::Mailbox(mailbox.original()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `In-Reply-To` header
|
||||
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.in_reply_to.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `References` header
|
||||
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.references.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.sender = Some(mailbox.original());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
|
||||
self.message = self.message.header((
|
||||
"Subject".to_string(),
|
||||
encode_rfc2047(subject.into().as_ref()),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &OffsetDateTime) -> EmailBuilder {
|
||||
self.message = self.message.header(("Date", date.format(DT_RFC822Z)));
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a file
|
||||
///
|
||||
/// If not specified, the filename will be extracted from the file path.
|
||||
pub fn attachment_from_file(
|
||||
self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
self.attachment(
|
||||
fs::read(path)?.as_slice(),
|
||||
filename.unwrap_or(
|
||||
path.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.ok_or(Error::CannotParseFilename)?,
|
||||
),
|
||||
content_type,
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a vector of bytes.
|
||||
pub fn attachment(
|
||||
self,
|
||||
body: &[u8],
|
||||
filename: &str,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
let encoded_body = base64::encode(&body);
|
||||
let content = PartBuilder::new()
|
||||
.body(encoded_body)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
))
|
||||
.header(("Content-Type", content_type.to_string()))
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.build();
|
||||
|
||||
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
|
||||
}
|
||||
|
||||
/// Embed file so it can be referenced by Content-ID
|
||||
///
|
||||
/// If not specified, the filename will be extracted from the file path.
|
||||
pub fn embed_from_file(
|
||||
self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
content_id: &str,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
self.embed(
|
||||
fs::read(path)?.as_slice(),
|
||||
filename.unwrap_or(
|
||||
path.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.ok_or(Error::CannotParseFilename)?,
|
||||
),
|
||||
content_type,
|
||||
content_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds an embed to the email from a vector of bytes.
|
||||
pub fn embed(
|
||||
self,
|
||||
body: &[u8],
|
||||
filename: &str,
|
||||
content_type: &Mime,
|
||||
content_id: &str,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
let encoded_body = base64::encode(&body);
|
||||
let content = PartBuilder::new()
|
||||
.body(encoded_body)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
format!("inline; filename=\"{}\"", filename),
|
||||
))
|
||||
.header((
|
||||
"Content-Type",
|
||||
format!("{}; name=\"{}\"", content_type, filename),
|
||||
))
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.header(("Content-ID", format!("<{}>", content_id)))
|
||||
.build();
|
||||
|
||||
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
|
||||
}
|
||||
|
||||
/// Set the message type
|
||||
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
|
||||
self.message = self.message.message_type(message_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a child
|
||||
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
|
||||
self.message = self.message.child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body)
|
||||
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
|
||||
.build();
|
||||
self.child(text)
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let html = PartBuilder::new()
|
||||
.body(body)
|
||||
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
|
||||
.build();
|
||||
self.child(html)
|
||||
}
|
||||
|
||||
/// Sets the email content
|
||||
pub fn alternative<S: Into<String>, T: Into<String>>(
|
||||
self,
|
||||
body_html: S,
|
||||
body_text: T,
|
||||
) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body_text)
|
||||
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
|
||||
.build();
|
||||
|
||||
let html = PartBuilder::new()
|
||||
.body(body_html)
|
||||
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
|
||||
.build();
|
||||
|
||||
let alternate = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(text)
|
||||
.child(html);
|
||||
|
||||
self.message_type(MimeMultipartType::Mixed)
|
||||
.child(alternate.build())
|
||||
}
|
||||
|
||||
/// Sets the `Message-ID` header
|
||||
pub fn message_id<S: Clone + Into<String>>(mut self, id: S) -> EmailBuilder {
|
||||
self.message = self.message.header(("Message-ID", id.clone()));
|
||||
self.message_id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the envelope for manual destination control
|
||||
/// If this function is not called, the envelope will be calculated
|
||||
/// from the "to" and "cc" addresses you set.
|
||||
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
|
||||
self.envelope = Some(envelope);
|
||||
self
|
||||
}
|
||||
|
||||
/// Only builds the body, this can be used to encrypt or sign
|
||||
/// using S/MIME
|
||||
pub fn build_body(self) -> Result<Vec<u8>, Error> {
|
||||
Ok(self.message.build().as_string().into_bytes())
|
||||
}
|
||||
|
||||
/// Builds the Email
|
||||
pub fn build(mut self) -> Result<Email, Error> {
|
||||
// If there are multiple addresses in "From", the "Sender" is required.
|
||||
if self.from.len() >= 2 && self.sender.is_none() {
|
||||
// So, we must find something to put as Sender.
|
||||
for possible_sender in &self.from {
|
||||
// Only a mailbox can be used as sender, not Address::Group.
|
||||
if let Address::Mailbox(ref mbx) = *possible_sender {
|
||||
self.sender = Some(mbx.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Address::Group is not yet supported, so the line below will never panic.
|
||||
// If groups are supported one day, add another Error for this case
|
||||
// and return it here, if sender_header is still None at this point.
|
||||
assert!(self.sender.is_some());
|
||||
}
|
||||
// Add the sender header, if any.
|
||||
if let Some(ref v) = self.sender {
|
||||
self.message = self.message.header(("Sender", v.to_string()));
|
||||
}
|
||||
// Calculate the envelope
|
||||
let envelope = match self.envelope {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
// we need to generate the envelope
|
||||
let mut to = vec![];
|
||||
// add all receivers in to_header and cc_header
|
||||
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
|
||||
match *receiver {
|
||||
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
|
||||
Address::Group(_, ref ms) => {
|
||||
for m in ms.iter() {
|
||||
to.push(EmailAddress::from_str(&m.address.clone())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let from = Some(EmailAddress::from_str(&match self.sender {
|
||||
Some(x) => Ok(x.address), // if we have a sender_header, use it
|
||||
None => {
|
||||
// use a from header
|
||||
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
|
||||
match self.from.first() {
|
||||
Some(a) => match *a {
|
||||
// if we have a from header
|
||||
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
|
||||
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
|
||||
// if it's an author group, use the first author
|
||||
Some(mailbox) => Ok(mailbox.address.clone()),
|
||||
// for an empty author group (the rarest of the rare cases)
|
||||
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
|
||||
},
|
||||
},
|
||||
// if we don't have a from header
|
||||
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
|
||||
}
|
||||
}
|
||||
}?)?);
|
||||
Envelope::new(from, to)?
|
||||
}
|
||||
};
|
||||
// Add the collected addresses as mailbox-list all at once.
|
||||
// The unwraps are fine because the conversions for Vec<Address> never errs.
|
||||
if !self.to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("To".into(), self.to).unwrap());
|
||||
}
|
||||
if !self.from.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), self.from).unwrap());
|
||||
} else if let Some(from) = envelope.from() {
|
||||
let from = vec![Address::new_mailbox(from.to_string())];
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), from).unwrap());
|
||||
} else {
|
||||
return Err(Error::Envelope(LettreError::MissingFrom));
|
||||
}
|
||||
if !self.cc.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
|
||||
}
|
||||
if !self.reply_to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
|
||||
}
|
||||
if !self.in_reply_to.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
if !self.references.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.date_issued {
|
||||
self.message = self
|
||||
.message
|
||||
.header(("Date", OffsetDateTime::now().format(DT_RFC822Z)));
|
||||
}
|
||||
|
||||
self.message = self.message.header(("MIME-Version", "1.0"));
|
||||
|
||||
let message_id = match self.message_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let message_id = Uuid::new_v4();
|
||||
self.message = self
|
||||
.message
|
||||
.header(("Message-ID", format!("<{}.lettre@localhost>", message_id)));
|
||||
message_id.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Email::new(
|
||||
envelope,
|
||||
message_id,
|
||||
self.message.build().as_string().into_bytes(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::EmailAddress;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[test]
|
||||
fn test_encode_rfc2047() {
|
||||
assert_eq!(encode_rfc2047("test"), "test");
|
||||
assert_eq!(encode_rfc2047("testà"), "=?utf-8?B?dGVzdMOg?=");
|
||||
assert_eq!(
|
||||
encode_rfc2047(
|
||||
"testàtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest"
|
||||
),
|
||||
"=?utf-8?B?dGVzdMOgdGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHR?=\r\n=?utf-8?B?lc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0?="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_from() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = OffsetDateTime::now();
|
||||
let email: Email = email_builder
|
||||
.to("anna@example.com")
|
||||
.from("dieter@example.com")
|
||||
.from("joachim@example.com")
|
||||
.date(&date_now)
|
||||
.subject("Invitation")
|
||||
.body("We invite you!")
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
|
||||
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
|
||||
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
|
||||
date_now.format(DT_RFC822Z),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = OffsetDateTime::now();
|
||||
|
||||
let email: Email = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.cc(("cc2@localhost", "Aliäs"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.in_reply_to("original".to_string())
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>, \"=?utf-8?B?QWxpw6Rz?=\" <cc2@localhost>\r\n\
|
||||
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
|
||||
MIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.format(DT_RFC822Z),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_message_id() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = OffsetDateTime::now();
|
||||
|
||||
let email: Email = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.in_reply_to("original".to_string())
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.message_id("my-shiny-id")
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nMessage-ID: \
|
||||
my-shiny-id\r\nSender: <sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||
<reply@localhost>\r\nIn-Reply-To: original\r\nMIME-Version: 1.0\r\n\r\nHello \
|
||||
World!\r\n",
|
||||
date_now.format(DT_RFC822Z)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder_body() {
|
||||
let date_now = OffsetDateTime::now();
|
||||
let email_builder = EmailBuilder::new()
|
||||
.text("TestTest")
|
||||
.subject("A Subject")
|
||||
.to("user@localhost")
|
||||
.date(&date_now);
|
||||
let string_res = String::from_utf8(email_builder.build_body().unwrap());
|
||||
assert!(string_res.unwrap().starts_with("Subject: A Subject\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_subject_encoding() {
|
||||
let date_now = OffsetDateTime::now();
|
||||
let email_builder = EmailBuilder::new()
|
||||
.text("TestTest")
|
||||
.subject("A ö Subject")
|
||||
.to("user@localhost")
|
||||
.date(&date_now);
|
||||
let string_res = String::from_utf8(email_builder.build_body().unwrap());
|
||||
assert!(string_res
|
||||
.unwrap()
|
||||
.starts_with("Subject: =?utf-8?B?QSDDtiBTdWJqZWN0?=\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_sendable() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = OffsetDateTime::now();
|
||||
|
||||
let email: Email = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
assert_eq!(
|
||||
email.envelope().from().unwrap().to_string(),
|
||||
"sender@localhost".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
email.envelope().to(),
|
||||
vec![
|
||||
EmailAddress::new("user@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("cc@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
|
||||
]
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
}
|
||||
43
src/error.rs
43
src/error.rs
@@ -1,35 +1,52 @@
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// Error type for email content
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Missing from in envelope
|
||||
MissingFrom,
|
||||
/// Missing to in envelope
|
||||
MissingTo,
|
||||
/// Invalid email
|
||||
InvalidEmailAddress,
|
||||
/// Can only be one from in envelope
|
||||
TooManyFrom,
|
||||
/// Invalid email: missing at
|
||||
EmailMissingAt,
|
||||
/// Invalid email: missing local part
|
||||
EmailMissingLocalPart,
|
||||
/// Invalid email: missing domain
|
||||
EmailMissingDomain,
|
||||
/// Cannot parse filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(&match *self {
|
||||
MissingFrom => "missing source address, invalid envelope".to_owned(),
|
||||
MissingTo => "missing destination address, invalid envelope".to_owned(),
|
||||
InvalidEmailAddress => "invalid email address".to_owned(),
|
||||
fmt.write_str(&match self {
|
||||
Error::MissingFrom => "missing source address, invalid envelope".to_string(),
|
||||
Error::MissingTo => "missing destination address, invalid envelope".to_string(),
|
||||
Error::TooManyFrom => "there can only be one source address".to_string(),
|
||||
Error::EmailMissingAt => "missing @ in email address".to_string(),
|
||||
Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
|
||||
Error::EmailMissingDomain => "missing domain in email address".to_string(),
|
||||
Error::CannotParseFilename => "could not parse attachment filename".to_string(),
|
||||
Error::Io(e) => e.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
None
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Email result type
|
||||
pub type EmailResult<T> = Result<T, Error>;
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
273
src/lib.rs
273
src/lib.rs
@@ -1,98 +1,43 @@
|
||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
||||
//!
|
||||
//! This mailer contains the available transports for your emails.
|
||||
//! Lettre provides an email builder and several email transports.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.10.0")]
|
||||
#![doc(html_favicon_url = "https://lettre.at/favicon.png")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
|
||||
#[cfg(feature = "builder")]
|
||||
pub mod builder;
|
||||
pub mod address;
|
||||
pub mod error;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::builder::EmailBuilder;
|
||||
use crate::error::EmailResult;
|
||||
pub mod message;
|
||||
pub mod transport;
|
||||
|
||||
pub use crate::address::Address;
|
||||
use crate::error::Error;
|
||||
#[cfg(feature = "builder")]
|
||||
pub use crate::message::{
|
||||
header::{self, Headers},
|
||||
Mailboxes, Message,
|
||||
};
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use crate::file::FileTransport;
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use crate::sendmail::SendmailTransport;
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::smtp::client::net::ClientTlsParameters;
|
||||
pub use crate::transport::smtp::client::net::ClientTlsParameters;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use crate::smtp::r2d2::SmtpConnectionManager;
|
||||
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
use fast_chemail::is_valid_email;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Email address
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct EmailAddress(String);
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(address: String) -> EmailResult<EmailAddress> {
|
||||
if !EmailAddress::is_valid(&address) {
|
||||
return Err(Error::InvalidEmailAddress);
|
||||
}
|
||||
Ok(EmailAddress(address))
|
||||
}
|
||||
|
||||
pub fn is_valid(addr: &str) -> bool {
|
||||
is_valid_email(addr) || addr.ends_with("localhost")
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
EmailAddress::new(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EmailAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for EmailAddress {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
#[cfg(feature = "builder")]
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Simple email envelope representation
|
||||
///
|
||||
@@ -103,14 +48,14 @@ pub struct Envelope {
|
||||
/// The envelope recipients' addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
forward_path: Vec<EmailAddress>,
|
||||
forward_path: Vec<Address>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<EmailAddress>,
|
||||
reverse_path: Option<Address>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
|
||||
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
|
||||
if to.is_empty() {
|
||||
return Err(Error::MissingTo);
|
||||
}
|
||||
@@ -121,88 +66,128 @@ impl Envelope {
|
||||
}
|
||||
|
||||
/// Destination addresses of the envelope
|
||||
pub fn to(&self) -> &[EmailAddress] {
|
||||
pub fn to(&self) -> &[Address] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Source address of the envelope
|
||||
pub fn from(&self) -> Option<&EmailAddress> {
|
||||
pub fn from(&self) -> Option<&Address> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Reader(Box<dyn Read + Send>),
|
||||
Bytes(Cursor<Vec<u8>>),
|
||||
}
|
||||
impl TryFrom<&Headers> for Envelope {
|
||||
type Error = Error;
|
||||
|
||||
impl Read for Message {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
Message::Reader(ref mut rdr) => rdr.read(buf),
|
||||
Message::Bytes(ref mut rdr) => rdr.read(buf),
|
||||
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
|
||||
let from = match headers.get::<header::Sender>() {
|
||||
// If there is a Sender, use it
|
||||
Some(header::Sender(a)) => Some(a.email.clone()),
|
||||
// ... else use the first From address
|
||||
None => match headers.get::<header::From>() {
|
||||
Some(header::From(ref a)) => Some(a.iter().next().unwrap().email.clone()),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
fn add_addresses_from_mailboxes(
|
||||
addresses: &mut Vec<Address>,
|
||||
mailboxes: Option<&Mailboxes>,
|
||||
) {
|
||||
if let Some(mailboxes) = mailboxes {
|
||||
for mailbox in mailboxes.iter() {
|
||||
addresses.push(mailbox.email.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut to = vec![];
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
|
||||
|
||||
Self::new(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sendable email structure
|
||||
pub struct Email {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Message,
|
||||
}
|
||||
|
||||
impl Email {
|
||||
/// Creates a new email builder
|
||||
#[cfg(feature = "builder")]
|
||||
pub fn builder() -> EmailBuilder {
|
||||
EmailBuilder::new()
|
||||
}
|
||||
|
||||
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> Email {
|
||||
Email {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Bytes(Cursor::new(message)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_reader(
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Box<dyn Read + Send>,
|
||||
) -> Email {
|
||||
Email {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Reader(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
pub fn message_id(&self) -> &str {
|
||||
&self.message_id
|
||||
}
|
||||
|
||||
pub fn message(self) -> Message {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub fn message_to_string(mut self) -> Result<String, io::Error> {
|
||||
let mut message_content = String::new();
|
||||
self.message.read_to_string(&mut message_content)?;
|
||||
Ok(message_content)
|
||||
}
|
||||
}
|
||||
// FIXME generate random log id
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait Transport<'a> {
|
||||
pub trait Transport<'a, B> {
|
||||
/// Result type for the transport
|
||||
type Result;
|
||||
|
||||
/// Sends the email
|
||||
fn send<E: Into<Email>>(&mut self, email: E) -> Self::Result;
|
||||
/// FIXME not mut
|
||||
fn send(&mut self, email: Message<B>) -> Self::Result
|
||||
where
|
||||
B: Display;
|
||||
/*
|
||||
{
|
||||
&mut self,
|
||||
Box::new(Cursor::new(email.to_string().as_bytes())),
|
||||
email.envelope(),
|
||||
Uuid::new_v4().to_string(),
|
||||
|
||||
}*/
|
||||
|
||||
// TODO allow sending generic data
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::{header, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::To(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
Envelope::new(
|
||||
Some(Address::new("kayo", "example.com").unwrap()),
|
||||
vec![Address::new("amousset", "example.com").unwrap()]
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers_sender() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::Sender(sender));
|
||||
headers.set(header::To(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
Envelope::new(
|
||||
Some(Address::new("kayo2", "example.com").unwrap()),
|
||||
vec![Address::new("amousset", "example.com").unwrap()]
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers_no_to() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::Sender(sender));
|
||||
|
||||
assert!(Envelope::try_from(&headers).is_err(),);
|
||||
}
|
||||
}
|
||||
|
||||
451
src/message/encoder.rs
Normal file
451
src/message/encoder.rs
Normal file
@@ -0,0 +1,451 @@
|
||||
use crate::message::header::ContentTransferEncoding;
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf};
|
||||
use std::{
|
||||
cmp::min,
|
||||
error::Error,
|
||||
fmt::{Debug, Display, Formatter, Result as FmtResult},
|
||||
};
|
||||
|
||||
/// Content encoding error
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EncoderError<E> {
|
||||
Source(E),
|
||||
Coding,
|
||||
}
|
||||
|
||||
impl<E> Error for EncoderError<E> where E: Debug + Display {}
|
||||
|
||||
impl<E> Display for EncoderError<E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
EncoderError::Source(error) => write!(f, "Source error: {}", error),
|
||||
EncoderError::Coding => f.write_str("Coding error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoder trait
|
||||
pub trait EncoderCodec: Send {
|
||||
/// Encode chunk of data
|
||||
fn encode_chunk(&mut self, input: &dyn Buf) -> Result<Bytes, ()>;
|
||||
|
||||
/// Encode end of stream
|
||||
///
|
||||
/// This proposed to use for stateful encoders like *base64*.
|
||||
fn finish_chunk(&mut self) -> Result<Bytes, ()> {
|
||||
Ok(Bytes::new())
|
||||
}
|
||||
|
||||
/// Encode all data
|
||||
fn encode_all(&mut self, source: &dyn Buf) -> Result<Bytes, ()> {
|
||||
let chunk = self.encode_chunk(source)?;
|
||||
let end = self.finish_chunk()?;
|
||||
|
||||
Ok(if end.is_empty() {
|
||||
chunk
|
||||
} else {
|
||||
let mut chunk = chunk.try_mut().unwrap();
|
||||
chunk.put(end);
|
||||
chunk.freeze()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 7bit codec
|
||||
///
|
||||
struct SevenBitCodec {
|
||||
line_wrapper: EightBitCodec,
|
||||
}
|
||||
|
||||
impl SevenBitCodec {
|
||||
pub fn new() -> Self {
|
||||
SevenBitCodec {
|
||||
line_wrapper: EightBitCodec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for SevenBitCodec {
|
||||
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
|
||||
if chunk.bytes().iter().all(u8::is_ascii) {
|
||||
self.line_wrapper.encode_chunk(chunk)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quoted-Printable codec
|
||||
///
|
||||
struct QuotedPrintableCodec();
|
||||
|
||||
impl QuotedPrintableCodec {
|
||||
pub fn new() -> Self {
|
||||
QuotedPrintableCodec()
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for QuotedPrintableCodec {
|
||||
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
|
||||
Ok(quoted_printable::encode(chunk.bytes()).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Base64 codec
|
||||
///
|
||||
struct Base64Codec {
|
||||
line_wrapper: EightBitCodec,
|
||||
last_padding: Bytes,
|
||||
}
|
||||
|
||||
impl Base64Codec {
|
||||
pub fn new() -> Self {
|
||||
Base64Codec {
|
||||
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
|
||||
last_padding: Bytes::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for Base64Codec {
|
||||
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
|
||||
let in_len = self.last_padding.len() + chunk.remaining();
|
||||
let out_len = in_len * 4 / 3;
|
||||
|
||||
let mut out = BytesMut::with_capacity(out_len);
|
||||
|
||||
let chunk = if self.last_padding.is_empty() {
|
||||
chunk.bytes()[..].into_buf()
|
||||
} else {
|
||||
let mut src = BytesMut::with_capacity(3);
|
||||
let len = min(chunk.remaining(), 3 - self.last_padding.len());
|
||||
|
||||
src.put(&self.last_padding);
|
||||
src.put(&chunk.bytes()[..len]);
|
||||
|
||||
// encode beginning
|
||||
unsafe {
|
||||
let len = base64::encode_config_slice(&src, base64::STANDARD, out.bytes_mut());
|
||||
out.advance_mut(len);
|
||||
}
|
||||
|
||||
chunk.bytes()[len..].into_buf()
|
||||
};
|
||||
|
||||
let len = chunk.remaining() - (chunk.remaining() % 3);
|
||||
let chunk = if len > 0 {
|
||||
// encode chunk
|
||||
unsafe {
|
||||
let len = base64::encode_config_slice(
|
||||
&chunk.bytes()[..len],
|
||||
base64::STANDARD,
|
||||
out.bytes_mut(),
|
||||
);
|
||||
out.advance_mut(len);
|
||||
}
|
||||
chunk.bytes()[len..].into_buf()
|
||||
} else {
|
||||
chunk.bytes()[..].into_buf()
|
||||
};
|
||||
|
||||
// update last padding
|
||||
self.last_padding = chunk.bytes().into();
|
||||
|
||||
self.line_wrapper.encode_chunk(&out.freeze().into_buf())
|
||||
}
|
||||
|
||||
fn finish_chunk(&mut self) -> Result<Bytes, ()> {
|
||||
let mut out = BytesMut::with_capacity(4);
|
||||
|
||||
unsafe {
|
||||
let len =
|
||||
base64::encode_config_slice(&self.last_padding, base64::STANDARD, out.bytes_mut());
|
||||
out.advance_mut(len);
|
||||
}
|
||||
|
||||
self.line_wrapper.encode_chunk(&out.freeze().into_buf())
|
||||
}
|
||||
}
|
||||
|
||||
/// 8bit codec
|
||||
///
|
||||
struct EightBitCodec {
|
||||
max_length: usize,
|
||||
line_bytes: usize,
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
|
||||
|
||||
impl EightBitCodec {
|
||||
pub fn new() -> Self {
|
||||
EightBitCodec {
|
||||
max_length: DEFAULT_MAX_LINE_LENGTH,
|
||||
line_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_limit(mut self, max_length: usize) -> Self {
|
||||
self.max_length = max_length;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for EightBitCodec {
|
||||
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
|
||||
let mut out = BytesMut::with_capacity(chunk.remaining() + 20);
|
||||
let mut src = chunk.bytes()[..].into_buf();
|
||||
while src.has_remaining() {
|
||||
let line_break = src.bytes().iter().position(|b| *b == b'\n');
|
||||
let mut split_pos = if let Some(line_break) = line_break {
|
||||
line_break
|
||||
} else {
|
||||
src.remaining()
|
||||
};
|
||||
let max_length = self.max_length - self.line_bytes;
|
||||
if split_pos < max_length {
|
||||
// advance line bytes
|
||||
self.line_bytes += split_pos;
|
||||
} else {
|
||||
split_pos = max_length;
|
||||
// reset line bytes
|
||||
self.line_bytes = 0;
|
||||
};
|
||||
let has_remaining = split_pos < src.remaining();
|
||||
//let mut taken = src.take(split_pos);
|
||||
out.reserve(split_pos + if has_remaining { 2 } else { 0 });
|
||||
//out.put(&mut taken);
|
||||
out.put(&src.bytes()[..split_pos]);
|
||||
if has_remaining {
|
||||
out.put_slice(b"\r\n");
|
||||
}
|
||||
src.advance(split_pos);
|
||||
//src = taken.into_inner();
|
||||
}
|
||||
Ok(out.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary codec
|
||||
///
|
||||
struct BinaryCodec;
|
||||
|
||||
impl BinaryCodec {
|
||||
pub fn new() -> Self {
|
||||
BinaryCodec
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for BinaryCodec {
|
||||
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
|
||||
Ok(chunk.bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
|
||||
use self::ContentTransferEncoding::*;
|
||||
if let Some(encoding) = encoding {
|
||||
match encoding {
|
||||
SevenBit => Box::new(SevenBitCodec::new()),
|
||||
QuotedPrintable => Box::new(QuotedPrintableCodec::new()),
|
||||
Base64 => Box::new(Base64Codec::new()),
|
||||
EightBit => Box::new(EightBitCodec::new()),
|
||||
Binary => Box::new(BinaryCodec::new()),
|
||||
}
|
||||
} else {
|
||||
Box::new(BinaryCodec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{
|
||||
Base64Codec, BinaryCodec, EightBitCodec, EncoderCodec, QuotedPrintableCodec, SevenBitCodec,
|
||||
};
|
||||
use bytes::IntoBuf;
|
||||
use std::str::from_utf8;
|
||||
|
||||
#[test]
|
||||
fn seven_bit_encode() {
|
||||
let mut c = SevenBitCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Hello, world!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Hello, world!".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Hello, мир!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Err(())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode() {
|
||||
let mut c = QuotedPrintableCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Привет, мир!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok(
|
||||
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".into()
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(c.encode_chunk(&"Текст письма в уникоде".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode() {
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_all(&"Привет, мир!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("0J/RgNC40LLQtdGCLCDQvNC40YAh".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.encode_all(&"Текст письма в уникоде подлиннее.".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok(concat!(
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ\r\n",
|
||||
"vtC00LUg0L/QvtC00LvQuNC90L3QtdC1Lg=="
|
||||
)
|
||||
.into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_all() {
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_all(
|
||||
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую."
|
||||
.into_buf()
|
||||
).map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok(
|
||||
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
|
||||
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
|
||||
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
|
||||
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=").into()
|
||||
))
|
||||
);
|
||||
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_all(
|
||||
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это."
|
||||
.into_buf()
|
||||
).map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok(
|
||||
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
|
||||
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
|
||||
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
|
||||
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
|
||||
"0L4u").into()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_chunked() {
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Chunk.".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Q2h1bmsu".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.finish_chunk()
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("".into()))
|
||||
);
|
||||
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Chunk".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Q2h1".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.finish_chunk()
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("bms=".into()))
|
||||
);
|
||||
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Chun".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Q2h1".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.finish_chunk()
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("bg==".into()))
|
||||
);
|
||||
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Chu".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Q2h1".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.finish_chunk()
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_encode() {
|
||||
let mut c = EightBitCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Hello, world!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Hello, world!".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Hello, мир!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Hello, мир!".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_encode() {
|
||||
let mut c = BinaryCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Hello, world!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Hello, world!".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
c.encode_chunk(&"Hello, мир!".into_buf())
|
||||
.map(|s| from_utf8(&s).map(|s| String::from(s))),
|
||||
Ok(Ok("Hello, мир!".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
121
src/message/header/content.rs
Normal file
121
src/message/header/content.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
|
||||
str::{from_utf8, FromStr},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ContentTransferEncoding {
|
||||
SevenBit,
|
||||
QuotedPrintable,
|
||||
Base64,
|
||||
// 8BITMIME
|
||||
EightBit,
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::SevenBit
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTransferEncoding {
|
||||
fn fmt(&self, f: &mut FmtFormatter) -> FmtResult {
|
||||
use self::ContentTransferEncoding::*;
|
||||
f.write_str(match *self {
|
||||
SevenBit => "7bit",
|
||||
QuotedPrintable => "quoted-printable",
|
||||
Base64 => "base64",
|
||||
EightBit => "8bit",
|
||||
Binary => "binary",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ContentTransferEncoding {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use self::ContentTransferEncoding::*;
|
||||
match s {
|
||||
"7bit" => Ok(SevenBit),
|
||||
"quoted-printable" => Ok(QuotedPrintable),
|
||||
"base64" => Ok(Base64),
|
||||
"8bit" => Ok(EightBit),
|
||||
"binary" => Ok(Binary),
|
||||
_ => Err(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn header_name() -> &'static str {
|
||||
"Content-Transfer-Encoding"
|
||||
}
|
||||
|
||||
// FIXME HeaderError->HeaderError, same for result
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
|
||||
.and_then(|s| {
|
||||
s.parse::<ContentTransferEncoding>()
|
||||
.map_err(|_| HeaderError::Header)
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentTransferEncoding;
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentTransferEncoding::SevenBit);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: 7bit\r\n"
|
||||
);
|
||||
|
||||
headers.set(ContentTransferEncoding::Base64);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: base64\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "7bit");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::SevenBit)
|
||||
);
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "base64");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::Base64)
|
||||
);
|
||||
}
|
||||
}
|
||||
289
src/message/header/mailbox.rs
Normal file
289
src/message/header/mailbox.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use crate::message::{
|
||||
mailbox::{Mailbox, Mailboxes},
|
||||
utf8_b,
|
||||
};
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
|
||||
|
||||
/// Header which can contains multiple mailboxes
|
||||
pub trait MailboxesHeader {
|
||||
fn join_mailboxes(&mut self, other: Self);
|
||||
}
|
||||
|
||||
macro_rules! mailbox_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailbox);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized {
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.and_then(|mbs| {
|
||||
mbs.into_single().ok_or(HeaderError::Header)
|
||||
}).map($type_name)
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&self.0.recode_name(utf8_b::encode))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mailboxes_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailboxes);
|
||||
|
||||
impl MailboxesHeader for $type_name {
|
||||
fn join_mailboxes(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.map($type_name)
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
format_mailboxes(self.0.iter(), f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mailbox_header! {
|
||||
/**
|
||||
|
||||
`Sender` header
|
||||
|
||||
This header contains [`Mailbox`](::Mailbox) associated with sender.
|
||||
|
||||
```no_test
|
||||
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
|
||||
```
|
||||
*/
|
||||
(Sender, "Sender")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`From` header
|
||||
|
||||
This header contains [`Mailboxes`](::Mailboxes).
|
||||
|
||||
*/
|
||||
(From, "From")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Reply-To` header
|
||||
|
||||
This header contains [`Mailboxes`](::Mailboxes).
|
||||
|
||||
*/
|
||||
(ReplyTo, "Reply-To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`To` header
|
||||
|
||||
This header contains [`Mailboxes`](::Mailboxes).
|
||||
|
||||
*/
|
||||
(To, "To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Cc` header
|
||||
|
||||
This header contains [`Mailboxes`](::Mailboxes).
|
||||
|
||||
*/
|
||||
(Cc, "Cc")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Bcc` header
|
||||
|
||||
This header contains [`Mailboxes`](::Mailboxes).
|
||||
|
||||
*/
|
||||
(Bcc, "Bcc")
|
||||
}
|
||||
|
||||
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Ok(mbs) = src.parse() {
|
||||
return Ok(mbs);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
}
|
||||
|
||||
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&Mailboxes::from(
|
||||
mbs.map(|mb| mb.recode_name(utf8_b::encode))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{From, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_single_without_name() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_name() {
|
||||
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_without_name() {
|
||||
let from = Mailboxes::new()
|
||||
.with("kayo@example.com".parse().unwrap())
|
||||
.with("pony@domain.tld".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: kayo@example.com, pony@domain.tld\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_with_name() {
|
||||
let from = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_utf8_name() {
|
||||
let from = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_without_name() {
|
||||
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_name() {
|
||||
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_without_name() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
"pony@domain.tld".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_utf8_name() {
|
||||
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
}
|
||||
17
src/message/header/mod.rs
Normal file
17
src/message/header/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
|
||||
## Headers widely used in email messages
|
||||
|
||||
*/
|
||||
|
||||
mod content;
|
||||
mod mailbox;
|
||||
mod special;
|
||||
mod textual;
|
||||
|
||||
pub use self::{content::*, mailbox::*, special::*, textual::*};
|
||||
|
||||
pub use hyperx::header::{
|
||||
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
|
||||
DispositionType, Header, Headers, HttpDate as EmailDate,
|
||||
};
|
||||
86
src/message/header/special.rs
Normal file
86
src/message/header/special.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MimeVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
|
||||
|
||||
impl MimeVersion {
|
||||
pub fn new(major: u8, minor: u8) -> Self {
|
||||
MimeVersion { major, minor }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeVersion {
|
||||
fn default() -> Self {
|
||||
MIME_VERSION_1_0
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn header_name() -> &'static str {
|
||||
"MIME-Version"
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one().ok_or(HeaderError::Header).and_then(|r| {
|
||||
let s: Vec<&str> = from_utf8(r)
|
||||
.map_err(|_| HeaderError::Header)?
|
||||
.split('.')
|
||||
.collect();
|
||||
if s.len() != 2 {
|
||||
return Err(HeaderError::Header);
|
||||
}
|
||||
let major = s[0].parse().map_err(|_| HeaderError::Header)?;
|
||||
let minor = s[1].parse().map_err(|_| HeaderError::Header)?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&format!("{}.{}", self.major, self.minor))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{MimeVersion, MIME_VERSION_1_0};
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(MIME_VERSION_1_0);
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
|
||||
|
||||
headers.set(MimeVersion::new(0, 1));
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("MIME-Version", "1.0");
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
|
||||
|
||||
headers.set_raw("MIME-Version", "0.1");
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
|
||||
}
|
||||
}
|
||||
105
src/message/header/textual.rs
Normal file
105
src/message/header/textual.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use crate::message::utf8_b;
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
macro_rules! text_header {
|
||||
( $type_name: ident, $header_name: expr ) => {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub String);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_text)
|
||||
.map($type_name)
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
fmt_text(&self.0, f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
text_header!(Subject, "Subject");
|
||||
text_header!(Comments, "Comments");
|
||||
text_header!(Keywords, "Keywords");
|
||||
text_header!(InReplyTo, "In-Reply-To");
|
||||
text_header!(References, "References");
|
||||
text_header!(MessageId, "Message-Id");
|
||||
text_header!(UserAgent, "User-Agent");
|
||||
|
||||
fn parse_text(raw: &[u8]) -> HyperResult<String> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Some(txt) = utf8_b::decode(src) {
|
||||
return Ok(txt);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
}
|
||||
|
||||
fn fmt_text(s: &str, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&utf8_b::encode(s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Subject;
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Sample subject".into()));
|
||||
|
||||
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Тема сообщения".into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("Subject", "Sample subject");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw(
|
||||
"Subject",
|
||||
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Тема сообщения".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
5
src/message/mailbox/mod.rs
Normal file
5
src/message/mailbox/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
mod types;
|
||||
|
||||
pub use self::types::*;
|
||||
215
src/message/mailbox/serde.rs
Normal file
215
src/message/mailbox/serde.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
impl Serialize for Mailbox {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mailbox {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
Name,
|
||||
Email,
|
||||
};
|
||||
|
||||
const FIELDS: &[&str] = &["name", "email"];
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("'name' or 'email'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"name" => Ok(Field::Name),
|
||||
"email" => Ok(Field::Email),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct MailboxVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MailboxVisitor {
|
||||
type Value = Mailbox;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("mailbox string or object")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut name = None;
|
||||
let mut addr = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::Name => {
|
||||
if name.is_some() {
|
||||
return Err(DeError::duplicate_field("name"));
|
||||
}
|
||||
name = Some(map.next_value()?);
|
||||
}
|
||||
Field::Email => {
|
||||
if addr.is_some() {
|
||||
return Err(DeError::duplicate_field("email"));
|
||||
}
|
||||
addr = Some(map.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
let addr = addr.ok_or_else(|| DeError::missing_field("email"))?;
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(MailboxVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Mailboxes {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mailboxes {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct MailboxesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MailboxesVisitor {
|
||||
type Value = Mailboxes;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("mailboxes string or sequence")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: SeqAccess<'de>,
|
||||
{
|
||||
let mut mboxes = Mailboxes::new();
|
||||
while let Some(mbox) = seq.next_element()? {
|
||||
mboxes.push(mbox);
|
||||
}
|
||||
Ok(mboxes)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(MailboxesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::address::Address;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[test]
|
||||
fn parse_address_string() {
|
||||
let m: Address = from_str(r#""kayo@example.com""#).unwrap();
|
||||
assert_eq!(m, "kayo@example.com".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_object() {
|
||||
let m: Address = from_str(r#"{ "user": "kayo", "domain": "example.com" }"#).unwrap();
|
||||
assert_eq!(m, "kayo@example.com".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_string() {
|
||||
let m: Mailbox = from_str(r#""Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_stirng() {
|
||||
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_object() {
|
||||
let m: Mailbox =
|
||||
from_str(r#"{ "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }"#)
|
||||
.unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailboxes_string() {
|
||||
let m: Mailboxes =
|
||||
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailboxes_array() {
|
||||
let m: Mailboxes =
|
||||
from_str(r#"["yin@dtb.com", { "name": "Hei", "email": "hei@dtb.com" }, { "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }]"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
325
src/message/mailbox/types.rs
Normal file
325
src/message/mailbox/types.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use crate::{
|
||||
address::{Address, AddressError},
|
||||
message::utf8_b,
|
||||
};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Email address with optional addressee name
|
||||
///
|
||||
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailbox {
|
||||
/// User name part
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Email address part
|
||||
pub email: Address,
|
||||
}
|
||||
|
||||
impl Mailbox {
|
||||
/// Create new mailbox using email address and addressee name
|
||||
#[inline]
|
||||
pub fn new(name: Option<String>, email: Address) -> Self {
|
||||
Mailbox { name, email }
|
||||
}
|
||||
|
||||
/// Encode addressee name using function
|
||||
pub(crate) fn recode_name<F>(&self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(&str) -> String,
|
||||
{
|
||||
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailbox {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
if let Some(ref name) = self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
f.write_str(&name)?;
|
||||
f.write_str(" <")?;
|
||||
self.email.fmt(f)?;
|
||||
return f.write_char('>');
|
||||
}
|
||||
}
|
||||
self.email.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
||||
let (name, address) = header;
|
||||
Ok(Mailbox::new(Some(name.into()), address.into().parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
||||
let (name, address) = header;
|
||||
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
|
||||
}
|
||||
}*/
|
||||
|
||||
impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
match (src.find('<'), src.find('>')) {
|
||||
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
|
||||
let name = src.split_at(addr_open).0;
|
||||
let addr_open = addr_open + 1;
|
||||
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
|
||||
let addr = addr.parse()?;
|
||||
let name = name.trim();
|
||||
let name = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.into())
|
||||
};
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
(Some(_), _) => Err(AddressError::Unbalanced),
|
||||
_ => {
|
||||
let addr = src.parse()?;
|
||||
Ok(Mailbox::new(None, addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List or email mailboxes
|
||||
///
|
||||
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailboxes(Vec<Mailbox>);
|
||||
|
||||
impl Mailboxes {
|
||||
/// Create mailboxes list
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Mailboxes(Vec::new())
|
||||
}
|
||||
|
||||
/// Add mailbox to a list
|
||||
#[inline]
|
||||
pub fn with(mut self, mbox: Mailbox) -> Self {
|
||||
self.0.push(mbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to a list
|
||||
#[inline]
|
||||
pub fn push(&mut self, mbox: Mailbox) {
|
||||
self.0.push(mbox);
|
||||
}
|
||||
|
||||
/// Extract first mailbox
|
||||
#[inline]
|
||||
pub fn into_single(self) -> Option<Mailbox> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Iterate over mailboxes
|
||||
#[inline]
|
||||
pub fn iter(&self) -> Iter<Mailbox> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mailboxes {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mailbox> for Mailboxes {
|
||||
fn from(single: Mailbox) -> Self {
|
||||
Mailboxes(vec![single])
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Option<Mailbox>> for Mailboxes {
|
||||
fn into(self) -> Option<Mailbox> {
|
||||
self.into_iter().next()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Mailbox>> for Mailboxes {
|
||||
fn from(list: Vec<Mailbox>) -> Self {
|
||||
Mailboxes(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<Mailbox>> for Mailboxes {
|
||||
fn into(self) -> Vec<Mailbox> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Mailboxes {
|
||||
type Item = Mailbox;
|
||||
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Mailbox> for Mailboxes {
|
||||
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
|
||||
for elem in iter {
|
||||
self.0.push(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailboxes {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
let mut iter = self.iter();
|
||||
|
||||
if let Some(mbox) = iter.next() {
|
||||
mbox.fmt(f)?;
|
||||
|
||||
for mbox in iter {
|
||||
f.write_str(", ")?;
|
||||
mbox.fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mailboxes {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
src.split(',')
|
||||
.map(|m| {
|
||||
m.trim().parse().and_then(|Mailbox { name, email }| {
|
||||
if let Some(name) = name {
|
||||
if let Some(name) = utf8_b::decode(&name) {
|
||||
Ok(Mailbox::new(Some(name), email))
|
||||
} else {
|
||||
Err(AddressError::InvalidUtf8b)
|
||||
}
|
||||
} else {
|
||||
Ok(Mailbox::new(None, email))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mailbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_only() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(None, "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"K. <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_address_with_name_trim() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"K. <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_only() {
|
||||
assert_eq!(
|
||||
"kayo@example.com".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_name() {
|
||||
assert_eq!(
|
||||
"K. <kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
"<kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_empty_name_trim() {
|
||||
assert_eq!(
|
||||
" <kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_from_tuple() {
|
||||
assert_eq!(
|
||||
("K.".to_string(), "kayo@example.com".to_string()).try_into(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
636
src/message/mimebody.rs
Normal file
636
src/message/mimebody.rs
Normal file
@@ -0,0 +1,636 @@
|
||||
use crate::message::{
|
||||
encoder::codec,
|
||||
header::{ContentTransferEncoding, ContentType, Header, Headers},
|
||||
};
|
||||
use bytes::{Bytes, IntoBuf};
|
||||
use mime::Mime;
|
||||
use std::{
|
||||
fmt::{Display, Error as FmtError, Formatter, Result as FmtResult},
|
||||
str::from_utf8,
|
||||
};
|
||||
use textnonce::TextNonce;
|
||||
|
||||
/// MIME part variants
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part<B = Bytes> {
|
||||
/// Single part with content
|
||||
///
|
||||
Single(SinglePart<B>),
|
||||
|
||||
/// Multiple parts of content
|
||||
///
|
||||
Multi(MultiPart<B>),
|
||||
}
|
||||
|
||||
impl<B> Display for Part<B>
|
||||
where
|
||||
B: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Part::Single(ref part) => part.fmt(f),
|
||||
Part::Multi(ref part) => part.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parts of multipart body
|
||||
///
|
||||
pub type Parts<B = Bytes> = Vec<Part<B>>;
|
||||
|
||||
/// Creates builder for single part
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePartBuilder {
|
||||
headers: Headers,
|
||||
}
|
||||
|
||||
impl SinglePartBuilder {
|
||||
/// Creates a default singlepart builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the header to singlepart
|
||||
#[inline]
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build singlepart using body
|
||||
#[inline]
|
||||
pub fn body<T>(self, body: T) -> SinglePart<T> {
|
||||
SinglePart {
|
||||
headers: self.headers,
|
||||
body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SinglePartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Single part
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_test
|
||||
/// extern crate mime;
|
||||
/// extern crate emailmessage;
|
||||
///
|
||||
/// use emailmessage::{SinglePart, header};
|
||||
///
|
||||
/// let part = SinglePart::builder()
|
||||
/// .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
/// .header(header::ContentTransferEncoding::Binary)
|
||||
/// .body("Текст письма в уникоде");
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePart<B = Bytes> {
|
||||
headers: Headers,
|
||||
body: B,
|
||||
}
|
||||
|
||||
impl SinglePart<()> {
|
||||
/// Creates a default builder for singlepart
|
||||
pub fn builder() -> SinglePartBuilder {
|
||||
SinglePartBuilder::new()
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with 7bit encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
|
||||
pub fn seven_bit() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::SevenBit)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with quoted-printable encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::QuotedPrintable)`.
|
||||
pub fn quoted_printable() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with base64 encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Base64)`.
|
||||
pub fn base64() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::Base64)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with 8-bit encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
|
||||
#[inline]
|
||||
pub fn eight_bit() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::EightBit)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with binary encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
|
||||
#[inline]
|
||||
pub fn binary() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::Binary)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> SinglePart<B> {
|
||||
/// Get the transfer encoding
|
||||
#[inline]
|
||||
pub fn encoding(&self) -> Option<&ContentTransferEncoding> {
|
||||
self.headers.get()
|
||||
}
|
||||
|
||||
/// Get the headers from singlepart
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
#[inline]
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Read the body from singlepart
|
||||
#[inline]
|
||||
pub fn body_ref(&self) -> &B {
|
||||
&self.body
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Display for SinglePart<B>
|
||||
where
|
||||
B: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
self.headers.fmt(f)?;
|
||||
"\r\n".fmt(f)?;
|
||||
|
||||
let body = self.body.as_ref();
|
||||
let mut encoder = codec(self.encoding());
|
||||
let result = encoder
|
||||
.encode_all(&body.into_buf())
|
||||
.map_err(|_| FmtError::default())?;
|
||||
let body = from_utf8(&result).map_err(|_| FmtError::default())?;
|
||||
|
||||
body.fmt(f)?;
|
||||
"\r\n".fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of multipart
|
||||
///
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MultiPartKind {
|
||||
/// Mixed kind to combine unrelated content parts
|
||||
///
|
||||
/// For example this kind can be used to mix email message and attachments.
|
||||
Mixed,
|
||||
|
||||
/// Alternative kind to join several variants of same email contents.
|
||||
///
|
||||
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
|
||||
Alternative,
|
||||
|
||||
/// Related kind to mix content and related resources.
|
||||
///
|
||||
/// For example, you can include images into HTML content using that.
|
||||
Related,
|
||||
}
|
||||
|
||||
impl MultiPartKind {
|
||||
fn to_mime<S: AsRef<str>>(self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary
|
||||
.map(|s| s.as_ref().into())
|
||||
.unwrap_or_else(|| TextNonce::sized(68).unwrap().into_string());
|
||||
|
||||
use self::MultiPartKind::*;
|
||||
format!(
|
||||
"multipart/{}; boundary=\"{}\"",
|
||||
match self {
|
||||
Mixed => "mixed",
|
||||
Alternative => "alternative",
|
||||
Related => "related",
|
||||
},
|
||||
boundary
|
||||
)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn from_mime(m: &Mime) -> Option<Self> {
|
||||
use self::MultiPartKind::*;
|
||||
match m.subtype().as_ref() {
|
||||
"mixed" => Some(Mixed),
|
||||
"alternative" => Some(Alternative),
|
||||
"related" => Some(Related),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MultiPartKind> for Mime {
|
||||
fn from(m: MultiPartKind) -> Self {
|
||||
m.to_mime::<String>(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart builder
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPartBuilder {
|
||||
headers: Headers,
|
||||
}
|
||||
|
||||
impl MultiPartBuilder {
|
||||
/// Creates default multipart builder
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a header
|
||||
#[inline]
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set `Content-Type` header using [`MultiPartKind`]
|
||||
#[inline]
|
||||
pub fn kind(self, kind: MultiPartKind) -> Self {
|
||||
self.header(ContentType(kind.into()))
|
||||
}
|
||||
|
||||
/// Set custom boundary
|
||||
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
|
||||
let kind = {
|
||||
let mime = &self.headers.get::<ContentType>().unwrap().0;
|
||||
MultiPartKind::from_mime(mime).unwrap()
|
||||
};
|
||||
let mime = kind.to_mime(Some(boundary.as_ref()));
|
||||
self.header(ContentType(mime))
|
||||
}
|
||||
|
||||
/// Creates multipart without parts
|
||||
#[inline]
|
||||
pub fn build<B>(self) -> MultiPart<B> {
|
||||
MultiPart {
|
||||
headers: self.headers,
|
||||
parts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates multipart using part
|
||||
#[inline]
|
||||
pub fn part<B>(self, part: Part<B>) -> MultiPart<B> {
|
||||
self.build().part(part)
|
||||
}
|
||||
|
||||
/// Creates multipart using singlepart
|
||||
#[inline]
|
||||
pub fn singlepart<B>(self, part: SinglePart<B>) -> MultiPart<B> {
|
||||
self.build().singlepart(part)
|
||||
}
|
||||
|
||||
/// Creates multipart using multipart
|
||||
#[inline]
|
||||
pub fn multipart<B>(self, part: MultiPart<B>) -> MultiPart<B> {
|
||||
self.build().multipart(part)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiPartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart variant with parts
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPart<B = Bytes> {
|
||||
headers: Headers,
|
||||
parts: Parts<B>,
|
||||
}
|
||||
|
||||
impl MultiPart<()> {
|
||||
/// Creates multipart builder
|
||||
#[inline]
|
||||
pub fn builder() -> MultiPartBuilder {
|
||||
MultiPartBuilder::new()
|
||||
}
|
||||
|
||||
/// Creates mixed multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
|
||||
#[inline]
|
||||
pub fn mixed() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Mixed)
|
||||
}
|
||||
|
||||
/// Creates alternative multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
|
||||
#[inline]
|
||||
pub fn alternative() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Alternative)
|
||||
}
|
||||
|
||||
/// Creates related multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
|
||||
#[inline]
|
||||
pub fn related() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Related)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MultiPart<B> {
|
||||
/// Add part to multipart
|
||||
#[inline]
|
||||
pub fn part(mut self, part: Part<B>) -> Self {
|
||||
self.parts.push(part);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add single part to multipart
|
||||
#[inline]
|
||||
pub fn singlepart(mut self, part: SinglePart<B>) -> Self {
|
||||
self.parts.push(Part::Single(part));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multi part to multipart
|
||||
#[inline]
|
||||
pub fn multipart(mut self, part: MultiPart<B>) -> Self {
|
||||
self.parts.push(Part::Multi(part));
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the boundary of multipart contents
|
||||
#[inline]
|
||||
pub fn boundary(&self) -> String {
|
||||
let content_type = &self.headers.get::<ContentType>().unwrap().0;
|
||||
content_type.get_param("boundary").unwrap().as_str().into()
|
||||
}
|
||||
|
||||
/// Get the headers from the multipart
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
#[inline]
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get the parts from the multipart
|
||||
#[inline]
|
||||
pub fn parts(&self) -> &Parts<B> {
|
||||
&self.parts
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the parts
|
||||
#[inline]
|
||||
pub fn parts_mut(&mut self) -> &mut Parts<B> {
|
||||
&mut self.parts
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Display for MultiPart<B>
|
||||
where
|
||||
B: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
self.headers.fmt(f)?;
|
||||
"\r\n".fmt(f)?;
|
||||
|
||||
let boundary = self.boundary();
|
||||
|
||||
for part in &self.parts {
|
||||
"--".fmt(f)?;
|
||||
boundary.fmt(f)?;
|
||||
"\r\n".fmt(f)?;
|
||||
part.fmt(f)?;
|
||||
}
|
||||
|
||||
"--".fmt(f)?;
|
||||
boundary.fmt(f)?;
|
||||
"--\r\n".fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{MultiPart, Part, SinglePart};
|
||||
use crate::message::header;
|
||||
|
||||
#[test]
|
||||
fn single_part_binary() {
|
||||
let part: SinglePart<String> = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", part),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_part_quoted_printable() {
|
||||
let part: SinglePart<String> = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", part),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
"\r\n",
|
||||
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
|
||||
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_part_base64() {
|
||||
let part: SinglePart<String> = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", part),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed() {
|
||||
let part: MultiPart<String> = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"example.c".as_bytes().into(),
|
||||
)],
|
||||
})
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")),
|
||||
);
|
||||
|
||||
assert_eq!(format!("{}", part),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_alternative() {
|
||||
let part: MultiPart<String> = MultiPart::alternative()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
||||
|
||||
assert_eq!(format!("{}", part),
|
||||
concat!("Content-Type: multipart/alternative;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed_related() {
|
||||
let part: MultiPart<String> = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.multipart(MultiPart::related()
|
||||
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("image/png".parse().unwrap()))
|
||||
.header(header::ContentLocation("/image.png".into()))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".as_bytes().into())]
|
||||
})
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")));
|
||||
|
||||
assert_eq!(format!("{}", part),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: multipart/related;",
|
||||
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
|
||||
"\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"Content-Type: image/png\r\n",
|
||||
"Content-Location: /image.png\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
|
||||
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
|
||||
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
}
|
||||
329
src/message/mod.rs
Normal file
329
src/message/mod.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
//! Provides a strongly typed way to build emails
|
||||
|
||||
pub use encoder::*;
|
||||
pub use mailbox::*;
|
||||
pub use mimebody::*;
|
||||
|
||||
pub use mime;
|
||||
|
||||
mod encoder;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
mod mimebody;
|
||||
mod utf8_b;
|
||||
|
||||
use crate::{
|
||||
message::header::{EmailDate, Header, Headers, MailboxesHeader},
|
||||
Envelope, Error as EmailError,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
time::SystemTime,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
|
||||
|
||||
/// A builder for messages
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageBuilder {
|
||||
headers: Headers,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
/// Creates a new default message builder
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom header to message
|
||||
#[inline]
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to header
|
||||
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
|
||||
if self.headers.has::<H>() {
|
||||
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
|
||||
self
|
||||
} else {
|
||||
self.header(header)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date(date))`.
|
||||
#[inline]
|
||||
pub fn date(self, date: EmailDate) -> Self {
|
||||
self.header(header::Date(date))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`.
|
||||
#[inline]
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now().into())
|
||||
}
|
||||
|
||||
/// Set `Subject` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
||||
#[inline]
|
||||
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
||||
self.header(header::Subject(subject.into()))
|
||||
}
|
||||
|
||||
/// Set `Mime-Version` header to 1.0
|
||||
///
|
||||
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
|
||||
#[inline]
|
||||
pub fn mime_1_0(self) -> Self {
|
||||
self.header(header::MIME_VERSION_1_0)
|
||||
}
|
||||
|
||||
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
///
|
||||
/// Shortcut for `self.header(header::Sender(mbox))`.
|
||||
#[inline]
|
||||
pub fn sender(self, mbox: Mailbox) -> Self {
|
||||
self.header(header::Sender(mbox))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `From` header
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::From(mbox))`.
|
||||
#[inline]
|
||||
pub fn from(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::From(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
|
||||
#[inline]
|
||||
pub fn reply_to(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::ReplyTo(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `To` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::To(mbox))`.
|
||||
#[inline]
|
||||
pub fn to(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::To(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `Cc` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::Cc(mbox))`.
|
||||
#[inline]
|
||||
pub fn cc(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::Cc(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `Bcc` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::Bcc(mbox))`.
|
||||
#[inline]
|
||||
pub fn bcc(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::Bcc(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add message id to [`In-Reply-To`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
#[inline]
|
||||
pub fn in_reply_to(self, id: String) -> Self {
|
||||
self.header(header::InReplyTo(id))
|
||||
}
|
||||
|
||||
/// Set or add message id to [`References`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
#[inline]
|
||||
pub fn references(self, id: String) -> Self {
|
||||
self.header(header::References(id))
|
||||
}
|
||||
|
||||
/// Set [Message-Id
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
///
|
||||
/// Should generally be inserted by the mail relay.
|
||||
///
|
||||
/// If `None` is provided, an id will be generated in the
|
||||
/// `<UUID@HOSTNAME>`.
|
||||
#[inline]
|
||||
pub fn message_id(self, id: Option<String>) -> Self {
|
||||
match id {
|
||||
Some(i) => self.header(header::MessageId(i)),
|
||||
None => {
|
||||
#[cfg(feature = "hostname")]
|
||||
let hostname = hostname::get()
|
||||
.map_err(|_| ())
|
||||
.and_then(|s| s.into_string().map_err(|_| ()))
|
||||
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
||||
|
||||
self.header(header::MessageId(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
format!("<{}@{}>", Uuid::new_v4(), hostname),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set [User-Agent
|
||||
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
|
||||
#[inline]
|
||||
pub fn user_agent(self, id: String) -> Self {
|
||||
self.header(header::UserAgent(id))
|
||||
}
|
||||
|
||||
fn insert_missing_headers(self) -> Self {
|
||||
// Insert Date if missing
|
||||
if self.headers.get::<header::Date>().is_none() {
|
||||
self.date_now()
|
||||
} else {
|
||||
self
|
||||
}
|
||||
// TODO insert sender if needed?
|
||||
}
|
||||
|
||||
// TODO: High-level methods for attachments and embedded files
|
||||
|
||||
/// Create message by joining content
|
||||
#[inline]
|
||||
fn build<T>(self, body: T, split: bool) -> Result<Message<T>, EmailError> {
|
||||
let res = self.insert_missing_headers();
|
||||
let envelope = Envelope::try_from(&res.headers)?;
|
||||
Ok(Message {
|
||||
headers: res.headers,
|
||||
split,
|
||||
body,
|
||||
envelope,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create message using body
|
||||
#[inline]
|
||||
pub fn body<T>(self, body: T) -> Result<Message<T>, EmailError> {
|
||||
self.build(body, true)
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`MultiPart`](::MultiPart) or [`SinglePart`](::SinglePart))
|
||||
// FIXME restrict usage on MIME?
|
||||
#[inline]
|
||||
pub fn mime_body<T>(self, body: T) -> Result<Message<T>, EmailError> {
|
||||
self.mime_1_0().build(body, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message which can be formatted
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message<B = Bytes> {
|
||||
headers: Headers,
|
||||
split: bool,
|
||||
body: B,
|
||||
envelope: Envelope,
|
||||
}
|
||||
|
||||
impl Message<()> {
|
||||
/// Create a new message builder without headers
|
||||
#[inline]
|
||||
pub fn builder() -> MessageBuilder {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Message<B> {
|
||||
/// Get the headers from the Message
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Read the body
|
||||
#[inline]
|
||||
pub fn body_ref(&self) -> &B {
|
||||
&self.body
|
||||
}
|
||||
|
||||
/// Try to extract envelope data from `Message` headers
|
||||
#[inline]
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MessageBuilder {
|
||||
fn default() -> Self {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Display for Message<B>
|
||||
where
|
||||
B: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
self.headers.fmt(f)?;
|
||||
if self.split {
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
self.body.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
// An email is Message + Envelope
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::message::{header, mailbox::Mailbox, Message};
|
||||
|
||||
#[test]
|
||||
fn email_message() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
)]
|
||||
.into(),
|
||||
))
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject("яңа ел белән!".into()))
|
||||
.body("Happy new year!")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", email),
|
||||
concat!(
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||
"To: Pony O.P. <pony@domain.tld>\r\n",
|
||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||
"\r\n",
|
||||
"Happy new year!"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
67
src/message/utf8_b.rs
Normal file
67
src/message/utf8_b.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::str::from_utf8;
|
||||
|
||||
fn allowed_char(c: char) -> bool {
|
||||
c >= 1 as char && c <= 9 as char
|
||||
|| c == 11 as char
|
||||
|| c == 12 as char
|
||||
|| c >= 14 as char && c <= 127 as char
|
||||
}
|
||||
|
||||
pub fn encode(s: &str) -> String {
|
||||
if s.chars().all(allowed_char) {
|
||||
s.into()
|
||||
} else {
|
||||
format!("=?utf-8?b?{}?=", base64::encode(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(s: &str) -> Option<String> {
|
||||
let s = s.trim();
|
||||
if s.starts_with("=?utf-8?b?") && s.ends_with("?=") {
|
||||
let s = s.split_at(10).1;
|
||||
let s = s.split_at(s.len() - 2).0;
|
||||
base64::decode(s)
|
||||
.map_err(|_| ())
|
||||
.and_then(|v| {
|
||||
if let Ok(s) = from_utf8(&v) {
|
||||
Ok(Some(s.into()))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
})
|
||||
.unwrap_or(None)
|
||||
} else {
|
||||
Some(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode, encode};
|
||||
|
||||
#[test]
|
||||
fn encode_ascii() {
|
||||
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ascii() {
|
||||
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_utf8() {
|
||||
assert_eq!(
|
||||
&encode("Привет, мир!"),
|
||||
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8() {
|
||||
assert_eq!(
|
||||
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
|
||||
Some("Привет, мир!".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use self::Error::*;
|
||||
use serde_json;
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
@@ -21,20 +20,16 @@ pub enum Error {
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
match *self {
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
JsonSerialization(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
JsonSerialization(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
JsonSerialization(ref err) => Some(&*err),
|
||||
@@ -61,5 +56,7 @@ impl From<&'static str> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
type Id = String;
|
||||
|
||||
/// SMTP result type
|
||||
pub type FileResult = Result<(), Error>;
|
||||
pub type FileResult = Result<Id, Error>;
|
||||
@@ -3,14 +3,14 @@
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
|
||||
use crate::file::error::FileResult;
|
||||
use crate::Email;
|
||||
use crate::Envelope;
|
||||
use crate::Transport;
|
||||
use serde_json;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::{transport::file::error::FileResult, Envelope, Message, Transport};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
fs::File,
|
||||
io::prelude::*,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -34,29 +34,27 @@ impl FileTransport {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
struct SerializableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for FileTransport {
|
||||
impl<'a, B> Transport<'a, B> for FileTransport {
|
||||
type Result = FileResult;
|
||||
|
||||
fn send<E: Into<Email>>(&mut self, email: E) -> FileResult {
|
||||
let email = email.into();
|
||||
|
||||
let message_id = email.message_id().to_string();
|
||||
let envelope = email.envelope().clone();
|
||||
fn send(&mut self, email: Message<B>) -> Self::Result
|
||||
where
|
||||
B: Display,
|
||||
{
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let mut file = self.path.clone();
|
||||
file.push(format!("{}.json", message_id));
|
||||
file.push(format!("{}.json", email_id));
|
||||
|
||||
let serialized = serde_json::to_string(&SerializableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: email.message_to_string()?.as_bytes().to_vec(),
|
||||
envelope: email.envelope().clone(),
|
||||
message: email.to_string().into_bytes(),
|
||||
})?;
|
||||
|
||||
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
|
||||
Ok(())
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
7
src/transport/mod.rs
Normal file
7
src/transport/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
@@ -13,7 +13,7 @@ use std::{
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(String),
|
||||
/// Error parsing UTF8in response
|
||||
/// Error parsing UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
@@ -21,20 +21,16 @@ pub enum Error {
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
match *self {
|
||||
Client(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(ref err) => err,
|
||||
Utf8Parsing(ref err) => err.description(),
|
||||
Io(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
@@ -1,14 +1,15 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
|
||||
use crate::sendmail::error::SendmailResult;
|
||||
use crate::Email;
|
||||
use crate::Transport;
|
||||
use crate::{transport::sendmail::error::SendmailResult, Message, Transport};
|
||||
use log::info;
|
||||
use std::convert::AsRef;
|
||||
use std::io::prelude::*;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{
|
||||
convert::AsRef,
|
||||
fmt::Display,
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -35,34 +36,38 @@ impl SendmailTransport {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SendmailTransport {
|
||||
impl<'a, B> Transport<'a, B> for SendmailTransport {
|
||||
type Result = SendmailResult;
|
||||
|
||||
fn send<E: Into<Email>>(&mut self, email: E) -> SendmailResult {
|
||||
let email = email.into();
|
||||
|
||||
let message_id = email.message_id().to_string();
|
||||
fn send(&mut self, email: Message<B>) -> Self::Result
|
||||
where
|
||||
B: Display,
|
||||
{
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = Command::new(&self.command)
|
||||
.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(email.envelope().from().map(AsRef::as_ref).unwrap_or("\"\""))
|
||||
.args(email.envelope.to())
|
||||
.arg(
|
||||
email
|
||||
.envelope()
|
||||
.from()
|
||||
.map(|f| f.as_ref())
|
||||
.unwrap_or("\"\""),
|
||||
)
|
||||
.args(email.envelope().to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(message_content.as_bytes())?;
|
||||
.write_all(email.to_string().as_bytes())?;
|
||||
|
||||
info!("Wrote {} message to stdin", message_id);
|
||||
info!("Wrote {} message to stdin", email_id);
|
||||
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use crate::smtp::error::Error;
|
||||
use crate::transport::smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms on an encrypted connection
|
||||
@@ -1,8 +1,10 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::io::{self, Cursor, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub type MockCursor = Cursor<Vec<u8>>;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
//! SMTP client
|
||||
|
||||
use crate::smtp::authentication::{Credentials, Mechanism};
|
||||
use crate::smtp::client::net::ClientTlsParameters;
|
||||
use crate::smtp::client::net::{Connector, NetworkStream, Timeout};
|
||||
use crate::smtp::commands::*;
|
||||
use crate::smtp::error::{Error, SmtpResult};
|
||||
use crate::smtp::response::Response;
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout},
|
||||
commands::*,
|
||||
error::{Error, SmtpResult},
|
||||
response::Response,
|
||||
};
|
||||
use bufstream::BufStream;
|
||||
use log::debug;
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::string::String;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, BufRead, Read, Write},
|
||||
net::ToSocketAddrs,
|
||||
string::String,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub mod mock;
|
||||
pub mod net;
|
||||
@@ -33,7 +36,6 @@ impl ClientCodec {
|
||||
}
|
||||
|
||||
/// Adds transparency
|
||||
/// TODO: replace CR and LF by CRLF
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
@@ -174,7 +176,7 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
|
||||
// TODO
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
@@ -195,31 +197,11 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
|
||||
pub fn message(&mut self, message: &[u8]) -> SmtpResult {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
|
||||
let mut message_reader = BufReader::new(message);
|
||||
|
||||
loop {
|
||||
out_buf.clear();
|
||||
|
||||
let consumed = match message_reader.fill_buf() {
|
||||
Ok(bytes) => {
|
||||
codec.encode(bytes, &mut out_buf)?;
|
||||
bytes.len()
|
||||
}
|
||||
Err(ref err) => panic!("Failed with: {}", err),
|
||||
};
|
||||
message_reader.consume(consumed);
|
||||
|
||||
if consumed == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
self.write(out_buf.as_slice())?;
|
||||
}
|
||||
|
||||
codec.encode(message, &mut out_buf)?;
|
||||
self.write(out_buf.as_slice())?;
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
self.read_response()
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use crate::smtp::client::mock::MockStream;
|
||||
use crate::smtp::error::Error;
|
||||
use crate::transport::smtp::{client::mock::MockStream, error::Error};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{TlsConnector, TlsStream};
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::{ClientConfig, ClientSession};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use std::io::ErrorKind;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
|
||||
#[cfg(feature = "rustls")]
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
@@ -1,15 +1,19 @@
|
||||
//! SMTP commands
|
||||
|
||||
use crate::smtp::authentication::{Credentials, Mechanism};
|
||||
use crate::smtp::error::Error;
|
||||
use crate::smtp::extension::ClientId;
|
||||
use crate::smtp::extension::{MailParameter, RcptParameter};
|
||||
use crate::smtp::response::Response;
|
||||
use crate::EmailAddress;
|
||||
use base64;
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
error::Error,
|
||||
extension::{ClientId, MailParameter, RcptParameter},
|
||||
response::Response,
|
||||
},
|
||||
Address,
|
||||
};
|
||||
use log::debug;
|
||||
use std::convert::AsRef;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::{
|
||||
convert::AsRef,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
@@ -47,7 +51,7 @@ impl Display for StarttlsCommand {
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MailCommand {
|
||||
sender: Option<EmailAddress>,
|
||||
sender: Option<Address>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
@@ -56,7 +60,7 @@ impl Display for MailCommand {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(AsRef::as_ref).unwrap_or("")
|
||||
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
@@ -67,7 +71,7 @@ impl Display for MailCommand {
|
||||
|
||||
impl MailCommand {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
MailCommand { sender, parameters }
|
||||
}
|
||||
}
|
||||
@@ -76,7 +80,7 @@ impl MailCommand {
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct RcptCommand {
|
||||
recipient: EmailAddress,
|
||||
recipient: Address,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
@@ -92,7 +96,7 @@ impl Display for RcptCommand {
|
||||
|
||||
impl RcptCommand {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
RcptCommand {
|
||||
recipient,
|
||||
parameters,
|
||||
@@ -247,7 +251,7 @@ impl AuthCommand {
|
||||
challenge: Option<String>,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
|
||||
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -292,12 +296,13 @@ impl AuthCommand {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::smtp::extension::MailBodyParameter;
|
||||
use crate::transport::smtp::extension::MailBodyParameter;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_string());
|
||||
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
|
||||
let email = Address::from_str("test@example.com").unwrap();
|
||||
let mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
@@ -1,16 +1,16 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use crate::smtp::response::{Response, Severity};
|
||||
use crate::transport::smtp::response::{Response, Severity};
|
||||
use base64::DecodeError;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls;
|
||||
use nom;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
fmt::{Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
@@ -46,40 +46,36 @@ pub enum Error {
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
match *self {
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref err) => fmt.write_str(match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "transient error during SMTP transaction",
|
||||
}),
|
||||
Permanent(ref err) => fmt.write_str(match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "permanent error during SMTP transaction",
|
||||
}),
|
||||
ResponseParsing(err) => fmt.write_str(err),
|
||||
ChallengeParsing(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Resolution => fmt.write_str("could not resolve hostname"),
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => err.fmt(fmt),
|
||||
Parsing(ref err) => fmt.write_str(err.description()),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref err) => match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed transient error during SMTP transaction",
|
||||
},
|
||||
Permanent(ref err) => match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed permanent error during SMTP transaction",
|
||||
},
|
||||
ResponseParsing(err) => err,
|
||||
ChallengeParsing(ref err) => err.description(),
|
||||
Utf8Parsing(ref err) => err.description(),
|
||||
Resolution => "could not resolve hostname",
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => err.description(),
|
||||
Parsing(ref err) => err.description(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
ChallengeParsing(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
@@ -1,13 +1,14 @@
|
||||
//! ESMTP features
|
||||
|
||||
use crate::smtp::authentication::Mechanism;
|
||||
use crate::smtp::error::Error;
|
||||
use crate::smtp::response::Response;
|
||||
use crate::smtp::util::XText;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::result::Result;
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism, error::Error, response::Response, util::XText,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{self, Display, Formatter},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
result::Result,
|
||||
};
|
||||
|
||||
/// Default client id
|
||||
const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
|
||||
@@ -42,6 +43,7 @@ impl ClientId {
|
||||
|
||||
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
|
||||
/// found
|
||||
#[cfg(feature = "hostname")]
|
||||
pub fn hostname() -> ClientId {
|
||||
ClientId::Domain(
|
||||
hostname::get()
|
||||
@@ -264,8 +266,10 @@ impl Display for RcptParameter {
|
||||
mod test {
|
||||
|
||||
use super::{ClientId, Extension, ServerInfo};
|
||||
use crate::smtp::authentication::Mechanism;
|
||||
use crate::smtp::response::{Category, Code, Detail, Response, Severity};
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism,
|
||||
response::{Category, Code, Detail, Response, Severity},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
@@ -13,22 +13,29 @@
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
|
||||
use crate::smtp::authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
},
|
||||
client::{net::ClientTlsParameters, InnerClient},
|
||||
commands::*,
|
||||
error::{Error, SmtpResult},
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
},
|
||||
Message, Transport,
|
||||
};
|
||||
use crate::smtp::client::net::ClientTlsParameters;
|
||||
use crate::smtp::client::InnerClient;
|
||||
use crate::smtp::commands::*;
|
||||
use crate::smtp::error::{Error, SmtpResult};
|
||||
use crate::smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
|
||||
use crate::{Email, Transport};
|
||||
use log::{debug, info};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::ClientConfig;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
@@ -130,7 +137,10 @@ impl SmtpClient {
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
||||
#[cfg(feature = "hostname")]
|
||||
hello_name: ClientId::hostname(),
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
hello_name: ClientId::new("localhost".to_string()),
|
||||
authentication_mechanism: None,
|
||||
force_set_auth: false,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
@@ -420,7 +430,7 @@ impl<'a> SmtpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SmtpTransport {
|
||||
impl<'a, B> Transport<'a, B> for SmtpTransport {
|
||||
type Result = SmtpResult;
|
||||
|
||||
/// Sends an email
|
||||
@@ -428,10 +438,12 @@ impl<'a> Transport<'a> for SmtpTransport {
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
||||
)]
|
||||
fn send<E: Into<Email>>(&mut self, email: E) -> SmtpResult {
|
||||
let email = email.into();
|
||||
|
||||
let message_id = email.message_id().to_string();
|
||||
fn send(&mut self, email: Message<B>) -> Self::Result
|
||||
where
|
||||
B: Display,
|
||||
{
|
||||
let email_id = Uuid::new_v4();
|
||||
let envelope = email.envelope();
|
||||
|
||||
if !self.client.is_connected() {
|
||||
self.connect()?;
|
||||
@@ -460,39 +472,37 @@ impl<'a> Transport<'a> for SmtpTransport {
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.client.command(MailCommand::new(
|
||||
email.envelope().from().cloned(),
|
||||
mail_options,
|
||||
)),
|
||||
self.client
|
||||
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
|
||||
self
|
||||
);
|
||||
|
||||
// Log the mail command
|
||||
info!(
|
||||
"{}: from=<{}>",
|
||||
message_id,
|
||||
match email.envelope().from() {
|
||||
email_id,
|
||||
match envelope.from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in email.envelope().to() {
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(
|
||||
self.client
|
||||
.command(RcptCommand::new(to_address.clone(), vec![])),
|
||||
self
|
||||
);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
info!("{}: to=<{}>", email_id, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.command(DataCommand), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(Box::new(email.message()));
|
||||
let result = self.client.message(email.to_string().as_bytes());
|
||||
|
||||
if let Ok(ref result) = result {
|
||||
// Increment the connection reuse counter
|
||||
@@ -501,7 +511,7 @@ impl<'a> Transport<'a> for SmtpTransport {
|
||||
// Log the message
|
||||
info!(
|
||||
"{}: conn_use={}, status=sent ({})",
|
||||
message_id,
|
||||
email_id,
|
||||
self.state.connection_reuse_count,
|
||||
result
|
||||
.message
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::smtp::error::Error;
|
||||
use crate::smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
||||
use crate::transport::smtp::{error::Error, ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
||||
use r2d2::ManageConnection;
|
||||
|
||||
pub struct SmtpConnectionManager {
|
||||
@@ -1,7 +1,7 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use crate::smtp::Error;
|
||||
use crate::transport::smtp::Error;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_until},
|
||||
@@ -10,10 +10,12 @@ use nom::{
|
||||
sequence::{preceded, tuple},
|
||||
IResult,
|
||||
};
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
result,
|
||||
str::FromStr,
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
@@ -2,9 +2,9 @@
|
||||
//! testing purposes.
|
||||
//!
|
||||
|
||||
use crate::Email;
|
||||
use crate::Transport;
|
||||
use crate::{Message, Transport};
|
||||
use log::info;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -27,15 +27,18 @@ impl StubTransport {
|
||||
/// SMTP result type
|
||||
pub type StubResult = Result<(), ()>;
|
||||
|
||||
impl<'a> Transport<'a> for StubTransport {
|
||||
impl<'a, B> Transport<'a, B> for StubTransport
|
||||
where
|
||||
B: Display,
|
||||
{
|
||||
type Result = StubResult;
|
||||
|
||||
fn send<E: Into<Email>>(&mut self, email: E) -> StubResult {
|
||||
let email = email.into();
|
||||
|
||||
fn send(&mut self, email: Message<B>) -> Self::Result
|
||||
where
|
||||
B: Display,
|
||||
{
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
"from=<{}> to=<{:?}>",
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
@@ -1,32 +0,0 @@
|
||||
use lettre::builder::EmailBuilder;
|
||||
use lettre::{EmailAddress, Envelope};
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_test() {
|
||||
let e = Envelope::new(
|
||||
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_without_from_test() {
|
||||
let e = Envelope::new(
|
||||
None,
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.is_err());
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::env::{self, consts::EXE_EXTENSION};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
env::{self, consts::EXE_EXTENSION},
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
skeptic_test(Path::new("README.md"));
|
||||
// README needs to be compatible with latest release
|
||||
//skeptic_test(Path::new("README.md"));
|
||||
|
||||
for entry in WalkDir::new("website").into_iter().filter(|e| {
|
||||
e.as_ref()
|
||||
@@ -35,7 +38,6 @@ fn skeptic_test(path: &Path) {
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}", path)
|
||||
@@ -1,10 +1,10 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
||||
mod test {
|
||||
use lettre::{ClientSecurity, Email, EmailAddress, Envelope, SmtpClient};
|
||||
use lettre::{SmtpConnectionManager, Transport};
|
||||
use lettre::{
|
||||
ClientSecurity, Email, EmailAddress, Envelope, SmtpClient, SmtpConnectionManager, Transport,
|
||||
};
|
||||
use r2d2::Pool;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::{sync::mpsc, thread};
|
||||
|
||||
fn email(message: &str) -> Email {
|
||||
Email::new(
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{Email, EmailAddress, Envelope, Transport};
|
||||
use std::env::temp_dir;
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use lettre::{transport::file::FileTransport, Message, Transport};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{remove_file, File},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let message_id = email.message_id().to_string();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
let id = result.unwrap();
|
||||
|
||||
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
|
||||
let file = temp_dir().join(format!("{}.json", id));
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
|
||||
);
|
||||
|
||||
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"message\":[70,114,111,109,58,32,78,111,66,111,100,121,32,60,110,111,98,111,100,121,64,100,111,109,97,105,110,46,116,108,100,62,13,10,82,101,112,108,121,45,84,111,58,32,89,117,105,110,32,60,121,117,105,110,64,100,111,109,97,105,110,46,116,108,100,62,13,10,84,111,58,32,72,101,105,32,60,104,101,105,64,100,111,109,97,105,110,46,116,108,100,62,13,10,83,117,98,106,101,99,116,58,32,72,97,112,112,121,32,110,101,119,32,121,101,97,114,13,10,68,97,116,101,58,32,84,117,101,44,32,49,53,32,78,111,118,32,49,57,57,52,32,48,56,58,49,50,58,51,49,32,71,77,84,13,10,13,10,66,101,32,104,97,112,112,121,33]}");
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
mod test {
|
||||
use lettre::sendmail::SendmailTransport;
|
||||
use lettre::{Email, EmailAddress, Envelope, Transport};
|
||||
use lettre::{transport::sendmail::SendmailTransport, Message, Transport};
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email);
|
||||
println!("{:?}", result);
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
use lettre::{ClientSecurity, Email, EmailAddress, Envelope, SmtpClient, Transport};
|
||||
use lettre::{ClientSecurity, Message, SmtpClient, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{Email, EmailAddress, Envelope, Transport};
|
||||
use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let mut sender_ok = StubTransport::new_positive();
|
||||
let mut sender_ko = StubTransport::new(Err(()));
|
||||
let email_ok = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email_ko = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email_ok).unwrap();
|
||||
sender_ko.send(email_ko).unwrap_err();
|
||||
sender_ok.send(email.clone()).unwrap();
|
||||
sender_ko.send(email).unwrap_err();
|
||||
}
|
||||
|
||||
2
website/.gitignore
vendored
2
website/.gitignore
vendored
@@ -1 +1 @@
|
||||
/_book
|
||||
/book
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
all: depends _book
|
||||
|
||||
depends:
|
||||
cargo install --force mdbook --vers "^0.2"
|
||||
cargo install --force mdbook-linkcheck --vers "^0.2"
|
||||
|
||||
serve:
|
||||
mdbook serve
|
||||
|
||||
_book:
|
||||
mdbook build
|
||||
|
||||
clean:
|
||||
rm -rf _book/
|
||||
|
||||
.PHONY: _book
|
||||
@@ -1,11 +1,10 @@
|
||||
[book]
|
||||
title = "Lettre"
|
||||
author = "Alexis Mousset"
|
||||
title = "Lettre documentation"
|
||||
authors = ["Alexis Mousset"]
|
||||
description = "The user documentation of the Lettre crate."
|
||||
|
||||
[build]
|
||||
build-dir = "_book"
|
||||
|
||||
[output.html]
|
||||
default-theme = "Ayu"
|
||||
no-section-label = true
|
||||
|
||||
[output.linkcheck]
|
||||
[output.linkcheck]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Summary
|
||||
|
||||
* [Introduction](README.md)
|
||||
* [News](posts/_index.md)
|
||||
* [Lettre 0.10 (2020.04.12)](posts/lettre-0-10.md)
|
||||
* [Are we email yet? (2020.04.12)](posts/are-we-email-yet.md)
|
||||
* [Creating Messages](creating-messages/email.md)
|
||||
* [Sending Messages](sending-messages/_index.md)
|
||||
* [SMTP Transport](sending-messages/smtp.md)
|
||||
|
||||
@@ -2,38 +2,175 @@
|
||||
|
||||
This section explains how to create emails.
|
||||
|
||||
#### Simple example
|
||||
## Usage
|
||||
|
||||
The `email` part builds email messages. For now, it does not support attachments.
|
||||
An email is built using an `Email` builder. The simplest email could be:
|
||||
### Format email messages
|
||||
|
||||
#### With string body
|
||||
|
||||
The easiest way how we can create email message with simple string.
|
||||
|
||||
```rust
|
||||
# #[cfg(feature = "builder")]
|
||||
# {
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
use lettre::message::Message;
|
||||
|
||||
use lettre::Email;
|
||||
|
||||
fn main() {
|
||||
// Create an email
|
||||
let email = Email::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
|
||||
.build();
|
||||
|
||||
assert!(email.is_ok());
|
||||
}
|
||||
# }
|
||||
let m: Message<&str> = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
When the `build` method is called, the builder will add the missing headers (like
|
||||
`Message-ID` or `Date`) and check for missing necessary ones (like `From` or `To`). It will
|
||||
then generate an `Email` that can be sent.
|
||||
Will produce:
|
||||
|
||||
The `text()` method will create a plain text email, while the `html()` method will create an
|
||||
HTML email. You can use the `alternative()` method to provide both versions, using plain text
|
||||
as fallback for the HTML version.
|
||||
```sh
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
|
||||
Be happy!
|
||||
```
|
||||
|
||||
The unicode header data will be encoded using _UTF8-Base64_ encoding.
|
||||
|
||||
### With MIME body
|
||||
|
||||
##### Single part
|
||||
|
||||
The more complex way is using MIME contents.
|
||||
|
||||
```rust
|
||||
# extern crate lettre;
|
||||
use lettre::message::{header, Message, SinglePart};
|
||||
|
||||
let m: Message<SinglePart<&str>> = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.mime_body(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
)).header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
.body("Привет, мир!"),
|
||||
)
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
The body will be encoded using selected `Content-Transfer-Encoding`.
|
||||
|
||||
```sh
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=utf8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
|
||||
|
||||
```
|
||||
|
||||
##### Multiple parts
|
||||
|
||||
And more advanced way of building message by using multipart MIME contents.
|
||||
|
||||
```rust
|
||||
# extern crate lettre;
|
||||
use lettre::message::{header, Message, MultiPart, SinglePart};
|
||||
|
||||
let m: Message<MultiPart<&str>> = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.mime_body(
|
||||
MultiPart::mixed()
|
||||
.multipart(
|
||||
MultiPart::alternative()
|
||||
.singlepart(
|
||||
SinglePart::quoted_printable()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.body("Привет, мир!")
|
||||
)
|
||||
.multipart(
|
||||
MultiPart::related()
|
||||
.singlepart(
|
||||
SinglePart::eight_bit()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.body("<p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>")
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::base64()
|
||||
.header(header::ContentType("image/png".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Inline,
|
||||
parameters: vec![],
|
||||
})
|
||||
.body("<smile-raw-image-data>")
|
||||
)
|
||||
)
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::seven_bit()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![
|
||||
header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None, "example.c".as_bytes().into()
|
||||
)
|
||||
]
|
||||
})
|
||||
.body("int main() { return 0; }")
|
||||
)
|
||||
).unwrap();
|
||||
```
|
||||
|
||||
```sh
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m"
|
||||
|
||||
--RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
|
||||
Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
|
||||
|
||||
--qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=utf8
|
||||
|
||||
=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
|
||||
--qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
|
||||
Content-Type: multipart/related; boundary="BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8"
|
||||
|
||||
--BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Content-Type: text/html; charset=utf8
|
||||
|
||||
<p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>
|
||||
--BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
Content-Disposition: inline
|
||||
|
||||
PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
|
||||
--BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8--
|
||||
--qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy--
|
||||
--RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/plain; charset=utf8
|
||||
Content-Disposition: attachment; filename="example.c"
|
||||
|
||||
int main() { return 0; }
|
||||
--RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m--
|
||||
|
||||
```
|
||||
|
||||
6
website/src/posts/_index.md
Normal file
6
website/src/posts/_index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# News
|
||||
|
||||
## 2020
|
||||
|
||||
* Apr 12 - [0.10 Release](lettre-0-10.md)
|
||||
* Apr 12 - [Are we email yet?](are-we-email-yet.md)
|
||||
13
website/src/posts/are-we-email-yet.md
Normal file
13
website/src/posts/are-we-email-yet.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Are we email yet?
|
||||
|
||||
TODO
|
||||
|
||||
short email landscape in rust
|
||||
|
||||
* [rust-async](https://github.com/async-email/async-smtp), provides a async SMTP transport
|
||||
compatible with lettre 0.9 messages.
|
||||
* [Proposal for a mail WG](https://internals.rust-lang.org/t/starting-a-rust-mail-wg/11678)
|
||||
* samotop
|
||||
* mail (and other by 1aim), including async smtp implementation (with tokio 0.1). Not maintained
|
||||
anymore, pretty complete.
|
||||
|
||||
61
website/src/posts/lettre-0-10.md
Normal file
61
website/src/posts/lettre-0-10.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Lettre 0.10
|
||||
|
||||
*2019.04.12*
|
||||
|
||||
## What is `lettre`?
|
||||
|
||||
Lettre provides an email client for Rust programs, to allow easily sending emails from Rust
|
||||
applications with the following focuses
|
||||
|
||||
* Ease of use, without particular knowledge about email
|
||||
* Secure by default
|
||||
* Modern (support for full internationalization)
|
||||
|
||||
Non-goals:
|
||||
|
||||
* Implementing email RFCs extensively. The goal is to target a modern and safe subset needed to
|
||||
send emails today, with a nice API (i.e. UTF-8 only, etc.). Particularly, lettre
|
||||
currently cannot parse emails.
|
||||
|
||||
### Background
|
||||
|
||||
The `lettre` crate was previously named [`smtp`](https://crates.io/crates/smtp). It was [created](https://github.com/lettre/lettre/commit/270efd193a11e66dce14700a50d3c42c12e725bc) in early 2014 (before cargo, Rust 1.0, etc.).
|
||||
|
||||
The first goal was to start a toy project as a pretext to learn Rust. I started with an `smtp` implementation after seeing there was no existing implementation in Rust. Originally, the project aimed at implementing the `SMTP` protocol for client and server.
|
||||
|
||||
In 2016, the goal changed, and specialized to email client (as I did not see much use in another SMTP server may it be written in Rust). The project also moved away from "just SMTP" to email client, and was renamed to lettre at this time. Why `lettre`? After some time looking for a fitting name, not already taken by email-related software, I ended up just taking the the French word for "letter"!
|
||||
|
||||
## Changes in 0.10
|
||||
|
||||
* Replacement of the message implementation (which was based on `rust-email`)
|
||||
by a new one based on the `emailmessage` crate. To main goal is to provide
|
||||
sane encoding and multipart was a simple implementation (no message parsing).
|
||||
* Merge of the `lettre_email` crate into `lettre`. This split made not much sense, and the message
|
||||
builder is now a feature of the `lettre` crate.
|
||||
* More features to allow disabling most features.
|
||||
* Add the option to use `rustls` for TLS.
|
||||
* Improved parsing of server responses.
|
||||
* Moved CI from Travis to Github actions.
|
||||
|
||||
### Migration from 0.9
|
||||
|
||||
TODO
|
||||
|
||||
## Road to 1.0
|
||||
|
||||
Lettre is now used by several projects, including crates.io itself!
|
||||
It will be good to have a stable basis for the future.
|
||||
|
||||
The plan is that 0.10 is the release preparing the 1.0 in the following months.
|
||||
I'd also want to add more real-world automated testing with actual mail servers (at least postfix).
|
||||
|
||||
`async` is not a goal for 1.0, as it is not as relevant for emails as it is for other ecosystems
|
||||
(like web), and theadpool-based solutions are in general very suited.
|
||||
|
||||
## After
|
||||
|
||||
* reuse `smtp` crate for the protocol (a bit like `http`)
|
||||
* async
|
||||
|
||||
If you want to contribute, the `lettre` repo and organizations are definitely open for anything
|
||||
related to email.
|
||||
@@ -7,24 +7,23 @@ It can be useful for testing purposes, or if you want to keep track of sent mess
|
||||
```rust
|
||||
# #[cfg(feature = "file-transport")]
|
||||
# {
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
|
||||
use std::env::temp_dir;
|
||||
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{Transport, Envelope, EmailAddress, Email};
|
||||
use lettre::transport::file::FileTransport;
|
||||
use lettre::{Transport, Envelope, EmailAddress, Message};
|
||||
|
||||
fn main() {
|
||||
// Write to the local temp directory
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
@@ -5,20 +5,19 @@ The sendmail transport sends the email using the local sendmail command.
|
||||
```rust,no_run
|
||||
# #[cfg(feature = "sendmail-transport")]
|
||||
# {
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
|
||||
use lettre::sendmail::SendmailTransport;
|
||||
use lettre::{Email, Envelope, EmailAddress, Transport};
|
||||
use lettre::transport::sendmail::SendmailTransport;
|
||||
use lettre::{Message, Envelope, EmailAddress, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let mut sender = SendmailTransport::new();
|
||||
let result = sender.send(email);
|
||||
|
||||
@@ -20,19 +20,18 @@ This is the most basic example of usage:
|
||||
```rust,no_run
|
||||
# #[cfg(feature = "smtp-transport")]
|
||||
# {
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
|
||||
use lettre::{Email, EmailAddress, Transport, Envelope, SmtpClient};
|
||||
use lettre::{Message, EmailAddress, Transport, Envelope, SmtpClient};
|
||||
|
||||
fn main() {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer =
|
||||
@@ -50,12 +49,12 @@ fn main() {
|
||||
```rust,no_run
|
||||
# #[cfg(feature = "smtp-transport")]
|
||||
# {
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::transport::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::{Email, Envelope, EmailAddress, Transport, SmtpClient};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::transport::smtp::extension::ClientId;
|
||||
use lettre::transport::smtp::ConnectionReuseParameters;
|
||||
|
||||
fn main() {
|
||||
let email_1 = Email::new(
|
||||
@@ -107,15 +106,15 @@ You can specify custom TLS settings:
|
||||
```rust,no_run
|
||||
# #[cfg(feature = "native-tls")]
|
||||
# {
|
||||
extern crate native_tls;
|
||||
extern crate lettre;
|
||||
# extern crate native_tls;
|
||||
# extern crate lettre;
|
||||
|
||||
use lettre::{
|
||||
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
|
||||
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
|
||||
Email, SmtpClient, Transport,
|
||||
};
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::transport::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::transport::smtp::ConnectionReuseParameters;
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
|
||||
fn main() {
|
||||
@@ -163,14 +162,14 @@ error handling:
|
||||
```rust,no_run
|
||||
# #[cfg(feature = "smtp-transport")]
|
||||
# {
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
|
||||
use lettre::EmailAddress;
|
||||
use lettre::smtp::SMTP_PORT;
|
||||
use lettre::smtp::client::InnerClient;
|
||||
use lettre::smtp::client::net::NetworkStream;
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::commands::*;
|
||||
use lettre::transport::smtp::SMTP_PORT;
|
||||
use lettre::transport::smtp::client::InnerClient;
|
||||
use lettre::transport::smtp::client::net::NetworkStream;
|
||||
use lettre::transport::smtp::extension::ClientId;
|
||||
use lettre::transport::smtp::commands::*;
|
||||
|
||||
fn main() {
|
||||
let mut email_client: InnerClient<NetworkStream> = InnerClient::new();
|
||||
|
||||
@@ -4,20 +4,19 @@ The stub transport only logs message envelope and drops the content. It can be u
|
||||
testing purposes.
|
||||
|
||||
```rust
|
||||
extern crate lettre;
|
||||
# extern crate lettre;
|
||||
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{Email, Envelope, EmailAddress, Transport};
|
||||
use lettre::transport::stub::StubTransport;
|
||||
use lettre::{Message, Envelope, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = Email::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let mut sender = StubTransport::new_positive();
|
||||
let result = sender.send(email);
|
||||
|
||||
Reference in New Issue
Block a user