summaryrefslogtreecommitdiffstats
path: root/mail
diff options
context:
space:
mode:
authorPhilipp Korber <p.korber@1aim.com>2018-11-16 15:46:43 +0100
committerPhilipp Korber <p.korber@1aim.com>2018-11-16 15:46:43 +0100
commit652d6f0ffeee7302a2cb51059bef75d8b0bb50be (patch)
treec3851592642938172f280f7428d43e08b0fe2cbe /mail
parent0947fe8996149fe20a6d47a793f9555790eb2eae (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.toml42
-rw-r--r--mail/README.md117
-rw-r--r--mail/example_resources/templates/template_a/html/logo.pngbin0 -> 343 bytes
-rw-r--r--mail/example_resources/templates/template_a/html/mail.html5
-rw-r--r--mail/example_resources/templates/template_a/portfolio.pdfbin0 -> 31571 bytes
-rw-r--r--mail/example_resources/templates/template_a/text/mail.txt1
-rw-r--r--mail/example_resources/tera_base/base_mail.html10
-rw-r--r--mail/examples/mail_by_hand.rs84
-rw-r--r--mail/examples/mail_from_template/error.rs56
-rw-r--r--mail/examples/mail_from_template/main.rs99
-rw-r--r--mail/examples/send_mail/cli.rs212
-rw-r--r--mail/examples/send_mail/main.rs114
-rw-r--r--mail/src/lib.rs193
-rw-r--r--mail/src/macros.rs33
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 &emsp;
+
+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
new file mode 100644
index 0000000..f58bea6
--- /dev/null
+++ b/mail/example_resources/templates/template_a/html/logo.png
Binary files differ
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
new file mode 100644
index 0000000..3748b05
--- /dev/null
+++ b/mail/example_resources/templates/template_a/portfolio.pdf
Binary files differ
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