diff options
author | Philipp Korber <p.korber@1aim.com> | 2018-11-16 15:46:43 +0100 |
---|---|---|
committer | Philipp Korber <p.korber@1aim.com> | 2018-11-16 15:46:43 +0100 |
commit | 652d6f0ffeee7302a2cb51059bef75d8b0bb50be (patch) | |
tree | c3851592642938172f280f7428d43e08b0fe2cbe /mail | |
parent | 0947fe8996149fe20a6d47a793f9555790eb2eae (diff) |
refactor: merged sources of mail-headers,mail-internals,mail-core, mail
Originally it was palaned to do a merge with `--allow-unrelated-history`
but this can not be doesn as `mail-core` has a "invalid" history which
has a merge conflict **with itself**. So even rewinding the history on
a empty repo is not possible.
Instead the code was directly coppied over losing history.
But the history is still available in the different
`history-backup-*` branches. It is just that the past history
is decoupled from the current history.
Diffstat (limited to 'mail')
-rw-r--r-- | mail/Cargo.toml | 42 | ||||
-rw-r--r-- | mail/README.md | 117 | ||||
-rw-r--r-- | mail/example_resources/templates/template_a/html/logo.png | bin | 0 -> 343 bytes | |||
-rw-r--r-- | mail/example_resources/templates/template_a/html/mail.html | 5 | ||||
-rw-r--r-- | mail/example_resources/templates/template_a/portfolio.pdf | bin | 0 -> 31571 bytes | |||
-rw-r--r-- | mail/example_resources/templates/template_a/text/mail.txt | 1 | ||||
-rw-r--r-- | mail/example_resources/tera_base/base_mail.html | 10 | ||||
-rw-r--r-- | mail/examples/mail_by_hand.rs | 84 | ||||
-rw-r--r-- | mail/examples/mail_from_template/error.rs | 56 | ||||
-rw-r--r-- | mail/examples/mail_from_template/main.rs | 99 | ||||
-rw-r--r-- | mail/examples/send_mail/cli.rs | 212 | ||||
-rw-r--r-- | mail/examples/send_mail/main.rs | 114 | ||||
-rw-r--r-- | mail/src/lib.rs | 193 | ||||
-rw-r--r-- | mail/src/macros.rs | 33 |
14 files changed, 966 insertions, 0 deletions
diff --git a/mail/Cargo.toml b/mail/Cargo.toml new file mode 100644 index 0000000..6d198d6 --- /dev/null +++ b/mail/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "mail" +version = "0.1.0" +authors = ["Philipp Korber <philippkorber@gmail.com>"] +categories = [] +#TODO go through the keywords +keywords = ["mail", "rfc5322", "email", "mime", "smtp", "template"] +description = "mail, facade for a number of mail related crates for creating and sending mails" +license = "MIT OR Apache-2.0" +readme = "./README.md" +repository = "https://github.com/1aim/mail" + +[features] +smtp = ["mail-smtp"] +#askama-engine = ["mail-template/askama-engine"] +#tera-engine = ["render-template-engine", "mail-render-template-engine/tera-engine"] +#handlebars-engine = ["render-template-engine", "mail-render-template-engine/handlebars-engine"] +#render-template-engine = ["mail-render-template-engine"] + +traceing = ["mail-internals/traceing", "mail-headers/traceing"] + + + +[dependencies] +mail-internals = { git="https://github.com/1aim/mail-internals" } +mail-headers = { git="https://github.com/1aim/mail-headers" } +mail-core = { git="https://github.com/1aim/mail-types" } +#mail-template = { git="https://github.com/1aim/mail-template" } +mail-derive = { git="https://github.com/1aim/mail-derive" } +mail-smtp = { git="https://github.com/1aim/mail-smtp", optional=true } +#mail-render-template-engine = {git="https://github.com/1aim/mail-render-template-engine", optional=true } + +[dev-dependencies] +rpassword = "2.0.0" +tokio = "0.1.1" +serde = "1.0.10" +serde_derive = "1.0.10" +soft-ascii-string = "1.0" +futures = "0.1.0" +failure = "0.1.1" + + diff --git a/mail/README.md b/mail/README.md new file mode 100644 index 0000000..2a89248 --- /dev/null +++ b/mail/README.md @@ -0,0 +1,117 @@ +# mail / mail-api   + +Documentation can be [viewed on docs.rs](https://docs.rs/mail-api). (at least once it's published ;=) ) + +Facade which re-exports functionality from a number of mail related crates. + +The crates include: + +- `mail-internals` some parts used by more or less all other `mail-*` crates like + `MailType`, some grammar parts or the `EncodingBuffer`. As `mail-internals` is + mainly used internally it's not directly exposed. +- `mail-headers` functionality wrt. mail (mime) header, including a HeaderMap + containing the header fields (keeping insertion order) a number of components + used in header field bodies like e.g. `Mailbox` or `Phrase` and default + implementations for many headers, including From, To, Sender, Data, Subject, + Date, MessageId, Cc, Bcc, etc. +- `mail-core` provides the type `Mail` which represents a (possible) + multi-part mime Mail and includes functionality for encoding it. It also + contains an abstraction for the content of multi-part mails called + `Resource`, which includes support for embeddings, attachments etc. +- `mail-template` provides functionality to create a `Mail` from a template, + including multi-part mails containing embeddings and attachments. It's not + bound to a specific template engine. Currently bindings for the tera template + engine are provided behind feature flag. +- `mail-smtp` (feature: `smtp`) provides bindings between `mail-core` and + `new-tokio-smtp` allowing the simple sending of mails to a specific server. + It's mainly focused on the use-case where mails are sent to an Mail + Submission Agent (MSA) which then distributes them +- `mail-render-template-engine` (feature `render-template-engine`) provides a + partial implementation for the `TemplateEngine` trait from `mail-template` + only missing a "render engine" to render the template. The implementation + includes functionality for automatically generating multiple alternate bodies + (e.g. text, html) embedding, and attachments based on a spec, which can be + derived and loaded from a folder/file layout making it easy to create and + maintain complex mail templates. + +## Examples + +### [`mail_by_hand`](./examples/mail_by_hand.rs) + +Creates and encodes a simple mail without using any fancy helpers, templates or +similar. + +### [`mail_from_template`](./examples/mail_from_template/main.rs) + +Uses the bindings for the `tera` template engine to create a mail, including +alternate bodies and an attachment. + +### [`send_mail`](./examples/send_mail/main.rs) + +A simple program which queries the user for information and then sends a +(simple) mail to an MSA (Mail Submission Agent). While it is currently limited +to STARTTLS on port 587, Auth Plain and only simple text mails this is a +limitation of this cli program not the mail libraries which can handle other +forms of connecting and authenticating etc. + +Note that this is meant to send data to an MSA NOT a MX (Mail Exchanger), e.g. +`smtp.gmail.com` is a MSA but `gmail-smtp-in.l.google.com` is an MX. Also note +that some mail providers do not accept Auth Plain (at least not without +enabling it in the security settings). The reason for this is that they prefer +that applications do not use username+password for authentication but other +formats e.g. OAuth2 tokens. + +Rest assured that the authentication data is only sent over a TLS encrypted +channel. Still if you don't trust it consider using some throw away or testing +mail service e.g. `ethereal.email`. + +Lastly the examples uses the same unique seed every time, which means that +Message-ID's, and Content-ID's are not guaranteed to be world unique even +through they should (again a limitation of the example not the mail crate). +Nevertheless given that it also doesn't use its "own" domain but a `.test` +domain it can't guarantee world uniqueness anyway. + +## Features + +### `smtp` + +Provides bindings to `new-tokio-smtp` under `mail::smtp` by reexporting the +`mail-smtp` crate + +### `render-template-engine` + +Provides the render template engine under `mail::render_template_engine`. + +### `askama-engine` + +Provides bindings to the `askama` crate (a template engine) under +`mail::askama` + +### `tera-engine` + +Provides bindings to the `tera` crate (a template engine) under `mail::tera`. +This feature uses the `render-template-engine` feature. + +### `traceing` + +Enables the `traceing` debugging functionality in the `EncodingBuffer` +from `mail-internals`, this is only used for testing header implementations +and comes with noticeable overhead. **As such this should not be enabled +except for testing header implementations**. Also `EncodingBuffer` isn't +re-exported as it can be seen as an internal part of the implementation +which normally doesn't need to be accessed directly. + +## License + +Licensed under either of + +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any +additional terms or conditions. diff --git a/mail/example_resources/templates/template_a/html/logo.png b/mail/example_resources/templates/template_a/html/logo.png Binary files differnew file mode 100644 index 0000000..f58bea6 --- /dev/null +++ b/mail/example_resources/templates/template_a/html/logo.png diff --git a/mail/example_resources/templates/template_a/html/mail.html b/mail/example_resources/templates/template_a/html/mail.html new file mode 100644 index 0000000..a377e53 --- /dev/null +++ b/mail/example_resources/templates/template_a/html/mail.html @@ -0,0 +1,5 @@ +{% extends "base_mail.html" %} + +{% block head %}<meta charset="utf-8">{% endblock head %} + +{% block body %}logo: <img src="cid:{{cids.logo}}"> Hy {{data.name}}.{% endblock body %} diff --git a/mail/example_resources/templates/template_a/portfolio.pdf b/mail/example_resources/templates/template_a/portfolio.pdf Binary files differnew file mode 100644 index 0000000..3748b05 --- /dev/null +++ b/mail/example_resources/templates/template_a/portfolio.pdf diff --git a/mail/example_resources/templates/template_a/text/mail.txt b/mail/example_resources/templates/template_a/text/mail.txt new file mode 100644 index 0000000..903c8ce --- /dev/null +++ b/mail/example_resources/templates/template_a/text/mail.txt @@ -0,0 +1 @@ +Hy {{data.name}}.
\ No newline at end of file diff --git a/mail/example_resources/tera_base/base_mail.html b/mail/example_resources/tera_base/base_mail.html new file mode 100644 index 0000000..4bd251a --- /dev/null +++ b/mail/example_resources/tera_base/base_mail.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <!-- NOTE THAT THIS IS JUST AN EXAMPLE FOR THE TEMPLATE ENGINE NOT FOR MAIL ON ITSELF --> + {% block head %}{% endblock head %} +</head> +<body> + {% block body %}{% endblock body %} +</body> +</html> diff --git a/mail/examples/mail_by_hand.rs b/mail/examples/mail_by_hand.rs new file mode 100644 index 0000000..c5414b8 --- /dev/null +++ b/mail/examples/mail_by_hand.rs @@ -0,0 +1,84 @@ +//! In this example a `Mail` is directly created encoded and +//! printed + +#[macro_use] +extern crate mail; +extern crate futures; +extern crate soft_ascii_string; + +use futures::Future; +use soft_ascii_string::SoftAsciiString; + +use std::str; + +use mail::{ + HeaderTryFrom, + Mail, MailType, + Resource, + Context, Domain, + error::MailError, + default_impl::simple_context, +}; + +// Mail uses \r\n newlines!! +const MSG: &str = +"Dear Tree Apes,\r +\r +the next grate block buster is here 🎉\r +\r +With regards,\r +The Tree Movie Consortium\r +"; + +fn create_text_body() -> Resource { + Resource::sourceless_from_string(MSG.to_owned()) +} + +fn build_mail() -> Result<Mail, MailError> { + use mail::headers::*; + + let mut mail = Mail::new_singlepart_mail(create_text_body()); + mail.insert_headers(headers! { + // `From` can have more than one mailbox. + _From: [("Tree Movie Consortium", "datmail@dat.test")], + // `To` can have more then one mailbox. + _To: [("Tree Ape Chief", "datothermail@dat.test")], + Subject: "Rise of the Type Lord: The 🙈 Emoji Plight" + // `Date`, `ContentType`, `ContentTransferEncoding` get added automatically. + }?); + + Ok(mail) +} + +fn encode_mail_to_stdout(mail: Mail, ctx: impl Context) -> Result<(), MailError> { + let bytes = mail + // This loads lazy resources, e.g. attachments/embeddings. + .into_encodeable_mail(ctx) + // It's a future, but we will just block here. + .wait()? + // Encodes mail and returns a `Vec<u8>` representing the + // mail. `Vec<u8>` is used as mails can contain non-utf8 + // data through a number of ways, though in many (most?) + // situations today they should not. (Also with mail type + // Ascii it can not contain non ascii chars without being + // invalid but with 8BITMIME and Internationalized mails + // it can). + .encode_into_bytes(MailType::Ascii)?; + + // Note how the 🙈 utf8 char will be automatically encoded (it would not be + // specially encoded if MailType would be Internationalized). + println!("{}", str::from_utf8(bytes.as_slice()) + .expect("[BUG] MailType::Ascii can't have non utf8 bytes")); + Ok(()) +} + +fn main() { + println!("---------------- START ---------------- "); + let msg_id_domain = Domain::try_from("company_a.test").unwrap(); + let unique_part = SoftAsciiString::from_string("c207n521cec").unwrap(); + let ctx = simple_context::new(msg_id_domain, unique_part).unwrap(); + + let mail = build_mail().unwrap(); + encode_mail_to_stdout(mail, ctx).unwrap(); + println!("---------------- END ---------------- "); +} diff --git a/mail/examples/mail_from_template/error.rs b/mail/examples/mail_from_template/error.rs new file mode 100644 index 0000000..21bccfe --- /dev/null +++ b/mail/examples/mail_from_template/error.rs @@ -0,0 +1,56 @@ + +use mail::error::{CompositionError, ComponentCreationError}; +use mail::render_template_engine::error::{CreatingSpecError, InsertionError as _InsertionError}; +use mail::tera::error::TeraError; + +type InsertionError = _InsertionError<TeraError>; + +#[derive(Fail, Debug)] +pub enum SetupError { + #[fail(display = "{}", _0)] + Tera(TeraError), + + #[fail(display = "{}", _0)] + CreatingSpecs(CreatingSpecError), + + #[fail(display = "{}", _0)] + UsingSpecs(InsertionError) +} + +impl From<TeraError> for SetupError { + fn from(err: TeraError) -> Self { + SetupError::Tera(err) + } +} +impl From<InsertionError> for SetupError { + fn from(err: InsertionError) -> Self { + SetupError::UsingSpecs(err) + } +} + +impl From<CreatingSpecError> for SetupError { + fn from(err: CreatingSpecError) -> Self { + SetupError::CreatingSpecs(err) + } +} + +// TODO `Header` should be mergeable into `Composition`. +#[derive(Fail, Debug)] +pub enum Error { + #[fail(display = "{}", _0)] + Composition(CompositionError<TeraError>), + #[fail(display = "{}", _0)] + Header(ComponentCreationError) +} + +impl From<CompositionError<TeraError>> for Error { + fn from(err: CompositionError<TeraError>) -> Self { + Error::Composition(err) + } +} + +impl From<ComponentCreationError> for Error { + fn from(err: ComponentCreationError) -> Self { + Error::Header(err) + } +} diff --git a/mail/examples/mail_from_template/main.rs b/mail/examples/mail_from_template/main.rs new file mode 100644 index 0000000..6a43125 --- /dev/null +++ b/mail/examples/mail_from_template/main.rs @@ -0,0 +1,99 @@ +//! In this example a mail is created using the tempalte engine with tera bindings +//! (and then printed) +//! +#[cfg(not(all(feature = "render-template-engine", feature = "tera-engine")))] +compile_error!("example `mail_from_template` requires feature `render-template-engine` and `tera-engine`"); + +#[macro_use] +extern crate mail; +#[macro_use] +extern crate failure; +#[macro_use] +extern crate serde_derive; +extern crate futures; +extern crate soft_ascii_string; + +use futures::Future; +use soft_ascii_string::SoftAsciiString; + +use std::str; +use std::borrow::Cow; + +use mail::error::MailError; +use mail::{Mail, Mailbox, MailType, HeaderTryFrom, Context, Domain}; +use mail::default_impl::simple_context; +use mail::template::{InspectEmbeddedResources, Embedded, MailSendData}; +use mail::render_template_engine::{RenderTemplateEngine, TemplateSpec, DEFAULT_SETTINGS}; +use mail::tera::TeraRenderEngine; + +use self::error::{SetupError, Error}; + +mod error; + +#[derive(Debug, Serialize, InspectEmbeddedResources)] +struct UserData { + name: &'static str + // TODO: include embedded avatar. +} + +fn main() { + let msg_id_domain = Domain::try_from("company_a.test").unwrap(); + let unique_part = SoftAsciiString::from_string("c207n521cec").unwrap(); + let ctx = simple_context::new(msg_id_domain, unique_part).unwrap(); + + let engine = setup_template_engine().unwrap(); + + let mail = create_mail(&engine, &ctx).unwrap(); + let string_mail = encode_mail_to_string(mail, ctx.clone()).unwrap(); + println!("{}", string_mail); +} + +fn create_mail(engine: &RenderTemplateEngine<TeraRenderEngine>, ctx: &impl Context) + -> Result<Mail, Error> +{ + let from = Mailbox::try_from("a@b.c")?; + let to = Mailbox::try_from("d@e.f")?; + let subject = "Live might not be a roller coaster"; + // Use template_a. + let template_id = Cow::Borrowed("template_a"); + // This can be basically a type implementing `Serialize`, + // in the tera template we use it with `{{data.name}}`. + let data = UserData { name: "Mr. Slow Coaster" }; + + // `MailSendData` contains everything needed to crate (and send) + // a mail based on a template. + let send_data = MailSendData::simple_new( + from, to, subject, + template_id, data + ); + + let mail = send_data.compose(ctx, engine)?; + Ok(mail) +} + +fn setup_template_engine() -> Result<RenderTemplateEngine<TeraRenderEngine>, SetupError> { + // Create instance of the tera rte implementation, + // we can reuse/derive from all templates in `tera_base` with + // this (and we do so in the example using `{% extends "base_mail.html" %}`). + let tera = TeraRenderEngine::new("./example_resources/tera_base/**/*")?; + let mut rte = RenderTemplateEngine::new(tera); + // Load all template specs based on the files/folders in `templates` + // using the folder structure as way to define the templates is easy + // but we can do differently if we need to. + let specs = TemplateSpec + ::from_dirs("./example_resources/templates", &*DEFAULT_SETTINGS)?; + + for (name, spec) in specs { + rte.insert_spec(name, spec)?; + } + Ok(rte) +} + +fn encode_mail_to_string(mail: Mail, ctx: impl Context) -> Result<String, MailError> { + let res = mail + .into_encodeable_mail(ctx) + .wait()? + .encode_into_bytes(MailType::Ascii)?; + + Ok(String::from_utf8(res).unwrap()) +} diff --git a/mail/examples/send_mail/cli.rs b/mail/examples/send_mail/cli.rs new file mode 100644 index 0000000..e2c14d6 --- /dev/null +++ b/mail/examples/send_mail/cli.rs @@ -0,0 +1,212 @@ +use std::io::{ + self, + StdinLock, StdoutLock, + BufReader, BufRead, + Write +}; +use std::str::FromStr; + +use rpassword::read_password_with_reader; + + +use mail::{ + Mailbox, Email, HeaderTryFrom, + smtp::misc::Domain, + header_components::{Phrase, Unstructured} +}; + + +pub struct ClDialog<'a> { + stdin: BufReader<StdinLock<'a>>, + stdout: StdoutLock<'a> +} + +pub fn with_dialog<FN, R>(func: FN) -> R + where FN: for<'a> FnOnce(ClDialog<'a>) -> R +{ + let stdin = io::stdin(); + let stdout = io::stdout(); + let dialog = ClDialog::new(stdin.lock(), stdout.lock()); + func(dialog) +} + +impl<'a> ClDialog<'a> { + + pub fn new(stdin: StdinLock<'a>, stdout: StdoutLock<'a>) -> Self { + ClDialog { stdin: BufReader::new(stdin), stdout } + } + + pub fn stdout(&mut self) -> &mut StdoutLock<'a> { + &mut self.stdout + } + + // pub fn stdin(&mut self) -> &mut BufReader<StdinLock<'a>> { + // &mut self.stdin + // } + + pub fn prompt(&mut self, prompt: &str) -> Result<(), io::Error> { + write!(self.stdout, "{}: ", prompt)?; + self.stdout.flush() + } + + pub fn read_line(&mut self) -> Result<String, io::Error> { + let mut line = String::new(); + self.stdin.read_line(&mut line)?; + Ok(line) + } + + pub fn read_email(&mut self) -> Result<Email, io::Error> { + loop { + let line = self.read_line()?; + let line = line.trim(); + if let Ok(email) = Email::try_from(line) { + return Ok(email); + } + self.prompt("[syntax error] email")?; + } + } + + pub fn read_opt_phrase(&mut self) -> Result<Option<Phrase>, io::Error> { + loop { + let line = self.read_line()?; + let line = line.trim(); + if line.is_empty() { + return Ok(None); + } + if let Ok(phrase) = Phrase::try_from(line) { + return Ok(Some(phrase)); + } + self.prompt("[syntax error] optional phrase")?; + } + } + + pub fn read_mailbox(&mut self) -> Result<Mailbox, io::Error> { + self.prompt("- Email Address")?; + let email = self.read_email()?; + self.prompt("- Display Name")?; + let display_name = self.read_opt_phrase()?; + Ok(Mailbox::from((display_name, email))) + } + + pub fn read_password(&mut self) -> Result<String, io::Error> { + read_password_with_reader(Some(&mut self.stdin)) + } + + + pub fn read_mail_text_body(&mut self) -> Result<String, io::Error> { + let mut buf = String::new(); + while self.stdin.read_line(&mut buf)? != 0 { + if !buf.ends_with("\r\n") { + if buf.ends_with("\r") { + buf.push('\n') + } else if buf.ends_with("\n") { + let n_idx = buf.len() - 1; + buf.insert(n_idx, '\r'); + } else { + buf.push_str("\r\n"); + } + } + } + Ok(buf) + } + + // pub fn read_ip_addr(&mut self) -> Result<IpAddr, io::Error> { + // loop { + // let line = self.read_line()?; + // let line = line.trim(); + // if let Ok(ip_addr) = IpAddr::from_str(line) { + // return Ok(ip_addr) + // } + // self.prompt("[syntax error] address")?; + // } + // } + + pub fn read_domain(&mut self) -> Result<Domain, io::Error> { + loop { + let line = self.read_line()?; + let line = line.trim(); + if let Ok(domain) = Domain::from_str(line) { + return Ok(domain); + } + self.prompt("[syntax error] domain")?; + } + } + + pub fn read_auth_data(&mut self) -> Result<AuthData, io::Error> { + self.prompt("- username")?; + let username = self.read_line()?.trim().to_owned(); + self.prompt("- password")?; + let password = self.read_password()?; + Ok(AuthData { username, password }) + } + + pub fn read_msa_info(&mut self) -> Result<MsaInfo, io::Error> { + writeln!(self.stdout, "Mail Submission Agent (MSA) Information:")?; + self.prompt("MSA domain name")?; + let domain = self.read_domain()?; + writeln!(self.stdout, "[using port 587 and STARTTLS]")?; + writeln!(self.stdout, "MSA Authentication data")?; + let auth = self.read_auth_data()?; + Ok(MsaInfo { domain, auth }) + } + + pub fn read_simple_mail(&mut self) -> Result<SimpleMail, io::Error> { + writeln!(self.stdout, "From/Sender:")?; + let from = self.read_mailbox()?; + writeln!(self.stdout, "To/Recipient")?; + let to = self.read_mailbox()?; + self.prompt("Subject")?; + let subject = Unstructured + ::try_from(self.read_line()?.trim()) + .unwrap(); + + writeln!(self.stdout, "Utf-8 text body [end with Ctrl-D]:")?; + self.stdout.flush()?; + let text_body = self.read_mail_text_body()?; + Ok(SimpleMail { + from, + to, + subject, + text_body + }) + } + + pub fn read_yn(&mut self) -> Result<bool, io::Error> { + loop { + let line = self.read_line()?; + let line = line.trim(); + let valid = + match line { + "y" => true, + "n" => false, + _ => continue + }; + return Ok(valid); + } + } +} + + +/// POD +#[derive(Debug)] +pub struct AuthData { + pub username: String, + pub password: String, +} + +/// POD +#[derive(Debug)] +pub struct MsaInfo { + pub domain: Domain, + pub auth: AuthData +} + +/// POD +#[derive(Debug)] +pub struct SimpleMail { + pub from: Mailbox, + pub to: Mailbox, + pub subject: Unstructured, + pub text_body: String, +} + diff --git a/mail/examples/send_mail/main.rs b/mail/examples/send_mail/main.rs new file mode 100644 index 0000000..f00a982 --- /dev/null +++ b/mail/examples/send_mail/main.rs @@ -0,0 +1,114 @@ +//! In this examples runs a simple command line dialog to create a mail +//! and send it to an MSA + +#[cfg(not(feature = "smtp"))] +compile_error!("example `send_mail` requires feature `smtp`"); + +#[macro_use] +extern crate mail; +extern crate rpassword; +extern crate futures; +extern crate tokio; +extern crate soft_ascii_string; + +use std::io::{self, Write}; + +use futures::{Stream, future}; +use soft_ascii_string::SoftAsciiString; + +use mail::{ + Mail, + HeaderTryFrom, + smtp::{ + send_batch as send_mail_batch, + MailRequest, + ConnectionConfig, + ConnectionBuilder, + auth::{Plain as AuthPlain} + }, + error::MailError, + default_impl::simple_context, + header_components::Domain +}; + +mod cli; + +fn main() { + let msg_id_domain = Domain::try_from("company_a.test").unwrap(); + let unique_part = SoftAsciiString::from_string("c207n521cec").unwrap(); + let ctx = simple_context::new(msg_id_domain, unique_part).unwrap(); + let (msa_info, mails) = read_data().unwrap(); + let connection_config = create_connection_config(msa_info); + let mail_requests = create_mail_requests(mails).unwrap(); + + println!("[starting sending mails]"); + + // We run a tokio core "just" for sending the mails, + // normally we probably would schedule/spawn this task + // on a existing tokio runtime. + tokio::run(future::lazy(move || { + send_mail_batch(mail_requests, connection_config, ctx) + .then(|res| Ok(res)) + .for_each(|res| { + match res { + Ok(_) => println!("[mail send]"), + Err(err) => println!("[sending mail failed] {:?}", err) + } + Ok(()) + }) + })); + + println!("[DONE]"); +} + +fn create_connection_config(msa_info: cli::MsaInfo) -> ConnectionConfig<AuthPlain> { + let cli::MsaInfo { domain, auth } = msa_info; + + ConnectionBuilder::new(domain) + .expect("could not resolve domain/host name of MSA") + .auth(AuthPlain::from_username(auth.username, auth.password) + .expect("used \\0 in username or password")) + .build() +} + +fn create_mail_requests(mails: Vec<cli::SimpleMail>) -> Result<Vec<MailRequest>, MailError> { + use mail::headers::*; + + let requests = mails + .into_iter() + .map(|simple_mail| { + let cli::SimpleMail { from, to, subject, text_body } = simple_mail; + let mut mail = Mail::plain_text(text_body); + mail.insert_headers(headers! { + _From: [from], + _To: [to], + Subject: subject + }?); + + Ok(MailRequest::from(mail)) + }) + .collect::<Result<Vec<_>,_>>(); + + requests +} + + + +fn read_data() -> Result<(cli::MsaInfo, Vec<cli::SimpleMail>), io::Error> { + cli::with_dialog(|mut dialog| { + let msa_info = dialog.read_msa_info()?; + let mut mails = Vec::new(); + let mut more = true; + + writeln!(dialog.stdout(), "\nreading mails:")?; + while more { + let mail = dialog.read_simple_mail()?; + mails.push(mail); + dialog.prompt("Another Mail? [y/n]")?; + more = dialog.read_yn()?; + } + writeln!(dialog.stdout(), "[collecting data done]")?; + Ok((msa_info, mails)) + }) +} + diff --git a/mail/src/lib.rs b/mail/src/lib.rs new file mode 100644 index 0000000..dcdb10d --- /dev/null +++ b/mail/src/lib.rs @@ -0,0 +1,193 @@ +//! Facade which re-exports functionality from a number of mail related crates. +//! +//! The crates include: +//! +//! - `mail-internals` some parts used by more or less all other `mail-*` crates like +//! `MailType`, some grammar parts or the `EncodingBuffer`. As `mail-internals` is +//! mainly used internally it's not directly exposed. +//! - `mail-headers` functionality wrt. mail (mime) header, including a HeaderMap +//! containing the header fields (keeping insertion order) a number of components +//! used in header field bodies like e.g. `Mailbox` or `Phrase` and default +//! implementations for many headers, including From, To, Sender, Data, Subject, +//! Date, MessageId, Cc, Bcc, etc. +//! - `mail-core` provides the type `Mail` which represents a (possible) +//! multi-part mime Mail and includes functionality for encoding it. It also +//! contains an abstraction for the content of multi-part mails called +//! `Resource`, which includes support for embeddings, attachments etc. +//! - `mail-template` provides functionality to create a `Mail` from a template, +//! including multi-part mails containing embeddings and attachments. It's not +//! bound to a specific template engine. Currently bindings for the tera template +//! engine are provided behind feature flag. +//! - `mail-smtp` (feature: `smtp`) provides bindings between `mail-core` and +//! `new-tokio-smtp` allowing the simple sending of mails to a specific server. +//! It's mainly focused on the use-case where mails are sent to an Mail +//! Submission Agent (MSA) which then distributes them +//! - `mail-render-template-engine` (feature `render-template-engine`) provides a +//! partial implementation for the `TemplateEngine` trait from `mail-template` +//! only missing a "render engine" to render the template. The implementation +//! includes functionality for automatically generating multiple alternate bodies +//! (e.g. text, html) embedding, and attachments based on a spec, which can be +//! derived and loaded from a folder/file layout making it easy to create and +//! maintain complex mail templates. +//! +//! ## Examples +//! +//! ### [`mail_by_hand`](./examples/mail_by_hand.rs) +//! +//! Creates and encodes a simple mail without using any fancy helpers, templates or +//! similar. +//! +//! ### [`mail_from_template`](./examples/mail_from_template/main.rs) +//! +//! Uses the bindings for the `tera` template engine to create a mail, including +//! alternate bodies and an attachment. +//! +//! ### [`send_mail`](./examples/send_mail/main.rs) +//! +//! A simple program which queries the user for information and then sends a +//! (simple) mail to an MSA (Mail Submission Agent). While it is currently limited +//! to STARTTLS on port 587, Auth Plain and only simple text mails this is a +//! limitation of this cli program not the mail libraries which can handle other +//! forms of connecting and authenticating etc. +//! +//! Note that this is meant to send data to an MSA NOT a MX (Mail Exchanger), e.g. +//! `smtp.gmail.com` is a MSA but `gmail-smtp-in.l.google.com` is an MX. Also note +//! that some mail providers do not accept Auth Plain (at least not without +//! enabling it in the security settings). The reason for this is that they prefer +//! that applications do not use username+password for authentication but other +//! formats e.g. OAuth2 tokens. +//! +//! Rest assured that the authentication data is only sent over a TLS encrypted +//! channel. Still if you don't trust it consider using some throw away or testing +//! mail service e.g. `ethereal.email`. +//! +//! Lastly the examples uses the same unique seed every time, which means that +//! Message-ID's, and Content-ID's are not guaranteed to be world unique even +//! through they should (again a limitation of the example not the mail crate). +//! Nevertheless given that it also doesn't use its "own" domain but a `.test` +//! domain it can't guarantee world uniqueness anyway. +//! +//! ## Features +//! +//! ### `smtp` +//! +//! Provides bindings to `new-tokio-smtp` under `mail::smtp` by reexporting the +//! `mail-smtp` crate +//! +//! ### `render-template-engine` +//! +//! Provides the render template engine under `mail::render_template_engine`. +//! +//! ### `askama-engine` +//! +//! Provides bindings to the `askama` crate (a template engine) under +//! `mail::askama` +//! +//! ### `tera-engine` +//! +//! Provides bindings to the `tera` crate (a template engine) under `mail::tera`. +//! This feature uses the `render-template-engine` feature. +//! +//! ### `traceing` +//! +//! Enables the `traceing` debugging functionality in the `EncodingBuffer` +//! from `mail-internals`, this is only used for testing header implementations +//! and comes with noticeable overhead. **As such this should not be enabled |