diff --git a/CHANGELOG.md b/CHANGELOG.md index ca64fd3..cdc6280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,13 +16,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set up CLI arg parser [#15] - List mailboxes command [#5] - Text and HTML previews [#12] [#13] +- Set up SMTP connection [#4] +- Write new email [#8] [unreleased]: https://github.com/soywod/himalaya [#1]: https://github.com/soywod/himalaya/issues/1 [#2]: https://github.com/soywod/himalaya/issues/2 [#3]: https://github.com/soywod/himalaya/issues/3 +[#4]: https://github.com/soywod/himalaya/issues/4 [#5]: https://github.com/soywod/himalaya/issues/5 +[#8]: https://github.com/soywod/himalaya/issues/8 [#12]: https://github.com/soywod/himalaya/issues/12 [#13]: https://github.com/soywod/himalaya/issues/13 [#15]: https://github.com/soywod/himalaya/issues/15 diff --git a/Cargo.lock b/Cargo.lock index 3d24b10..1345b5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "bufstream" version = "0.1.4" @@ -80,6 +92,12 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "cc" version = "1.0.66" @@ -161,6 +179,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -176,6 +200,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "getrandom" version = "0.1.15" @@ -202,6 +232,7 @@ version = "0.1.0" dependencies = [ "clap", "imap", + "lettre", "mailparse", "native-tls", "rfc2047-decoder", @@ -209,6 +240,69 @@ dependencies = [ "toml", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "hyperx" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2adce67e2c21cd95288ae3d9f2bbb2762cf17c03744628d49679f315ed1e2e58" +dependencies = [ + "base64 0.13.0", + "bytes", + "http", + "httparse", + "httpdate", + "language-tags", + "log", + "mime", + "percent-encoding", + "unicase", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "imap" version = "2.4.0" @@ -221,7 +315,7 @@ dependencies = [ "imap-proto", "lazy_static", "native-tls", - "nom", + "nom 5.1.2", "regex", ] @@ -231,15 +325,59 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f" dependencies = [ - "nom", + "nom 5.1.2", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lettre" +version = "0.10.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8c2fc7873920aca23647e5e86d44ff3f40bbc5a5efaab445c9eb0e001c9f71" +dependencies = [ + "base64 0.13.0", + "hostname", + "hyperx", + "idna", + "mime", + "native-tls", + "nom 6.0.1", + "once_cell", + "quoted_printable", + "r2d2", + "rand", + "regex", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "lexical-core" version = "0.7.4" @@ -259,6 +397,15 @@ version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.11" @@ -279,12 +426,30 @@ dependencies = [ "quoted_printable", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + [[package]] name = "memchr" version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "native-tls" version = "0.2.6" @@ -314,6 +479,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" +dependencies = [ + "bitvec", + "memchr", + "version_check", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -333,6 +509,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + [[package]] name = "openssl" version = "0.10.32" @@ -366,6 +548,37 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pkg-config" version = "0.3.19" @@ -402,6 +615,23 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.7.3" @@ -503,6 +733,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "security-framework" version = "2.0.0" @@ -546,6 +791,23 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "smallvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0" + [[package]] name = "static_assertions" version = "1.1.0" @@ -569,6 +831,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" + [[package]] name = "tempfile" version = "3.1.0" @@ -612,6 +880,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.8" @@ -621,6 +904,33 @@ dependencies = [ "serde", ] +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.8" @@ -633,6 +943,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" +dependencies = [ + "rand", +] + [[package]] name = "vcpkg" version = "0.2.11" @@ -684,3 +1003,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml index faa443a..c814f3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] clap = "2.33.3" imap = "2.4.0" +lettre = "0.10.0-alpha.4" mailparse = "0.13.1" native-tls = "0.2" rfc2047-decoder = "0.1.2" diff --git a/src/config.rs b/src/config.rs index a0d488b..128fb5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,12 @@ pub struct Config { pub smtp: ServerInfo, } +impl Config { + pub fn email_full(&self) -> String { + format!("{} <{}>", self.name, self.email) + } +} + pub fn from_xdg() -> Option { match env::var("XDG_CONFIG_HOME") { Err(_) => None, diff --git a/src/main.rs b/src/main.rs index 951e300..39195cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,15 @@ mod config; mod imap; +mod smtp; mod table; use clap::{App, Arg, SubCommand}; +use std::io::prelude::*; +use std::{env, fs, process}; + +fn nem_email_tpl() -> String { + ["To: ", "Subject: ", ""].join("\r\n") +} fn mailbox_arg() -> Arg<'static, 'static> { Arg::with_name("mailbox") @@ -77,6 +84,10 @@ fn main() { ) .get_matches(); + if let Some(_) = matches.subcommand_matches("list") { + imap::list_mailboxes(&mut imap_sess).unwrap(); + } + if let Some(matches) = matches.subcommand_matches("search") { let mbox = matches.value_of("mailbox").unwrap(); @@ -108,10 +119,6 @@ fn main() { } } - if let Some(_) = matches.subcommand_matches("list") { - imap::list_mailboxes(&mut imap_sess).unwrap(); - } - if let Some(matches) = matches.subcommand_matches("read") { let mbox = matches.value_of("mailbox").unwrap(); let mime = matches.value_of("mime-type").unwrap(); @@ -120,4 +127,29 @@ fn main() { imap::read_email(&mut imap_sess, mbox, uid, mime).unwrap(); } } + + if let Some(_) = matches.subcommand_matches("write") { + let mut draft_path = env::temp_dir(); + draft_path.push("himalaya-draft.mail"); + + fs::File::create(&draft_path) + .expect("Could not create draft file") + .write(nem_email_tpl().as_bytes()) + .expect("Could not write into draft file"); + + process::Command::new(env!("EDITOR")) + .arg(&draft_path) + .status() + .expect("Could not start $EDITOR"); + + let mut draft = String::new(); + fs::File::open(&draft_path) + .expect("Could not open draft file") + .read_to_string(&mut draft) + .expect("Could not read draft file"); + + fs::remove_file(&draft_path).expect("Could not remove draft file"); + + smtp::send(&config, &draft.as_bytes()); + } } diff --git a/src/smtp.rs b/src/smtp.rs new file mode 100644 index 0000000..5751301 --- /dev/null +++ b/src/smtp.rs @@ -0,0 +1,52 @@ +use lettre::{ + message::{header, Message, SinglePart}, + transport::smtp::{authentication::Credentials, SmtpTransport}, + Transport, +}; +use mailparse; + +use crate::config; + +pub fn send(config: &config::Config, bytes: &[u8]) { + let email_origin = mailparse::parse_mail(bytes).unwrap(); + let email = email_origin + .headers + .iter() + .fold(Message::builder(), |msg, h| { + match h.get_key().to_lowercase().as_str() { + "to" => msg.to(h.get_value().parse().unwrap()), + "cc" => match h.get_value().parse() { + Err(_) => msg, + Ok(addr) => msg.cc(addr), + }, + "bcc" => match h.get_value().parse() { + Err(_) => msg, + Ok(addr) => msg.bcc(addr), + }, + "subject" => msg.subject(h.get_value()), + _ => msg, + } + }) + .from(config.email_full().parse().unwrap()) + .singlepart( + SinglePart::builder() + .header(header::ContentType( + "text/plain; charset=utf-8".parse().unwrap(), + )) + .header(header::ContentTransferEncoding::Base64) + .body(email_origin.get_body_raw().unwrap()), + ) + .unwrap(); + + let creds = Credentials::new(config.smtp.login.clone(), config.smtp.password.clone()); + let mailer = SmtpTransport::relay(&config.smtp.host) + .unwrap() + .credentials(creds) + .build(); + + println!("Sending ..."); + match mailer.send(&email) { + Ok(_) => println!("Email sent successfully!"), + Err(e) => panic!("Could not send email: {:?}", e), + } +}