diff --git a/CHANGELOG.md b/CHANGELOG.md index dbfc889..5dccd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move feature [#31] - Delete feature [#36] - Signature support [#33] +- Add attachment(s) to a message [#37] ### Changed @@ -92,6 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#33]: https://github.com/soywod/himalaya/issues/33 [#34]: https://github.com/soywod/himalaya/issues/34 [#35]: https://github.com/soywod/himalaya/issues/35 +[#37]: https://github.com/soywod/himalaya/issues/37 [#38]: https://github.com/soywod/himalaya/issues/38 [#39]: https://github.com/soywod/himalaya/issues/39 [#40]: https://github.com/soywod/himalaya/issues/40 diff --git a/Cargo.lock b/Cargo.lock index ab4bb28..1f7c464 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ - "memchr", + "memchr 2.3.4", ] [[package]] @@ -183,6 +183,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "core-foundation" version = "0.9.1" @@ -218,6 +227,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + [[package]] name = "fnv" version = "1.0.7" @@ -256,12 +271,29 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + [[package]] name = "gimli" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + [[package]] name = "hermit-abi" version = "0.1.17" @@ -286,6 +318,7 @@ dependencies = [ "serde_json", "terminal_size", "toml", + "tree_magic", "uuid", ] @@ -377,6 +410,16 @@ dependencies = [ "nom 5.1.2", ] +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.9" @@ -406,9 +449,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.0-alpha.4" +version = "0.10.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc8c2fc7873920aca23647e5e86d44ff3f40bbc5a5efaab445c9eb0e001c9f71" +checksum = "897171ed0e63da84c988b157106ad8b6532d7499aeeec906ce46b05415cc79d3" dependencies = [ "base64 0.13.0", "hostname", @@ -420,10 +463,8 @@ dependencies = [ "once_cell", "quoted_printable", "r2d2", - "rand", + "rand 0.8.3", "regex", - "serde", - "serde_json", "uuid", ] @@ -446,6 +487,15 @@ version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.2" @@ -487,6 +537,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.3.4" @@ -527,6 +586,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" +dependencies = [ + "memchr 1.0.2", +] + [[package]] name = "nom" version = "5.1.2" @@ -534,7 +602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" dependencies = [ "lexical-core", - "memchr", + "memchr 2.3.4", "version_check", ] @@ -545,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" dependencies = [ "bitvec", - "memchr", + "memchr 2.3.4", "version_check", ] @@ -613,6 +681,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -620,8 +698,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api", - "parking_lot_core", + "lock_api 0.4.2", + "parking_lot_core 0.8.2", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -644,6 +736,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pkg-config" version = "0.3.19" @@ -687,7 +789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" dependencies = [ "log", - "parking_lot", + "parking_lot 0.11.1", "scheduled-thread-pool", ] @@ -703,11 +805,23 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.15", "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.2", + "rand_hc 0.3.0", ] [[package]] @@ -717,7 +831,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.2", ] [[package]] @@ -726,7 +850,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.15", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom 0.2.2", ] [[package]] @@ -735,7 +868,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.2", ] [[package]] @@ -751,7 +893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" dependencies = [ "aho-corasick", - "memchr", + "memchr 2.3.4", "regex-syntax", "thread_local", ] @@ -810,7 +952,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" dependencies = [ - "parking_lot", + "parking_lot 0.11.1", ] [[package]] @@ -916,7 +1058,7 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ "cfg-if 0.1.10", "libc", - "rand", + "rand 0.7.3", "redox_syscall", "remove_dir_all", "winapi", @@ -985,6 +1127,19 @@ dependencies = [ "serde", ] +[[package]] +name = "tree_magic" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d99367ce3e553a84738f73bd626ccca541ef90ae757fdcdc4cbe728e6cb629" +dependencies = [ + "fnv", + "lazy_static", + "nom 3.2.1", + "parking_lot 0.10.2", + "petgraph", +] + [[package]] name = "unicase" version = "2.6.0" @@ -1030,7 +1185,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" dependencies = [ - "rand", + "rand 0.7.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 25ed7bc..4e0e2da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" clap = "2.33.3" error-chain = "0.12.4" imap = "2.4.0" -lettre = "0.10.0-alpha.4" +lettre = "0.10.0-beta.3" mailparse = "0.13.1" native-tls = "0.2" rfc2047-decoder = "0.1.2" @@ -17,4 +17,5 @@ serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" terminal_size = "0.1.15" toml = "0.5.8" +tree_magic = "0.2.3" uuid = { version = "0.8", features = ["v4"] } diff --git a/src/msg/cli.rs b/src/msg/cli.rs index bfa5929..44a42a6 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -56,6 +56,16 @@ fn page_arg<'a>() -> Arg<'a, 'a> { .default_value("0") } +fn attachment_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("attachments") + .help("Adds attachment to the message") + .short("a") + .long("attachment") + .value_name("PATH") + .multiple(true) + .takes_value(true) +} + pub fn msg_subcmds<'a>() -> Vec> { vec![ SubCommand::with_name("list") @@ -75,7 +85,9 @@ pub fn msg_subcmds<'a>() -> Vec> { .multiple(true) .required(true), ), - SubCommand::with_name("write").about("Writes a new message"), + SubCommand::with_name("write") + .about("Writes a new message") + .arg(attachment_arg()), SubCommand::with_name("send") .about("Sends a raw message") .arg(Arg::with_name("message").raw(true)), @@ -247,10 +259,16 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { break; } - if let Some(_) = matches.subcommand_matches("write") { + if let Some(matches) = matches.subcommand_matches("write") { + let attachments = matches + .values_of("attachments") + .unwrap_or_default() + .map(String::from) + .collect::>(); let tpl = Msg::build_new_tpl(&config, &account)?; let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; let mut msg = Msg::from(content); + msg.attachments = attachments; loop { match input::post_edit_choice() { @@ -317,6 +335,11 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("reply") { + let attachments = matches + .values_of("attachments") + .unwrap_or_default() + .map(String::from) + .collect::>(); let uid = matches.value_of("uid").unwrap(); let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); @@ -328,6 +351,7 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; let mut msg = Msg::from(content); + msg.attachments = attachments; loop { match input::post_edit_choice() { @@ -360,12 +384,18 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("forward") { + let attachments = matches + .values_of("attachments") + .unwrap_or_default() + .map(String::from) + .collect::>(); let uid = matches.value_of("uid").unwrap(); let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = msg.build_forward_tpl(&config, &account)?; let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; let mut msg = Msg::from(content); + msg.attachments = attachments; loop { match input::post_edit_choice() { diff --git a/src/msg/model.rs b/src/msg/model.rs index 31c5523..1e8bac0 100644 --- a/src/msg/model.rs +++ b/src/msg/model.rs @@ -6,7 +6,8 @@ use serde::{ ser::{self, SerializeStruct}, Serialize, }; -use std::{fmt, result}; +use std::{borrow::Cow, fmt, fs, path::PathBuf, result}; +use tree_magic; use uuid::Uuid; use crate::config::model::{Account, Config}; @@ -171,25 +172,6 @@ impl<'a> ReadableMsg { // Message -// #[derive(Debug, Serialize, PartialEq)] -// #[serde(rename_all = "lowercase")] -// pub enum Flag { -// Seen, -// Answered, -// Flagged, -// } - -// impl Flag { -// fn from_imap_flag(flag: &imap::types::Flag<'_>) -> Option { -// match flag { -// imap::types::Flag::Seen => Some(Self::Seen), -// imap::types::Flag::Answered => Some(Self::Answered), -// imap::types::Flag::Flagged => Some(Self::Flagged), -// _ => None, -// } -// } -// } - #[derive(Debug, Serialize)] pub struct Msg<'m> { pub uid: u32, @@ -197,7 +179,8 @@ pub struct Msg<'m> { pub subject: String, pub sender: String, pub date: String, - + #[serde(skip_serializing)] + pub attachments: Vec, #[serde(skip_serializing)] raw: Vec, } @@ -210,6 +193,7 @@ impl<'m> From> for Msg<'m> { subject: String::from(""), sender: String::from(""), date: String::from(""), + attachments: vec![], raw, } } @@ -242,6 +226,7 @@ impl<'m> From<&'m imap::types::Fetch> for Msg<'m> { .internal_date() .map(|date| date.naive_local().to_string()) .unwrap_or_default(), + attachments: vec![], raw: fetch.body().unwrap_or_default().to_vec(), }, } @@ -263,52 +248,85 @@ impl<'m> Msg<'m> { } pub fn to_sendable_msg(&self) -> Result { - use lettre::message::header::{ContentTransferEncoding, ContentType}; - use lettre::message::{Message, SinglePart}; + use lettre::message::{ + header::*, + {Body, Message, MultiPart, SinglePart}, + }; let parsed = self.parse()?; - let msg = parsed - .headers - .iter() - .fold(Message::builder(), |msg, h| { - let value = String::from_utf8(h.get_value_raw().to_vec()) - .unwrap() - .replace("\r", ""); + let msg_builder = parsed.headers.iter().fold(Message::builder(), |msg, h| { + let value = String::from_utf8(h.get_value_raw().to_vec()) + .unwrap() + .replace("\r", ""); - match h.get_key().to_lowercase().as_str() { - "in-reply-to" => msg.in_reply_to(value.parse().unwrap()), - "from" => match value.parse() { - Ok(addr) => msg.from(addr), + match h.get_key().to_lowercase().as_str() { + "in-reply-to" => msg.in_reply_to(value.parse().unwrap()), + "from" => match value.parse() { + Ok(addr) => msg.from(addr), + Err(_) => msg, + }, + "to" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.to(addr), Err(_) => msg, - }, - "to" => value - .split(",") - .fold(msg, |msg, addr| match addr.trim().parse() { - Ok(addr) => msg.to(addr), - Err(_) => msg, - }), - "cc" => value - .split(",") - .fold(msg, |msg, addr| match addr.trim().parse() { - Ok(addr) => msg.cc(addr), - Err(_) => msg, - }), - "bcc" => value - .split(",") - .fold(msg, |msg, addr| match addr.trim().parse() { - Ok(addr) => msg.bcc(addr), - Err(_) => msg, - }), - "subject" => msg.subject(value), - _ => msg, - } - }) - .singlepart( - SinglePart::builder() - .header(ContentType("text/plain; charset=utf-8".parse().unwrap())) - .header(ContentTransferEncoding::Base64) - .body(parsed.get_body_raw()?), - )?; + }), + "cc" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.cc(addr), + Err(_) => msg, + }), + "bcc" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.bcc(addr), + Err(_) => msg, + }), + "subject" => msg.subject(value), + _ => msg, + } + }); + + let text_part = SinglePart::builder() + .header(ContentType("text/plain; charset=utf-8".parse().unwrap())) + .header(ContentTransferEncoding::Base64) + .body(parsed.get_body_raw()?); + + let msg = if self.attachments.is_empty() { + msg_builder.singlepart(text_part) + } else { + let mut parts = MultiPart::mixed().singlepart(text_part); + + for attachment in &self.attachments { + let attachment_name = PathBuf::from(attachment); + let attachment_name = attachment_name + .file_name() + .map(|fname| fname.to_string_lossy()) + .unwrap_or(Cow::from(Uuid::new_v4().to_string())); + let attachment_content = fs::read(attachment) + .chain_err(|| format!("Cannot read attachment `{}`", attachment))?; + let attachment_ctype = tree_magic::from_u8(&attachment_content); + + parts = parts.singlepart( + SinglePart::builder() + .header(ContentType(attachment_ctype.parse().chain_err(|| { + format!("Could not parse content type `{}`", attachment_ctype) + })?)) + .header(ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Filename( + Charset::Ext("utf-8".into()), + None, + attachment_name.as_bytes().into(), + )], + }) + .body(Body::new(attachment_content)), + ); + } + + msg_builder.multipart(parts) + }?; Ok(msg) }