implement add attachement to msg feature

This commit is contained in:
Clément DOUIN 2021-04-03 22:30:57 +02:00
parent cd48879dc3
commit 2850ae01fd
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
5 changed files with 294 additions and 88 deletions

View file

@ -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

199
Cargo.lock generated
View file

@ -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]]

View file

@ -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"] }

View file

@ -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<App<'a, 'a>> {
vec![
SubCommand::with_name("list")
@ -75,7 +85,9 @@ pub fn msg_subcmds<'a>() -> Vec<App<'a, 'a>> {
.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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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() {

View file

@ -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<Self> {
// 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<String>,
#[serde(skip_serializing)]
raw: Vec<u8>,
}
@ -210,6 +193,7 @@ impl<'m> From<Vec<u8>> 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<lettre::Message> {
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)
}