From 19f4483a3e96343d5f9fd7b6e32c7f78b90da097 Mon Sep 17 00:00:00 2001 From: Oskar <32576280+okrplay@users.noreply.github.com> Date: Fri, 4 Mar 2022 18:15:26 +0100 Subject: [PATCH 01/18] fix replies (#323) --- src/msg/msg_entity.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 6c2be35..3d53b0b 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -164,12 +164,12 @@ impl Msg { pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result { let account_addr = account.address()?; - // Message-Id - self.message_id = None; - // In-Reply-To self.in_reply_to = self.message_id.to_owned(); + // Message-Id + self.message_id = None; + // To let addrs = self .reply_to From 130ed24a5a9bba27cb00306e5f28339e2ac212df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 4 Mar 2022 21:50:09 +0100 Subject: [PATCH 02/18] fix missing or invalid cc when replying to a message (#324) I also added tests for the `Msg::into_reply` method and made the `Msg::merge_with` stricter. --- CHANGELOG.md | 7 ++ src/msg/msg_entity.rs | 215 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 196 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904b816..b087809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `In-Reply-To` not set properly when replying to a message [#323] +- `Cc` missing or invalid when replying to a message [#324] + ## [0.5.8] - 2022-03-04 ### Added @@ -480,3 +485,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#309]: https://github.com/soywod/himalaya/issues/309 [#318]: https://github.com/soywod/himalaya/issues/318 [#321]: https://github.com/soywod/himalaya/issues/321 +[#323]: https://github.com/soywod/himalaya/issues/323 +[#324]: https://github.com/soywod/himalaya/issues/324 diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 3d53b0b..dca6d51 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -13,7 +13,7 @@ use crate::{ config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM}, msg::{ from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils, - Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, + Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, }, output::PrinterService, smtp::SmtpService, @@ -176,10 +176,13 @@ impl Msg { .as_deref() .or_else(|| self.from.as_deref()) .map(|addrs| { - addrs - .clone() - .into_iter() - .filter(|addr| addr != &account_addr) + addrs.iter().cloned().filter(|addr| match addr { + Addr::Group(_) => false, + Addr::Single(a) => match &account_addr { + Addr::Group(_) => false, + Addr::Single(b) => a.addr != b.addr, + }, + }) }); if all { self.to = addrs.map(|addrs| addrs.collect::>().into()); @@ -189,11 +192,28 @@ impl Msg { .map(|addr| vec![addr].into()); } - // Cc & Bcc - if !all { - self.cc = None; - self.bcc = None; - } + // Cc + self.cc = if all { + self.cc.as_deref().map(|addrs| { + addrs + .iter() + .cloned() + .filter(|addr| match addr { + Addr::Group(_) => false, + Addr::Single(a) => match &account_addr { + Addr::Group(_) => false, + Addr::Single(b) => a.addr != b.addr, + }, + }) + .collect::>() + .into() + }) + } else { + None + }; + + // Bcc + self.bcc = None; // Body let plain_content = { @@ -413,24 +433,19 @@ impl Msg { } pub fn merge_with(&mut self, msg: Msg) { - if msg.from.is_some() { - self.from = msg.from; + self.from = msg.from; + self.reply_to = msg.reply_to; + self.to = msg.to; + self.cc = msg.cc; + self.bcc = msg.bcc; + self.subject = msg.subject; + + if msg.message_id.is_some() { + self.message_id = msg.message_id; } - if msg.to.is_some() { - self.to = msg.to; - } - - if msg.cc.is_some() { - self.cc = msg.cc; - } - - if msg.bcc.is_some() { - self.bcc = msg.bcc; - } - - if !msg.subject.is_empty() { - self.subject = msg.subject; + if msg.in_reply_to.is_some() { + self.in_reply_to = msg.in_reply_to; } for part in msg.parts.0.into_iter() { @@ -707,3 +722,151 @@ impl TryInto for Msg { Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?) } } + +#[cfg(test)] +mod tests { + use mailparse::SingleInfo; + + use crate::msg::Addr; + + use super::*; + + #[test] + fn test_into_reply() { + let config = AccountConfig { + display_name: "Test".into(), + email: "test-account@local".into(), + ..AccountConfig::default() + }; + + // Checks that: + // - "message_id" moves to "in_reply_to" + // - "subject" starts by "Re: " + // - "to" is replaced by "from" + // - "from" is replaced by the address from the account config + + let msg = Msg { + message_id: Some("msg-id".into()), + subject: "subject".into(), + from: Some( + vec![Addr::Single(SingleInfo { + addr: "test-sender@local".into(), + display_name: None, + })] + .into(), + ), + ..Msg::default() + } + .into_reply(false, &config) + .unwrap(); + + assert_eq!(msg.message_id, None); + assert_eq!(msg.in_reply_to.unwrap(), "msg-id"); + assert_eq!(msg.subject, "Re: subject"); + assert_eq!( + msg.from.unwrap().to_string(), + "\"Test\" " + ); + assert_eq!(msg.to.unwrap().to_string(), "test-sender@local"); + + // Checks that: + // - "subject" does not contains additional "Re: " + // - "to" is replaced by reply_to + // - "to" contains one address when "all" is false + // - "cc" are empty when "all" is false + + let msg = Msg { + subject: "Re: subject".into(), + from: Some( + vec![Addr::Single(SingleInfo { + addr: "test-sender@local".into(), + display_name: None, + })] + .into(), + ), + reply_to: Some( + vec![ + Addr::Single(SingleInfo { + addr: "test-sender-to-reply@local".into(), + display_name: Some("Sender".into()), + }), + Addr::Single(SingleInfo { + addr: "test-sender-to-reply-2@local".into(), + display_name: Some("Sender 2".into()), + }), + ] + .into(), + ), + cc: Some( + vec![Addr::Single(SingleInfo { + addr: "test-cc@local".into(), + display_name: None, + })] + .into(), + ), + ..Msg::default() + } + .into_reply(false, &config) + .unwrap(); + + assert_eq!(msg.subject, "Re: subject"); + assert_eq!( + msg.to.unwrap().to_string(), + "\"Sender\" " + ); + assert_eq!(msg.cc, None); + + // Checks that: + // - "to" contains all addresses except for the sender when "all" is true + // - "cc" contains all addresses except for the sender when "all" is true + + let msg = Msg { + from: Some( + vec![ + Addr::Single(SingleInfo { + addr: "test-sender-1@local".into(), + display_name: Some("Sender 1".into()), + }), + Addr::Single(SingleInfo { + addr: "test-sender-2@local".into(), + display_name: Some("Sender 2".into()), + }), + Addr::Single(SingleInfo { + addr: "test-account@local".into(), + display_name: Some("Test".into()), + }), + ] + .into(), + ), + cc: Some( + vec![ + Addr::Single(SingleInfo { + addr: "test-sender-1@local".into(), + display_name: Some("Sender 1".into()), + }), + Addr::Single(SingleInfo { + addr: "test-sender-2@local".into(), + display_name: Some("Sender 2".into()), + }), + Addr::Single(SingleInfo { + addr: "test-account@local".into(), + display_name: None, + }), + ] + .into(), + ), + ..Msg::default() + } + .into_reply(true, &config) + .unwrap(); + + assert_eq!( + msg.to.unwrap().to_string(), + "\"Sender 1\" , \"Sender 2\" " + ); + assert_eq!( + msg.cc.unwrap().to_string(), + "\"Sender 1\" , \"Sender 2\" " + ); + } +} From 212f5e6eb13f02dc6b55aaaefc6a2310fd90f492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 4 Mar 2022 23:05:01 +0100 Subject: [PATCH 03/18] improve attachments command (#281) Also fixed a printer typo. --- CHANGELOG.md | 5 ++ src/backends/imap/imap_envelope.rs | 8 ++-- src/backends/imap/imap_mbox.rs | 8 ++-- src/backends/maildir/maildir_envelope.rs | 8 ++-- src/backends/maildir/maildir_mbox.rs | 8 ++-- src/backends/notmuch/notmuch_envelope.rs | 8 ++-- src/backends/notmuch/notmuch_mbox.rs | 8 ++-- src/config/account.rs | 8 ++-- src/config/account_handlers.rs | 22 +++++---- src/mbox/mbox_handlers.rs | 22 +++++---- src/msg/flag_handlers.rs | 6 +-- src/msg/msg_entity.rs | 7 +-- src/msg/msg_handlers.rs | 32 ++++++++----- src/msg/tpl_handlers.rs | 10 ++-- src/output/print.rs | 14 ++---- src/output/print_table.rs | 2 +- src/output/printer_service.rs | 32 ++++++++----- src/ui/table.rs | 58 ++++++++++++------------ 18 files changed, 148 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b087809..9d04c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Improve `attachments` command [#281] + ### Fixed - `In-Reply-To` not set properly when replying to a message [#323] @@ -475,6 +479,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#271]: https://github.com/soywod/himalaya/issues/271 [#276]: https://github.com/soywod/himalaya/issues/276 [#280]: https://github.com/soywod/himalaya/issues/280 +[#281]: https://github.com/soywod/himalaya/issues/281 [#288]: https://github.com/soywod/himalaya/issues/288 [#289]: https://github.com/soywod/himalaya/issues/289 [#298]: https://github.com/soywod/himalaya/issues/298 diff --git a/src/backends/imap/imap_envelope.rs b/src/backends/imap/imap_envelope.rs index 68b8d74..323df8f 100644 --- a/src/backends/imap/imap_envelope.rs +++ b/src/backends/imap/imap_envelope.rs @@ -26,10 +26,10 @@ impl Deref for ImapEnvelopes { } impl PrintTable for ImapEnvelopes { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/backends/imap/imap_mbox.rs b/src/backends/imap/imap_mbox.rs index 2fdc098..054c496 100644 --- a/src/backends/imap/imap_mbox.rs +++ b/src/backends/imap/imap_mbox.rs @@ -28,10 +28,10 @@ impl Deref for ImapMboxes { } impl PrintTable for ImapMboxes { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/backends/maildir/maildir_envelope.rs b/src/backends/maildir/maildir_envelope.rs index 137be42..9a581e7 100644 --- a/src/backends/maildir/maildir_envelope.rs +++ b/src/backends/maildir/maildir_envelope.rs @@ -37,10 +37,10 @@ impl DerefMut for MaildirEnvelopes { } impl PrintTable for MaildirEnvelopes { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/backends/maildir/maildir_mbox.rs b/src/backends/maildir/maildir_mbox.rs index fad90b0..946d284 100644 --- a/src/backends/maildir/maildir_mbox.rs +++ b/src/backends/maildir/maildir_mbox.rs @@ -30,10 +30,10 @@ impl Deref for MaildirMboxes { } impl PrintTable for MaildirMboxes { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/backends/notmuch/notmuch_envelope.rs b/src/backends/notmuch/notmuch_envelope.rs index 297535f..f7efc45 100644 --- a/src/backends/notmuch/notmuch_envelope.rs +++ b/src/backends/notmuch/notmuch_envelope.rs @@ -36,10 +36,10 @@ impl DerefMut for NotmuchEnvelopes { } impl PrintTable for NotmuchEnvelopes { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/backends/notmuch/notmuch_mbox.rs b/src/backends/notmuch/notmuch_mbox.rs index 6cde8b5..47a6e40 100644 --- a/src/backends/notmuch/notmuch_mbox.rs +++ b/src/backends/notmuch/notmuch_mbox.rs @@ -28,10 +28,10 @@ impl Deref for NotmuchMboxes { } impl PrintTable for NotmuchMboxes { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/config/account.rs b/src/config/account.rs index 7ecd4cc..d593b8f 100644 --- a/src/config/account.rs +++ b/src/config/account.rs @@ -31,10 +31,10 @@ impl Deref for Accounts { } impl PrintTable for Accounts { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writter)?; - Table::print(writter, self, opts)?; - writeln!(writter)?; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; Ok(()) } } diff --git a/src/config/account_handlers.rs b/src/config/account_handlers.rs index a4f42cd..4ee2c57 100644 --- a/src/config/account_handlers.rs +++ b/src/config/account_handlers.rs @@ -49,11 +49,11 @@ mod tests { #[test] fn it_should_match_cmds_accounts() { #[derive(Debug, Default, Clone)] - struct StringWritter { + struct StringWriter { content: String, } - impl io::Write for StringWritter { + impl io::Write for StringWriter { fn write(&mut self, buf: &[u8]) -> io::Result { self.content .push_str(&String::from_utf8(buf.to_vec()).unwrap()); @@ -66,7 +66,7 @@ mod tests { } } - impl termcolor::WriteColor for StringWritter { + impl termcolor::WriteColor for StringWriter { fn supports_color(&self) -> bool { false } @@ -80,11 +80,11 @@ mod tests { } } - impl WriteColor for StringWritter {} + impl WriteColor for StringWriter {} #[derive(Debug, Default)] struct PrinterServiceTest { - pub writter: StringWritter, + pub writer: StringWriter, } impl PrinterService for PrinterServiceTest { @@ -93,10 +93,16 @@ mod tests { data: Box, opts: PrintTableOpts, ) -> Result<()> { - data.print_table(&mut self.writter, opts)?; + data.print_table(&mut self.writer, opts)?; Ok(()) } - fn print(&mut self, _data: T) -> Result<()> { + fn print_str(&mut self, _data: T) -> Result<()> { + unimplemented!() + } + fn print_struct( + &mut self, + _data: T, + ) -> Result<()> { unimplemented!() } fn is_json(&self) -> bool { @@ -126,7 +132,7 @@ mod tests { "account-1 │imap │yes \n", "\n" ], - printer.writter.content + printer.writer.content ); } } diff --git a/src/mbox/mbox_handlers.rs b/src/mbox/mbox_handlers.rs index 52c36f8..44881e3 100644 --- a/src/mbox/mbox_handlers.rs +++ b/src/mbox/mbox_handlers.rs @@ -47,11 +47,11 @@ mod tests { #[test] fn it_should_list_mboxes() { #[derive(Debug, Default, Clone)] - struct StringWritter { + struct StringWriter { content: String, } - impl io::Write for StringWritter { + impl io::Write for StringWriter { fn write(&mut self, buf: &[u8]) -> io::Result { self.content .push_str(&String::from_utf8(buf.to_vec()).unwrap()); @@ -64,7 +64,7 @@ mod tests { } } - impl termcolor::WriteColor for StringWritter { + impl termcolor::WriteColor for StringWriter { fn supports_color(&self) -> bool { false } @@ -78,11 +78,11 @@ mod tests { } } - impl WriteColor for StringWritter {} + impl WriteColor for StringWriter {} #[derive(Debug, Default)] struct PrinterServiceTest { - pub writter: StringWritter, + pub writer: StringWriter, } impl PrinterService for PrinterServiceTest { @@ -91,10 +91,16 @@ mod tests { data: Box, opts: PrintTableOpts, ) -> Result<()> { - data.print_table(&mut self.writter, opts)?; + data.print_table(&mut self.writer, opts)?; Ok(()) } - fn print(&mut self, _data: T) -> Result<()> { + fn print_str(&mut self, _data: T) -> Result<()> { + unimplemented!() + } + fn print_struct( + &mut self, + _data: T, + ) -> Result<()> { unimplemented!() } fn is_json(&self) -> bool { @@ -181,7 +187,7 @@ mod tests { "/ │Sent │NoInferiors, HasNoChildren \n", "\n" ], - printer.writter.content + printer.writer.content ); } } diff --git a/src/msg/flag_handlers.rs b/src/msg/flag_handlers.rs index 18af167..33ed696 100644 --- a/src/msg/flag_handlers.rs +++ b/src/msg/flag_handlers.rs @@ -16,7 +16,7 @@ pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>( backend: Box<&'a mut B>, ) -> Result<()> { backend.add_flags(mbox, seq_range, flags)?; - printer.print(format!( + printer.print_struct(format!( "Flag(s) {:?} successfully added to message(s) {:?}", flags, seq_range )) @@ -32,7 +32,7 @@ pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>( backend: Box<&'a mut B>, ) -> Result<()> { backend.del_flags(mbox, seq_range, flags)?; - printer.print(format!( + printer.print_struct(format!( "Flag(s) {:?} successfully removed from message(s) {:?}", flags, seq_range )) @@ -48,7 +48,7 @@ pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>( backend: Box<&'a mut B>, ) -> Result<()> { backend.set_flags(mbox, seq_range, flags)?; - printer.print(format!( + printer.print_struct(format!( "Flag(s) {:?} successfully set for message(s) {:?}", flags, seq_range )) diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index dca6d51..85a8d49 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -367,7 +367,7 @@ impl Msg { .unwrap_or(DEFAULT_SENT_FOLDER); backend.add_msg(&sent_folder, &sent_msg.formatted(), "seen")?; msg_utils::remove_local_draft()?; - printer.print("Message successfully sent")?; + printer.print_struct("Message successfully sent")?; break; } Ok(PostEditChoice::Edit) => { @@ -375,7 +375,7 @@ impl Msg { continue; } Ok(PostEditChoice::LocalDraft) => { - printer.print("Message successfully saved locally")?; + printer.print_struct("Message successfully saved locally")?; break; } Ok(PostEditChoice::RemoteDraft) => { @@ -387,7 +387,8 @@ impl Msg { .unwrap_or(DEFAULT_DRAFT_FOLDER); backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; msg_utils::remove_local_draft()?; - printer.print(format!("Message successfully saved to {}", draft_folder))?; + printer + .print_struct(format!("Message successfully saved to {}", draft_folder))?; break; } Ok(PostEditChoice::Discard) => { diff --git a/src/msg/msg_handlers.rs b/src/msg/msg_handlers.rs index 69e9154..6357d35 100644 --- a/src/msg/msg_handlers.rs +++ b/src/msg/msg_handlers.rs @@ -32,21 +32,29 @@ pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( ) -> Result<()> { let attachments = backend.get_msg(mbox, seq)?.attachments(); let attachments_len = attachments.len(); - debug!( - r#"{} attachment(s) found for message "{}""#, - attachments_len, seq - ); + + if attachments_len == 0 { + return printer.print_struct(format!("No attachment found for message {:?}", seq)); + } + + printer.print_str(format!( + "Found {:?} attachment{} for message {:?}", + attachments_len, + if attachments_len > 1 { "s" } else { "" }, + seq + ))?; for attachment in attachments { let file_path = config.get_download_file_path(&attachment.filename)?; - debug!("downloading {}…", attachment.filename); + printer.print_str(format!("Downloading {:?}…", file_path))?; fs::write(&file_path, &attachment.content) .context(format!("cannot download attachment {:?}", file_path))?; } - printer.print(format!( - "{} attachment(s) successfully downloaded to {:?}", - attachments_len, config.downloads_dir + printer.print_struct(format!( + "Attachment{} successfully downloaded to {:?}", + if attachments_len > 1 { "s" } else { "" }, + config.downloads_dir )) } @@ -59,7 +67,7 @@ pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>( backend: Box<&mut B>, ) -> Result<()> { backend.copy_msg(mbox_src, mbox_dst, seq)?; - printer.print(format!( + printer.print_struct(format!( r#"Message {} successfully copied to folder "{}""#, seq, mbox_dst )) @@ -73,7 +81,7 @@ pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>( backend: Box<&'a mut B>, ) -> Result<()> { backend.del_msg(mbox, seq)?; - printer.print(format!(r#"Message(s) {} successfully deleted"#, seq)) + printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq)) } /// Forward the given message UID from the selected mailbox. @@ -189,7 +197,7 @@ pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>( backend: Box<&'a mut B>, ) -> Result<()> { backend.move_msg(mbox_src, mbox_dst, seq)?; - printer.print(format!( + printer.print_struct(format!( r#"Message {} successfully moved to folder "{}""#, seq, mbox_dst )) @@ -212,7 +220,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( msg.fold_text_parts(text_mime) }; - printer.print(msg) + printer.print_struct(msg) } /// Reply to the given message UID. diff --git a/src/msg/tpl_handlers.rs b/src/msg/tpl_handlers.rs index 45be083..712f426 100644 --- a/src/msg/tpl_handlers.rs +++ b/src/msg/tpl_handlers.rs @@ -21,7 +21,7 @@ pub fn new<'a, P: PrinterService>( printer: &'a mut P, ) -> Result<()> { let tpl = Msg::default().to_tpl(opts, account)?; - printer.print(tpl) + printer.print_struct(tpl) } /// Generate a reply message template. @@ -38,7 +38,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>( .get_msg(mbox, seq)? .into_reply(all, config)? .to_tpl(opts, config)?; - printer.print(tpl) + printer.print_struct(tpl) } /// Generate a forward message template. @@ -54,7 +54,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>( .get_msg(mbox, seq)? .into_forward(config)? .to_tpl(opts, config)?; - printer.print(tpl) + printer.print_struct(tpl) } /// Saves a message based on a template. @@ -79,7 +79,7 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?; let raw_msg = msg.into_sendable_msg(config)?.formatted(); backend.add_msg(mbox, &raw_msg, "seen")?; - printer.print("Template successfully saved") + printer.print_struct("Template successfully saved") } /// Sends a message based on a template. @@ -105,5 +105,5 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?; let sent_msg = smtp.send_msg(account, &msg)?; backend.add_msg(mbox, &sent_msg.formatted(), "seen")?; - printer.print("Template successfully sent") + printer.print_struct("Template successfully sent") } diff --git a/src/output/print.rs b/src/output/print.rs index 62f65f6..a843501 100644 --- a/src/output/print.rs +++ b/src/output/print.rs @@ -1,23 +1,19 @@ use anyhow::{Context, Result}; -use log::error; use crate::output::WriteColor; pub trait Print { - fn print(&self, writter: &mut dyn WriteColor) -> Result<()>; + fn print(&self, writer: &mut dyn WriteColor) -> Result<()>; } impl Print for &str { - fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { - writeln!(writter, "{}", self).with_context(|| { - error!(r#"cannot write string to writter: "{}""#, self); - "cannot write string to writter" - }) + fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { + writeln!(writer, "{}", self).context("cannot write string to writer") } } impl Print for String { - fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writter) + fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { + self.as_str().print(writer) } } diff --git a/src/output/print_table.rs b/src/output/print_table.rs index 489ecfa..45557b9 100644 --- a/src/output/print_table.rs +++ b/src/output/print_table.rs @@ -9,7 +9,7 @@ pub trait WriteColor: io::Write + termcolor::WriteColor {} impl WriteColor for StandardStream {} pub trait PrintTable { - fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>; + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>; } pub struct PrintTableOpts<'a> { diff --git a/src/output/printer_service.rs b/src/output/printer_service.rs index 65ee91b..cc4685a 100644 --- a/src/output/printer_service.rs +++ b/src/output/printer_service.rs @@ -9,8 +9,9 @@ use termcolor::{ColorChoice, StandardStream}; use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor}; pub trait PrinterService { - fn print(&mut self, data: T) -> Result<()>; - fn print_table( + fn print_str(&mut self, data: T) -> Result<()>; + fn print_struct(&mut self, data: T) -> Result<()>; + fn print_table( &mut self, data: Box, opts: PrintTableOpts, @@ -19,16 +20,23 @@ pub trait PrinterService { } pub struct StdoutPrinter { - pub writter: Box, + pub writer: Box, pub fmt: OutputFmt, } impl PrinterService for StdoutPrinter { - fn print(&mut self, data: T) -> Result<()> { + fn print_str(&mut self, data: T) -> Result<()> { match self.fmt { - OutputFmt::Plain => data.print(self.writter.as_mut()), - OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data)) - .context("cannot write JSON to writter"), + OutputFmt::Plain => data.print(self.writer.as_mut()), + OutputFmt::Json => Ok(()), + } + } + + fn print_struct(&mut self, data: T) -> Result<()> { + match self.fmt { + OutputFmt::Plain => data.print(self.writer.as_mut()), + OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &OutputJson::new(data)) + .context("cannot write JSON to writer"), } } @@ -38,9 +46,9 @@ impl PrinterService for StdoutPrinter { opts: PrintTableOpts, ) -> Result<()> { match self.fmt { - OutputFmt::Plain => data.print_table(self.writter.as_mut(), opts), + OutputFmt::Plain => data.print_table(self.writer.as_mut(), opts), OutputFmt::Json => { - let json = &mut serde_json::Serializer::new(self.writter.as_mut()); + let json = &mut serde_json::Serializer::new(self.writer.as_mut()); let ser = &mut ::erase(json); data.erased_serialize(ser).unwrap(); Ok(()) @@ -55,7 +63,7 @@ impl PrinterService for StdoutPrinter { impl From for StdoutPrinter { fn from(fmt: OutputFmt) -> Self { - let writter = StandardStream::stdout(if atty::isnt(Stream::Stdin) { + let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) { // Colors should be deactivated if the terminal is not a tty. ColorChoice::Never } else { @@ -67,8 +75,8 @@ impl From for StdoutPrinter { // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection ColorChoice::Auto }); - let writter = Box::new(writter); - Self { writter, fmt } + let writer = Box::new(writer); + Self { writer, fmt } } } diff --git a/src/ui/table.rs b/src/ui/table.rs index 137c80e..0c97a13 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -127,14 +127,14 @@ impl Cell { /// Makes the cell printable. impl Print for Cell { - fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { + fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { // Applies colors to the cell - writter + writer .set_color(&self.style) .context(format!(r#"cannot apply colors to cell "{}""#, self.value))?; // Writes the colorized cell to stdout - write!(writter, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value)) + write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value)) } } @@ -167,8 +167,8 @@ where /// Defines the row template. fn row(&self) -> Row; - /// Writes the table to the writter. - fn print(writter: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> { + /// Writes the table to the writer. + fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> { let is_format_flowed = matches!(opts.format, Format::Flowed); let max_width = match opts.format { Format::Fixed(width) => opts.max_width.unwrap_or(*width), @@ -202,7 +202,7 @@ where for row in table.iter_mut() { let mut glue = Cell::default(); for (i, cell) in row.0.iter_mut().enumerate() { - glue.print(writter)?; + glue.print(writer)?; let table_is_overflowing = table_width > max_width; if table_is_overflowing && !is_format_flowed && cell.is_shrinkable() { @@ -256,10 +256,10 @@ where trace!("number of spaces added to value: {}", spaces_count); cell.value.push_str(&" ".repeat(spaces_count)); } - cell.print(writter)?; + cell.print(writer)?; glue = Cell::new("│").ansi_256(8); } - writeln!(writter)?; + writeln!(writer)?; } Ok(()) } @@ -272,11 +272,11 @@ mod tests { use super::*; #[derive(Debug, Default)] - struct StringWritter { + struct StringWriter { content: String, } - impl io::Write for StringWritter { + impl io::Write for StringWriter { fn write(&mut self, buf: &[u8]) -> io::Result { self.content .push_str(&String::from_utf8(buf.to_vec()).unwrap()); @@ -289,7 +289,7 @@ mod tests { } } - impl termcolor::WriteColor for StringWritter { + impl termcolor::WriteColor for StringWriter { fn supports_color(&self) -> bool { false } @@ -303,7 +303,7 @@ mod tests { } } - impl WriteColor for StringWritter {} + impl WriteColor for StringWriter {} struct Item { id: u16, @@ -338,16 +338,16 @@ mod tests { } macro_rules! write_items { - ($writter:expr, $($item:expr),*) => { - Table::print($writter, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap(); + ($writer:expr, $($item:expr),*) => { + Table::print($writer, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap(); }; } #[test] fn row_smaller_than_head() { - let mut writter = StringWritter::default(); + let mut writer = StringWriter::default(); write_items![ - &mut writter, + &mut writer, Item::new(1, "a", "aa"), Item::new(2, "b", "bb"), Item::new(3, "c", "cc") @@ -359,14 +359,14 @@ mod tests { "2 │b │bb \n", "3 │c │cc \n", ]; - assert_eq!(expected, writter.content); + assert_eq!(expected, writer.content); } #[test] fn row_bigger_than_head() { - let mut writter = StringWritter::default(); + let mut writer = StringWriter::default(); write_items![ - &mut writter, + &mut writer, Item::new(1, "a", "aa"), Item::new(2222, "bbbbb", "bbbbb"), Item::new(3, "c", "cc") @@ -378,11 +378,11 @@ mod tests { "2222 │bbbbb │bbbbb \n", "3 │c │cc \n", ]; - assert_eq!(expected, writter.content); + assert_eq!(expected, writer.content); - let mut writter = StringWritter::default(); + let mut writer = StringWriter::default(); write_items![ - &mut writter, + &mut writer, Item::new(1, "a", "aa"), Item::new(2222, "bbbbb", "bbbbb"), Item::new(3, "cccccc", "cc") @@ -394,14 +394,14 @@ mod tests { "2222 │bbbbb │bbbbb \n", "3 │cccccc │cc \n", ]; - assert_eq!(expected, writter.content); + assert_eq!(expected, writer.content); } #[test] fn basic_shrink() { - let mut writter = StringWritter::default(); + let mut writer = StringWriter::default(); write_items![ - &mut writter, + &mut writer, Item::new(1, "", "desc"), Item::new(2, "short", "desc"), Item::new(3, "loooooong", "desc"), @@ -423,14 +423,14 @@ mod tests { "7 │😍😍😍😍… │desc \n", "8 │!😍😍😍… │desc \n", ]; - assert_eq!(expected, writter.content); + assert_eq!(expected, writer.content); } #[test] fn max_shrink_width() { - let mut writter = StringWritter::default(); + let mut writer = StringWriter::default(); write_items![ - &mut writter, + &mut writer, Item::new(1111, "shriiiiiiiink", "desc very looong"), Item::new(2222, "shriiiiiiiink", "desc very loooooooooong") ]; @@ -440,6 +440,6 @@ mod tests { "1111 │shri… │desc very looong \n", "2222 │shri… │desc very loooooooooong \n", ]; - assert_eq!(expected, writter.content); + assert_eq!(expected, writer.content); } } From f79e0ae4fb4fdc9f777ecc34ae5c01f5e892be09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 5 Mar 2022 00:42:11 +0100 Subject: [PATCH 04/18] add pre-send hook (#178) --- CHANGELOG.md | 5 ++++ src/config/account_config.rs | 7 +++-- src/config/deserialized_account_config.rs | 6 +++- src/config/hooks.rs | 7 +++++ src/lib.rs | 3 ++ src/msg/msg_entity.rs | 25 ++++++++++++---- src/msg/msg_handlers.rs | 6 ++-- src/msg/tpl_handlers.rs | 4 +-- src/output/output_utils.rs | 29 ++++++++++++++++-- src/smtp/smtp_service.rs | 36 +++++++++++------------ 10 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 src/config/hooks.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d04c3d..9c659e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- SMTP pre-send hook [#178] + ### Changed - Improve `attachments` command [#281] @@ -453,6 +457,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#162]: https://github.com/soywod/himalaya/issues/162 [#176]: https://github.com/soywod/himalaya/issues/176 [#172]: https://github.com/soywod/himalaya/issues/172 +[#178]: https://github.com/soywod/himalaya/issues/178 [#181]: https://github.com/soywod/himalaya/issues/181 [#185]: https://github.com/soywod/himalaya/issues/185 [#186]: https://github.com/soywod/himalaya/issues/186 diff --git a/src/config/account_config.rs b/src/config/account_config.rs index ed01a30..d821d0c 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -36,6 +36,9 @@ pub struct AccountConfig { /// Represents mailbox aliases. pub mailboxes: HashMap, + /// Represents hooks. + pub hooks: Hooks, + /// Represents the SMTP host. pub smtp_host: String, /// Represents the SMTP port. @@ -155,6 +158,7 @@ impl<'a> AccountConfig { .to_owned(), format: base_account.format.unwrap_or_default(), mailboxes: base_account.mailboxes.clone(), + hooks: base_account.hooks.unwrap_or_default(), default: base_account.default.unwrap_or_default(), email: base_account.email.to_owned(), @@ -203,8 +207,7 @@ impl<'a> AccountConfig { /// Builds the full RFC822 compliant address of the user account. pub fn address(&self) -> Result { - let has_special_chars = - "()<>[]:;@.,".contains(|special_char| self.display_name.contains(special_char)); + let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c)); let addr = if self.display_name.is_empty() { self.email.clone() } else if has_special_chars { diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 3164ab1..84e7dff 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; -use crate::config::Format; +use crate::config::{Format, Hooks}; pub trait ToDeserializedBaseAccountConfig { fn to_base(&self) -> DeserializedBaseAccountConfig; @@ -84,6 +84,9 @@ macro_rules! make_account_config { #[serde(default)] pub mailboxes: HashMap, + /// Represents hooks. + pub hooks: Option, + $(pub $element: $ty),* } @@ -114,6 +117,7 @@ macro_rules! make_account_config { pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(), mailboxes: self.mailboxes.clone(), + hooks: self.hooks.clone(), } } } diff --git a/src/config/hooks.rs b/src/config/hooks.rs new file mode 100644 index 0000000..4bd44f0 --- /dev/null +++ b/src/config/hooks.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Hooks { + pub pre_send: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 3994874..32384d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,9 @@ pub mod config { pub mod format; pub use format::*; + + pub mod hooks; + pub use hooks::*; } pub mod compl; diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 85a8d49..edbc867 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -24,7 +24,7 @@ use crate::{ }; /// Representation of a message. -#[derive(Debug, Default)] +#[derive(Debug, Clone, Default)] pub struct Msg { /// The sequence number of the message. /// @@ -359,15 +359,18 @@ impl Msg { loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { - let sent_msg = smtp.send_msg(account, &self)?; + printer.print_str("Sending message…")?; + let sent_msg = smtp.send(account, &self)?; let sent_folder = account .mailboxes .get("sent") .map(|s| s.as_str()) .unwrap_or(DEFAULT_SENT_FOLDER); - backend.add_msg(&sent_folder, &sent_msg.formatted(), "seen")?; + printer + .print_str(format!("Adding message to the {:?} folder…", sent_folder))?; + backend.add_msg(&sent_folder, &sent_msg, "seen")?; msg_utils::remove_local_draft()?; - printer.print_struct("Message successfully sent")?; + printer.print_struct("Done!")?; break; } Ok(PostEditChoice::Edit) => { @@ -711,7 +714,19 @@ impl TryInto for Msg { type Error = Error; fn try_into(self) -> Result { - let from = match self.from.and_then(|addrs| addrs.extract_single_info()) { + (&self).try_into() + } +} + +impl TryInto for &Msg { + type Error = Error; + + fn try_into(self) -> Result { + let from = match self + .from + .as_ref() + .and_then(|addrs| addrs.clone().extract_single_info()) + { Some(addr) => addr.addr.parse().map(Some), None => Ok(None), }?; diff --git a/src/msg/msg_handlers.rs b/src/msg/msg_handlers.rs index 6357d35..d58e737 100644 --- a/src/msg/msg_handlers.rs +++ b/src/msg/msg_handlers.rs @@ -8,7 +8,6 @@ use log::{debug, info, trace}; use mailparse::addrparse; use std::{ borrow::Cow, - convert::TryInto, fs, io::{self, BufRead}, }; @@ -356,9 +355,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .join("\r\n") }; trace!("raw message: {:?}", raw_msg); - let envelope: lettre::address::Envelope = Msg::from_tpl(&raw_msg)?.try_into()?; - trace!("envelope: {:?}", envelope); - smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; + let msg = Msg::from_tpl(&raw_msg)?; + smtp.send(&config, &msg)?; backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?; Ok(()) } diff --git a/src/msg/tpl_handlers.rs b/src/msg/tpl_handlers.rs index 712f426..b2db225 100644 --- a/src/msg/tpl_handlers.rs +++ b/src/msg/tpl_handlers.rs @@ -103,7 +103,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .join("\n") }; let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?; - let sent_msg = smtp.send_msg(account, &msg)?; - backend.add_msg(mbox, &sent_msg.formatted(), "seen")?; + let sent_msg = smtp.send(account, &msg)?; + backend.add_msg(mbox, &sent_msg, "seen")?; printer.print_struct("Template successfully sent") } diff --git a/src/output/output_utils.rs b/src/output/output_utils.rs index 779089b..2053479 100644 --- a/src/output/output_utils.rs +++ b/src/output/output_utils.rs @@ -1,6 +1,9 @@ -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use log::debug; -use std::process::Command; +use std::{ + io::prelude::*, + process::{Command, Stdio}, +}; /// TODO: move this in a more approriate place. pub fn run_cmd(cmd: &str) -> Result { @@ -14,3 +17,25 @@ pub fn run_cmd(cmd: &str) -> Result { Ok(String::from_utf8(output.stdout)?) } + +pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result> { + let mut res = Vec::new(); + + let process = Command::new(cmd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .with_context(|| format!("cannot spawn process from command {:?}", cmd))?; + process + .stdin + .ok_or_else(|| anyhow!("cannot get stdin"))? + .write_all(data) + .with_context(|| "cannot write data to stdin")?; + process + .stdout + .ok_or_else(|| anyhow!("cannot get stdout"))? + .read_to_end(&mut res) + .with_context(|| "cannot read data from stdout")?; + + Ok(res) +} diff --git a/src/smtp/smtp_service.rs b/src/smtp/smtp_service.rs index 9eeccc1..35d79bd 100644 --- a/src/smtp/smtp_service.rs +++ b/src/smtp/smtp_service.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use lettre::{ self, transport::smtp::{ @@ -7,13 +7,12 @@ use lettre::{ }, Transport, }; -use log::debug; +use std::convert::TryInto; -use crate::{config::AccountConfig, msg::Msg}; +use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd}; pub trait SmtpService { - fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result; - fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>; + fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; } pub struct LettreService<'a> { @@ -21,7 +20,7 @@ pub struct LettreService<'a> { transport: Option, } -impl<'a> LettreService<'a> { +impl LettreService<'_> { fn transport(&mut self) -> Result<&SmtpTransport> { if let Some(ref transport) = self.transport { Ok(transport) @@ -55,24 +54,25 @@ impl<'a> LettreService<'a> { } } -impl<'a> SmtpService for LettreService<'a> { - fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result { - debug!("sending message…"); - let sendable_msg = msg.into_sendable_msg(account)?; - self.transport()?.send(&sendable_msg)?; - Ok(sendable_msg) - } +impl SmtpService for LettreService<'_> { + fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result> { + let envelope: lettre::address::Envelope = msg.try_into()?; + let mut msg = msg.into_sendable_msg(account)?.formatted(); - fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()> { - debug!("sending raw message…"); - self.transport()?.send_raw(envelope, msg)?; - Ok(()) + if let Some(cmd) = account.hooks.pre_send.as_deref() { + for cmd in cmd.split('|') { + msg = pipe_cmd(cmd.trim(), &msg) + .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))? + } + }; + + self.transport()?.send_raw(&envelope, &msg)?; + Ok(msg) } } impl<'a> From<&'a AccountConfig> for LettreService<'a> { fn from(account: &'a AccountConfig) -> Self { - debug!("init SMTP service"); Self { account, transport: None, From 4dc1be25cd7282b1da4f4146dd759ee9a8c885f5 Mon Sep 17 00:00:00 2001 From: "Daniel M. Capella" Date: Sun, 6 Mar 2022 08:34:25 +0000 Subject: [PATCH 05/18] fix typo sig delim in comment (#328) --- src/config/deserialized_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/deserialized_config.rs b/src/config/deserialized_config.rs index 7067162..e26b5a0 100644 --- a/src/config/deserialized_config.rs +++ b/src/config/deserialized_config.rs @@ -23,7 +23,7 @@ pub struct DeserializedConfig { pub downloads_dir: Option, /// Represents the signature of the user. pub signature: Option, - /// Overrides the default signature delimiter "`--\n `". + /// Overrides the default signature delimiter "`-- \n`". pub signature_delimiter: Option, /// Represents the default page size for listings. pub default_page_size: Option, From 3899ec9c031e4c843c927877312a7e39b7dffbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 6 Mar 2022 09:51:34 +0100 Subject: [PATCH 06/18] build smtp envelope after executing pre_send hook --- src/smtp/smtp_service.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/smtp/smtp_service.rs b/src/smtp/smtp_service.rs index 35d79bd..13ea6ce 100644 --- a/src/smtp/smtp_service.rs +++ b/src/smtp/smtp_service.rs @@ -56,18 +56,22 @@ impl LettreService<'_> { impl SmtpService for LettreService<'_> { fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result> { - let envelope: lettre::address::Envelope = msg.try_into()?; - let mut msg = msg.into_sendable_msg(account)?.formatted(); + let mut raw_msg = msg.into_sendable_msg(account)?.formatted(); - if let Some(cmd) = account.hooks.pre_send.as_deref() { - for cmd in cmd.split('|') { - msg = pipe_cmd(cmd.trim(), &msg) - .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))? - } - }; + let envelope: lettre::address::Envelope = + if let Some(cmd) = account.hooks.pre_send.as_deref() { + for cmd in cmd.split('|') { + raw_msg = pipe_cmd(cmd.trim(), &raw_msg) + .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))? + } + let parsed_mail = mailparse::parse_mail(&raw_msg)?; + Msg::from_parsed_mail(parsed_mail, account)?.try_into() + } else { + msg.try_into() + }?; - self.transport()?.send_raw(&envelope, &msg)?; - Ok(msg) + self.transport()?.send_raw(&envelope, &raw_msg)?; + Ok(raw_msg) } } From 984eb8c9f7c6374bb6279a7c9c3ae451182dc295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 8 Mar 2022 14:22:02 +0100 Subject: [PATCH 07/18] fix notmuch backend infinite loop (#329) --- CHANGELOG.md | 2 ++ src/backends/id_mapper.rs | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c659e4..db714a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `In-Reply-To` not set properly when replying to a message [#323] - `Cc` missing or invalid when replying to a message [#324] +- Notmuch backend hangs [#329] ## [0.5.8] - 2022-03-04 @@ -497,3 +498,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#321]: https://github.com/soywod/himalaya/issues/321 [#323]: https://github.com/soywod/himalaya/issues/323 [#324]: https://github.com/soywod/himalaya/issues/324 +[#329]: https://github.com/soywod/himalaya/issues/329 diff --git a/src/backends/id_mapper.rs b/src/backends/id_mapper.rs index 953a729..09a5422 100644 --- a/src/backends/id_mapper.rs +++ b/src/backends/id_mapper.rs @@ -26,7 +26,6 @@ impl IdMapper { .open(&mapper.path) .context("cannot open id hash map file")?; let reader = BufReader::new(file); - for line in reader.lines() { let line = line.context("cannot read line from maildir envelopes id mapper cache file")?; @@ -83,13 +82,13 @@ impl IdMapper { for (hash, id) in self.iter() { loop { - let short_hash = &hash[0..self.short_hash_len]; + let short_hash = &hash[0..short_hash_len]; let conflict_found = self .map .keys() .find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash) .is_some(); - if self.short_hash_len > 32 || !conflict_found { + if short_hash_len > 32 || !conflict_found { break; } short_hash_len += 1; From d79c6c40a7e3e029cac979c6cd39cc67a5da15ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 8 Mar 2022 14:29:53 +0100 Subject: [PATCH 08/18] make mdir inbox condition case insensitive --- src/backends/maildir/maildir_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 2bf3a9c..10a50a5 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -43,7 +43,7 @@ impl<'a> MaildirBackend<'a> { pub fn get_mdir_from_dir(&self, dir: &str) -> Result { // If the dir points to the inbox folder, creates a maildir // instance from the root folder. - if dir == "inbox" { + if dir.to_lowercase() == "inbox" { self.validate_mdir_path(self.mdir.path().to_owned()) .map(maildir::Maildir::from) } else { From 1f01202262b1463b63d4678dce56779553d2e237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 9 Mar 2022 09:48:23 +0100 Subject: [PATCH 09/18] check for absolute and relative maildir paths --- src/backends/maildir/maildir_backend.rs | 9 ++++++--- src/backends/maildir/maildir_envelope.rs | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 10a50a5..9038d8a 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -47,13 +47,16 @@ impl<'a> MaildirBackend<'a> { self.validate_mdir_path(self.mdir.path().to_owned()) .map(maildir::Maildir::from) } else { - // If the dir is a valid maildir path, creates a maildir instance from it. + // If the dir is a valid maildir path, creates a maildir + // instance from it. Checks for absolute path first, self.validate_mdir_path(dir.into()) + // then for relative path, + .or_else(|_| self.validate_mdir_path(self.mdir.path().join(dir))) .or_else(|_| { - // Otherwise creates a maildir instance from a + // otherwise creates a maildir instance from a // maildir subdirectory by adding a "." in front // of the name as described in the spec: - // https://cr.yp.to/proto/maildir.html + // https://cr.yp.to/proto/maildir.html. let dir = self .account_config .mailboxes diff --git a/src/backends/maildir/maildir_envelope.rs b/src/backends/maildir/maildir_envelope.rs index 9a581e7..17e064c 100644 --- a/src/backends/maildir/maildir_envelope.rs +++ b/src/backends/maildir/maildir_envelope.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Context, Error, Result}; use chrono::DateTime; -use log::{debug, info, trace}; +use log::trace; use std::{ convert::{TryFrom, TryInto}, ops::{Deref, DerefMut}, @@ -125,7 +125,7 @@ impl<'a> TryFrom for MaildirEnvelope { type Error = Error; fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result { - info!("begin: try building envelope from maildir parsed mail"); + trace!(">> build envelope from maildir parsed mail"); let mut envelope = Self::default(); @@ -139,14 +139,14 @@ impl<'a> TryFrom for MaildirEnvelope { .parsed() .context("cannot parse maildir mail entry")?; - debug!("begin: parse headers"); + trace!(">> parse headers"); for h in parsed_mail.get_headers() { let k = h.get_key(); - debug!("header key: {:?}", k); + trace!("header key: {:?}", k); let v = rfc2047_decoder::decode(h.get_value_raw()) .context(format!("cannot decode value from header {:?}", k))?; - debug!("header value: {:?}", v); + trace!("header value: {:?}", v); match k.to_lowercase().as_str() { "date" => { @@ -182,10 +182,10 @@ impl<'a> TryFrom for MaildirEnvelope { _ => (), } } - debug!("end: parse headers"); + trace!("<< parse headers"); trace!("envelope: {:?}", envelope); - info!("end: try building envelope from maildir parsed mail"); + trace!("<< build envelope from maildir parsed mail"); Ok(envelope) } } From 811ea45610ca984d4c37203b32aeffdae2f6b3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 9 Mar 2022 12:30:10 +0100 Subject: [PATCH 10/18] improve mailbox alias management --- src/backends/maildir/maildir_backend.rs | 49 ++++++++++++------------- src/config/account_config.rs | 13 +++++++ 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 9038d8a..a52dd13 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Context, Result}; use log::{debug, info, trace}; -use std::{convert::TryInto, fs, path::PathBuf}; +use std::{convert::TryInto, env, fs, path::PathBuf}; use crate::{ backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, @@ -41,33 +41,32 @@ impl<'a> MaildirBackend<'a> { /// Creates a maildir instance from a string slice. pub fn get_mdir_from_dir(&self, dir: &str) -> Result { + let dir = self.account_config.get_mbox_alias(dir)?; + // If the dir points to the inbox folder, creates a maildir // instance from the root folder. - if dir.to_lowercase() == "inbox" { - self.validate_mdir_path(self.mdir.path().to_owned()) - .map(maildir::Maildir::from) - } else { - // If the dir is a valid maildir path, creates a maildir - // instance from it. Checks for absolute path first, - self.validate_mdir_path(dir.into()) - // then for relative path, - .or_else(|_| self.validate_mdir_path(self.mdir.path().join(dir))) - .or_else(|_| { - // otherwise creates a maildir instance from a - // maildir subdirectory by adding a "." in front - // of the name as described in the spec: - // https://cr.yp.to/proto/maildir.html. - let dir = self - .account_config - .mailboxes - .get(dir) - .map(|s| s.as_str()) - .unwrap_or(dir); - let path = self.mdir.path().join(format!(".{}", dir)); - self.validate_mdir_path(path) - }) - .map(maildir::Maildir::from) + if &dir == "inbox" { + return self + .validate_mdir_path(self.mdir.path().to_owned()) + .map(maildir::Maildir::from); } + + // If the dir is a valid maildir path, creates a maildir + // instance from it. First checks for absolute path, + self.validate_mdir_path((&dir).into()) + // then for relative path to `maildir-dir`, + .or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir))) + // and finally for relative path to the current directory. + .or_else(|_| self.validate_mdir_path(env::current_dir()?.join(&dir))) + .or_else(|_| { + // Otherwise creates a maildir instance from a maildir + // subdirectory by adding a "." in front of the name + // as described in the [spec]. + // + // [spec]: http://www.courier-mta.org/imap/README.maildirquota.html + self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir))) + }) + .map(maildir::Maildir::from) } } diff --git a/src/config/account_config.rs b/src/config/account_config.rs index d821d0c..9a73e8f 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -317,6 +317,19 @@ impl<'a> AccountConfig { run_cmd(&cmd).context("cannot run notify cmd")?; Ok(()) } + + /// Gets the mailbox alias if exists, otherwise returns the + /// mailbox. Also tries to expand shell variables. + pub fn get_mbox_alias(&self, mbox: &str) -> Result { + let mbox = self + .mailboxes + .get(&mbox.trim().to_lowercase()) + .map(|s| s.as_str()) + .unwrap_or(mbox); + shellexpand::full(mbox) + .map(String::from) + .with_context(|| format!("cannot expand mailbox path {:?}", mbox)) + } } /// Represents all existing kind of account (backend). From 6b042f5e6a2b9eca1af67d394aa61dbcb8a8a875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 00:33:50 +0100 Subject: [PATCH 11/18] fix listings json api (#331) and maildir e2e tests (#335) --- CHANGELOG.md | 4 +++ src/backends/imap/imap_envelope.rs | 9 ++++--- src/backends/imap/imap_mbox.rs | 14 ++++++++--- src/backends/maildir/maildir_backend.rs | 2 +- src/backends/maildir/maildir_envelope.rs | 11 +++++--- src/backends/maildir/maildir_mbox.rs | 9 ++++--- src/backends/notmuch/notmuch_backend.rs | 12 ++++----- src/backends/notmuch/notmuch_envelope.rs | 11 +++++--- src/backends/notmuch/notmuch_mbox.rs | 7 ++++-- src/mbox/mbox_handlers.rs | 32 +++++++++++++----------- tests/test_maildir_backend.rs | 5 +--- 11 files changed, 70 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db714a6..76a52aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `In-Reply-To` not set properly when replying to a message [#323] - `Cc` missing or invalid when replying to a message [#324] - Notmuch backend hangs [#329] +- Maildir e2e tests [#335] +- JSON API for listings [#331] ## [0.5.8] - 2022-03-04 @@ -499,3 +501,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#323]: https://github.com/soywod/himalaya/issues/323 [#324]: https://github.com/soywod/himalaya/issues/324 [#329]: https://github.com/soywod/himalaya/issues/329 +[#331]: https://github.com/soywod/himalaya/issues/331 +[#335]: https://github.com/soywod/himalaya/issues/335 diff --git a/src/backends/imap/imap_envelope.rs b/src/backends/imap/imap_envelope.rs index 323df8f..d087eb1 100644 --- a/src/backends/imap/imap_envelope.rs +++ b/src/backends/imap/imap_envelope.rs @@ -15,13 +15,16 @@ use super::{ImapFlag, ImapFlags}; /// Represents a list of IMAP envelopes. #[derive(Debug, Default, serde::Serialize)] -pub struct ImapEnvelopes(pub Vec); +pub struct ImapEnvelopes { + #[serde(rename = "response")] + pub envelopes: Vec, +} impl Deref for ImapEnvelopes { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.0 + &self.envelopes } } @@ -99,7 +102,7 @@ impl TryFrom for ImapEnvelopes { for raw_envelope in raw_envelopes.iter().rev() { envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?); } - Ok(Self(envelopes)) + Ok(Self { envelopes }) } } diff --git a/src/backends/imap/imap_mbox.rs b/src/backends/imap/imap_mbox.rs index 054c496..223ab6c 100644 --- a/src/backends/imap/imap_mbox.rs +++ b/src/backends/imap/imap_mbox.rs @@ -4,6 +4,7 @@ //! to the mailbox. use anyhow::Result; +use serde::Serialize; use std::fmt::{self, Display}; use std::ops::Deref; @@ -16,14 +17,17 @@ use crate::{ use super::ImapMboxAttrs; /// Represents a list of IMAP mailboxes. -#[derive(Debug, Default, serde::Serialize)] -pub struct ImapMboxes(pub Vec); +#[derive(Debug, Default, Serialize)] +pub struct ImapMboxes { + #[serde(rename = "response")] + pub mboxes: Vec, +} impl Deref for ImapMboxes { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.0 + &self.mboxes } } @@ -130,7 +134,9 @@ pub type RawImapMboxes = imap::types::ZeroCopy>; impl<'a> From for ImapMboxes { fn from(raw_mboxes: RawImapMboxes) -> Self { - Self(raw_mboxes.iter().map(ImapMbox::from).collect()) + Self { + mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(), + } } } diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index a52dd13..a2e85a5 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -151,7 +151,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); // Applies pagination boundaries. - envelopes.0 = envelopes[page_begin..page_end].to_owned(); + envelopes.envelopes = envelopes[page_begin..page_end].to_owned(); // Appends envelopes hash to the id mapper cache file and // calculates the new short hash length. The short hash length diff --git a/src/backends/maildir/maildir_envelope.rs b/src/backends/maildir/maildir_envelope.rs index 17e064c..10cb3be 100644 --- a/src/backends/maildir/maildir_envelope.rs +++ b/src/backends/maildir/maildir_envelope.rs @@ -20,19 +20,22 @@ use crate::{ /// Represents a list of envelopes. #[derive(Debug, Default, serde::Serialize)] -pub struct MaildirEnvelopes(pub Vec); +pub struct MaildirEnvelopes { + #[serde(rename = "response")] + pub envelopes: Vec, +} impl Deref for MaildirEnvelopes { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.0 + &self.envelopes } } impl DerefMut for MaildirEnvelopes { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.envelopes } } @@ -114,7 +117,7 @@ impl<'a> TryFrom for MaildirEnvelopes { envelopes.push(envelope); } - Ok(MaildirEnvelopes(envelopes)) + Ok(MaildirEnvelopes { envelopes }) } } diff --git a/src/backends/maildir/maildir_mbox.rs b/src/backends/maildir/maildir_mbox.rs index 946d284..3f2ec2f 100644 --- a/src/backends/maildir/maildir_mbox.rs +++ b/src/backends/maildir/maildir_mbox.rs @@ -19,13 +19,16 @@ use crate::{ /// Represents a list of Maildir mailboxes. #[derive(Debug, Default, serde::Serialize)] -pub struct MaildirMboxes(pub Vec); +pub struct MaildirMboxes { + #[serde(rename = "response")] + pub mboxes: Vec, +} impl Deref for MaildirMboxes { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.0 + &self.mboxes } } @@ -113,7 +116,7 @@ impl TryFrom for MaildirMboxes { for entry in mail_entries { mboxes.push(entry?.try_into()?); } - Ok(MaildirMboxes(mboxes)) + Ok(MaildirMboxes { mboxes }) } } diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 416e1c0..37e559a 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -81,7 +81,7 @@ impl<'a> NotmuchBackend<'a> { envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); // Applies pagination boundaries. - envelopes.0 = envelopes[page_begin..page_end].to_owned(); + envelopes.envelopes = envelopes[page_begin..page_end].to_owned(); // Appends envelopes hash to the id mapper cache file and // calculates the new short hash length. The short hash length @@ -118,17 +118,17 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn get_mboxes(&mut self) -> Result> { info!(">> get notmuch virtual mailboxes"); - let mut virt_mboxes: Vec<_> = self + let mut mboxes: Vec<_> = self .account_config .mailboxes .iter() .map(|(k, v)| NotmuchMbox::new(k, v)) .collect(); - trace!("virtual mailboxes: {:?}", virt_mboxes); - virt_mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); + trace!("virtual mailboxes: {:?}", mboxes); + mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); info!("<< get notmuch virtual mailboxes"); - Ok(Box::new(NotmuchMboxes(virt_mboxes))) + Ok(Box::new(NotmuchMboxes { mboxes })) } fn del_mbox(&mut self, _mbox: &str) -> Result<()> { @@ -202,7 +202,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { // Adds the message to the maildir folder and gets its hash. let hash = self .mdir - .add_msg("inbox", msg, "seen") + .add_msg("", msg, "seen") .with_context(|| { format!( "cannot add notmuch message to maildir {:?}", diff --git a/src/backends/notmuch/notmuch_envelope.rs b/src/backends/notmuch/notmuch_envelope.rs index f7efc45..626d949 100644 --- a/src/backends/notmuch/notmuch_envelope.rs +++ b/src/backends/notmuch/notmuch_envelope.rs @@ -19,19 +19,22 @@ use crate::{ /// Represents a list of envelopes. #[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchEnvelopes(pub Vec); +pub struct NotmuchEnvelopes { + #[serde(rename = "response")] + pub envelopes: Vec, +} impl Deref for NotmuchEnvelopes { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.0 + &self.envelopes } } impl DerefMut for NotmuchEnvelopes { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.envelopes } } @@ -107,7 +110,7 @@ impl<'a> TryFrom for NotmuchEnvelopes { .context("cannot parse notmuch mail entry")?; envelopes.push(envelope); } - Ok(NotmuchEnvelopes(envelopes)) + Ok(NotmuchEnvelopes { envelopes }) } } diff --git a/src/backends/notmuch/notmuch_mbox.rs b/src/backends/notmuch/notmuch_mbox.rs index 47a6e40..2fe1262 100644 --- a/src/backends/notmuch/notmuch_mbox.rs +++ b/src/backends/notmuch/notmuch_mbox.rs @@ -17,13 +17,16 @@ use crate::{ /// Represents a list of Notmuch mailboxes. #[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchMboxes(pub Vec); +pub struct NotmuchMboxes { + #[serde(rename = "response")] + pub mboxes: Vec, +} impl Deref for NotmuchMboxes { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.0 + &self.mboxes } } diff --git a/src/mbox/mbox_handlers.rs b/src/mbox/mbox_handlers.rs index 44881e3..4a110e9 100644 --- a/src/mbox/mbox_handlers.rs +++ b/src/mbox/mbox_handlers.rs @@ -115,21 +115,23 @@ mod tests { unimplemented!(); } fn get_mboxes(&mut self) -> Result> { - Ok(Box::new(ImapMboxes(vec![ - ImapMbox { - delim: "/".into(), - name: "INBOX".into(), - attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]), - }, - ImapMbox { - delim: "/".into(), - name: "Sent".into(), - attrs: ImapMboxAttrs(vec![ - ImapMboxAttr::NoInferiors, - ImapMboxAttr::Custom("HasNoChildren".into()), - ]), - }, - ]))) + Ok(Box::new(ImapMboxes { + mboxes: vec![ + ImapMbox { + delim: "/".into(), + name: "INBOX".into(), + attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]), + }, + ImapMbox { + delim: "/".into(), + name: "Sent".into(), + attrs: ImapMboxAttrs(vec![ + ImapMboxAttr::NoInferiors, + ImapMboxAttr::Custom("HasNoChildren".into()), + ]), + }, + ], + })) } fn del_mbox(&mut self, _: &str) -> Result<()> { unimplemented!(); diff --git a/tests/test_maildir_backend.rs b/tests/test_maildir_backend.rs index d998789..11eaa52 100644 --- a/tests/test_maildir_backend.rs +++ b/tests/test_maildir_backend.rs @@ -19,10 +19,7 @@ fn test_maildir_backend() { // configure accounts let account_config = AccountConfig { - mailboxes: HashMap::from_iter([ - ("inbox".into(), "INBOX".into()), - ("subdir".into(), "Subdir".into()), - ]), + mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]), ..AccountConfig::default() }; let mdir_config = MaildirBackendConfig { From eb6f51456bedc34d4ff286a3ef26fe3ea60dbf1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 00:49:51 +0100 Subject: [PATCH 12/18] improve draft file ext --- src/msg/msg_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msg/msg_utils.rs b/src/msg/msg_utils.rs index 27d4b37..b5ccacf 100644 --- a/src/msg/msg_utils.rs +++ b/src/msg/msg_utils.rs @@ -3,7 +3,7 @@ use log::{debug, trace}; use std::{env, fs, path::PathBuf}; pub fn local_draft_path() -> PathBuf { - let path = env::temp_dir().join("himalaya-draft.mail"); + let path = env::temp_dir().join("himalaya-draft.eml"); trace!("local draft path: {:?}", path); path } From 86b41e49142e2ac4ee7c9dcacf4a9b5c407a8eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 13:05:57 +0100 Subject: [PATCH 13/18] introduce --header arg for read command (#338) --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/main.rs | 4 +- src/msg/msg_args.rs | 28 +++++++++--- src/msg/msg_entity.rs | 94 +++++++++++++++++++++++++++++++++++++++-- src/msg/msg_handlers.rs | 10 ++--- src/msg/tpl_args.rs | 4 +- 7 files changed, 129 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 027fa1f..9dd6278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,6 +167,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "core-foundation" version = "0.9.2" @@ -443,6 +449,7 @@ dependencies = [ "atty", "chrono", "clap", + "convert_case", "env_logger", "erased-serde", "html-escape", diff --git a/Cargo.toml b/Cargo.toml index 642dab2..4cde60e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ anyhow = "1.0.44" atty = "0.2.14" chrono = "0.4.19" clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } +convert_case = "0.5.0" env_logger = "0.8.3" erased-serde = "0.3.18" html-escape = "0.2.9" diff --git a/src/main.rs b/src/main.rs index 334005a..9c0b8af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -221,8 +221,8 @@ fn main() -> Result<()> { Some(msg_args::Cmd::Move(seq, mbox_dst)) => { return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend); } - Some(msg_args::Cmd::Read(seq, text_mime, raw)) => { - return msg_handlers::read(seq, text_mime, raw, mbox, &mut printer, backend); + Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => { + return msg_handlers::read(seq, text_mime, raw, headers, mbox, &mut printer, backend); } Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { return msg_handlers::reply( diff --git a/src/msg/msg_args.rs b/src/msg/msg_args.rs index 12ccbde..32e02b6 100644 --- a/src/msg/msg_args.rs +++ b/src/msg/msg_args.rs @@ -25,6 +25,7 @@ type AttachmentPaths<'a> = Vec<&'a str>; type MaxTableWidth = Option; type Encrypt = bool; type Criteria = String; +type Headers<'a> = Vec<&'a str>; /// Message commands. #[derive(Debug, PartialEq, Eq)] @@ -35,7 +36,7 @@ pub enum Cmd<'a> { Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt), List(MaxTableWidth, Option, Page), Move(Seq<'a>, Mbox<'a>), - Read(Seq<'a>, TextMime<'a>, Raw), + Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>), Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt), Save(RawMsg<'a>), Search(Query, MaxTableWidth, Option, Page), @@ -121,7 +122,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("text mime: {}", mime); let raw = m.is_present("raw"); debug!("raw: {}", raw); - return Ok(Some(Cmd::Read(seq, mime, raw))); + let headers: Vec<&str> = m.values_of("headers").unwrap_or_default().collect(); + debug!("headers: {:?}", headers); + return Ok(Some(Cmd::Read(seq, mime, raw, headers))); } if let Some(m) = m.subcommand_matches("reply") { @@ -318,7 +321,7 @@ fn page_arg<'a>() -> Arg<'a, 'a> { } /// Message attachment argument. -pub fn attachment_arg<'a>() -> Arg<'a, 'a> { +pub fn attachments_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("attachments") .help("Adds attachment to the message") .short("a") @@ -327,6 +330,16 @@ pub fn attachment_arg<'a>() -> Arg<'a, 'a> { .multiple(true) } +/// Represents the message headers argument. +pub fn headers_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("headers") + .help("Shows additional headers with the message") + .short("h") + .long("header") + .value_name("STR") + .multiple(true) +} + /// Message encrypt argument. pub fn encrypt_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("encrypt") @@ -399,7 +412,7 @@ pub fn subcmds<'a>() -> Vec> { ), SubCommand::with_name("write") .about("Writes a new message") - .arg(attachment_arg()) + .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("send") .about("Sends a raw message") @@ -424,19 +437,20 @@ pub fn subcmds<'a>() -> Vec> { .help("Reads raw message") .long("raw") .short("r"), - ), + ) + .arg(headers_arg()), SubCommand::with_name("reply") .aliases(&["rep", "r"]) .about("Answers to a message") .arg(seq_arg()) .arg(reply_all_arg()) - .arg(attachment_arg()) + .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("forward") .aliases(&["fwd", "f"]) .about("Forwards a message") .arg(seq_arg()) - .arg(attachment_arg()) + .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("copy") .aliases(&["cp", "c"]) diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index edbc867..7d34adb 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -1,11 +1,19 @@ use ammonia; use anyhow::{anyhow, Context, Error, Result}; use chrono::{DateTime, FixedOffset}; +use convert_case::{Case, Casing}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use log::{debug, info, trace, warn}; use regex::Regex; -use std::{collections::HashSet, convert::TryInto, env::temp_dir, fmt::Debug, fs, path::PathBuf}; +use std::{ + collections::{HashMap, HashSet}, + convert::TryInto, + env::temp_dir, + fmt::Debug, + fs, + path::PathBuf, +}; use uuid::Uuid; use crate::{ @@ -41,6 +49,7 @@ pub struct Msg { pub bcc: Option, pub in_reply_to: Option, pub message_id: Option, + pub headers: HashMap, /// The internal date of the message. /// @@ -665,9 +674,11 @@ impl Msg { "message-id" => msg.message_id = Some(val), "in-reply-to" => msg.in_reply_to = Some(val), "subject" => { - msg.subject = val; + msg.subject = rfc2047_decoder::decode(val.as_bytes())?; } "date" => { + // TODO: use date format instead + // https://github.com/jonhoo/rust-imap/blob/afbc5118f251da4e3f6a1e560e749c0700020b54/src/types/fetch.rs#L16 msg.date = DateTime::parse_from_rfc2822( val.split_at(val.find(" (").unwrap_or_else(|| val.len())).0, ) @@ -697,7 +708,12 @@ impl Msg { msg.bcc = from_slice_to_addrs(val) .context(format!("cannot parse header {:?}", key))? } - _ => (), + key => { + msg.headers.insert( + key.to_owned(), + rfc2047_decoder::decode(val.as_bytes()).unwrap_or(val), + ); + } } } @@ -708,6 +724,78 @@ impl Msg { info!("end: building message from parsed mail"); Ok(msg) } + + /// Transforms a message into a readable string. A readable + /// message is like a template, except that: + /// - headers part is customizable (can be omitted if empty filter given in argument) + /// - body type is customizable (plain or html) + pub fn to_readable_string(&self, text_mime: &str, headers: Vec<&str>) -> Result { + let mut readable_msg = String::new(); + + for h in headers { + match h.to_lowercase().as_str() { + "message-id" => match self.message_id { + Some(ref message_id) if !message_id.is_empty() => { + readable_msg.push_str(&format!("Message-Id: {}\n", message_id)); + } + _ => (), + }, + "in-reply-to" => match self.in_reply_to { + Some(ref in_reply_to) if !in_reply_to.is_empty() => { + readable_msg.push_str(&format!("In-Reply-To: {}\n", in_reply_to)); + } + _ => (), + }, + "subject" => { + readable_msg.push_str(&format!("Subject: {}\n", self.subject)); + } + "date" => { + if let Some(ref date) = self.date { + readable_msg.push_str(&format!("Date: {}\n", date)); + } + } + "from" => match self.from { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("From: {}\n", addrs)); + } + _ => (), + }, + "to" => match self.to { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("To: {}\n", addrs)); + } + _ => (), + }, + "reply-to" => match self.reply_to { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("Reply-To: {}\n", addrs)); + } + _ => (), + }, + "cc" => match self.cc { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("Cc: {}\n", addrs)); + } + _ => (), + }, + "bcc" => match self.bcc { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("Bcc: {}\n", addrs)); + } + _ => (), + }, + key => match self.headers.get(key) { + Some(ref val) if !val.is_empty() => { + readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Pascal), val)); + } + _ => (), + }, + }; + } + readable_msg.push_str("\n"); + readable_msg.push_str(&self.fold_text_parts(text_mime)); + Ok(readable_msg) + } } impl TryInto for Msg { diff --git a/src/msg/msg_handlers.rs b/src/msg/msg_handlers.rs index d58e737..4728565 100644 --- a/src/msg/msg_handlers.rs +++ b/src/msg/msg_handlers.rs @@ -207,19 +207,19 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, text_mime: &str, raw: bool, + headers: Vec<&str>, mbox: &str, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { let msg = backend.get_msg(mbox, seq)?; - let msg = if raw { + + printer.print_struct(if raw { // Emails don't always have valid utf8. Using "lossy" to display what we can. String::from_utf8_lossy(&msg.raw).into_owned() } else { - msg.fold_text_parts(text_mime) - }; - - printer.print_struct(msg) + msg.to_readable_string(text_mime, headers)? + }) } /// Reply to the given message UID. diff --git a/src/msg/tpl_args.rs b/src/msg/tpl_args.rs index 5379acc..e3254ff 100644 --- a/src/msg/tpl_args.rs +++ b/src/msg/tpl_args.rs @@ -183,13 +183,13 @@ pub fn subcmds<'a>() -> Vec> { .subcommand( SubCommand::with_name("save") .about("Saves a message based on the given template") - .arg(&msg_args::attachment_arg()) + .arg(&msg_args::attachments_arg()) .arg(Arg::with_name("template").raw(true)), ) .subcommand( SubCommand::with_name("send") .about("Sends a message based on the given template") - .arg(&msg_args::attachment_arg()) + .arg(&msg_args::attachments_arg()) .arg(Arg::with_name("template").raw(true)), )] } From d3968461e2fda9597200a4d00b36447fb9de2d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 13:57:27 +0100 Subject: [PATCH 14/18] add tests for --header arg of read command (#338) --- src/msg/msg_entity.rs | 79 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 7d34adb..4a2855a 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -710,7 +710,7 @@ impl Msg { } key => { msg.headers.insert( - key.to_owned(), + key.to_lowercase(), rfc2047_decoder::decode(val.as_bytes()).unwrap_or(val), ); } @@ -786,13 +786,17 @@ impl Msg { }, key => match self.headers.get(key) { Some(ref val) if !val.is_empty() => { - readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Pascal), val)); + readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Train), val)); } _ => (), }, }; } - readable_msg.push_str("\n"); + + if !readable_msg.is_empty() { + readable_msg.push_str("\n"); + } + readable_msg.push_str(&self.fold_text_parts(text_mime)); Ok(readable_msg) } @@ -829,6 +833,8 @@ impl TryInto for &Msg { #[cfg(test)] mod tests { + use std::iter::FromIterator; + use mailparse::SingleInfo; use crate::msg::Addr; @@ -973,4 +979,71 @@ mod tests { "\"Sender 1\" , \"Sender 2\" " ); } + + #[test] + fn test_to_readable() { + let msg = Msg { + parts: Parts(vec![Part::TextPlain(TextPlainPart { + content: String::from("hello, world!"), + })]), + ..Msg::default() + }; + + // empty msg, empty headers + assert_eq!( + "hello, world!", + msg.to_readable_string("plain", vec![]).unwrap() + ); + // empty msg, basic headers + assert_eq!( + "hello, world!", + msg.to_readable_string("plain", vec!["from", "date", "custom-header"]) + .unwrap() + ); + // empty msg, subject header + assert_eq!( + "Subject: \n\nhello, world!", + msg.to_readable_string("plain", vec!["subject"]).unwrap() + ); + + let msg = Msg { + headers: HashMap::from_iter([("custom-header".into(), "custom value".into())]), + message_id: Some("".into()), + from: Some( + vec![Addr::Single(SingleInfo { + addr: "test@local".into(), + display_name: Some("Test".into()), + })] + .into(), + ), + cc: Some(vec![].into()), + parts: Parts(vec![Part::TextPlain(TextPlainPart { + content: String::from("hello, world!"), + })]), + ..Msg::default() + }; + + // header present in msg headers + assert_eq!( + "From: \"Test\" \n\nhello, world!", + msg.to_readable_string("plain", vec!["from"]).unwrap() + ); + // header present but empty in msg headers + assert_eq!( + "hello, world!", + msg.to_readable_string("plain", vec!["cc"]).unwrap() + ); + // custom header present in msg headers + assert_eq!( + "Custom-Header: custom value\n\nhello, world!", + msg.to_readable_string("plain", vec!["custom-header"]) + .unwrap() + ); + // custom header present in msg headers (case insensitivity) + assert_eq!( + "Custom-Header: custom value\n\nhello, world!", + msg.to_readable_string("plain", vec!["CUSTOM-hEaDer"]) + .unwrap() + ); + } } From f9bed5f3c221ae8f6ed59b5eebc213727aec3fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 15:25:35 +0100 Subject: [PATCH 15/18] introduce read_headers in account config (#338) --- CHANGELOG.md | 2 + src/config/account_config.rs | 4 ++ src/config/deserialized_account_config.rs | 7 +- src/main.rs | 11 ++- src/msg/msg_entity.rs | 84 ++++++++++++++++------- src/msg/msg_handlers.rs | 3 +- 6 files changed, 82 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a52aa..f76f862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - SMTP pre-send hook [#178] +- Customize headers to show at the top of a read message [#338] ### Changed @@ -503,3 +504,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#329]: https://github.com/soywod/himalaya/issues/329 [#331]: https://github.com/soywod/himalaya/issues/331 [#335]: https://github.com/soywod/himalaya/issues/335 +[#338]: https://github.com/soywod/himalaya/issues/338 diff --git a/src/config/account_config.rs b/src/config/account_config.rs index 9a73e8f..0d32a19 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -32,6 +32,9 @@ pub struct AccountConfig { /// Represents the text/plain format as defined in the /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) pub format: Format, + /// Overrides the default headers displayed at the top of + /// the read message. + pub read_headers: Vec, /// Represents mailbox aliases. pub mailboxes: HashMap, @@ -157,6 +160,7 @@ impl<'a> AccountConfig { .unwrap_or(&vec![]) .to_owned(), format: base_account.format.unwrap_or_default(), + read_headers: base_account.read_headers, mailboxes: base_account.mailboxes.clone(), hooks: base_account.hooks.unwrap_or_default(), default: base_account.default.unwrap_or_default(), diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 84e7dff..626b86d 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -45,7 +45,7 @@ macro_rules! make_account_config { pub signature: Option, /// Overrides the signature delimiter for this account. pub signature_delimiter: Option, - /// Overrides the default page size for this account. + /// Overrides the default page size for this account. pub default_page_size: Option, /// Overrides the notify command for this account. pub notify_cmd: Option, @@ -56,6 +56,10 @@ macro_rules! make_account_config { /// Represents the text/plain format as defined in the /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) pub format: Option, + /// Overrides the default headers displayed at the top of + /// the read message. + #[serde(default)] + pub read_headers: Vec, /// Makes this account the default one. pub default: Option, @@ -102,6 +106,7 @@ macro_rules! make_account_config { notify_query: self.notify_query.clone(), watch_cmds: self.watch_cmds.clone(), format: self.format.clone(), + read_headers: self.read_headers.clone(), default: self.default.clone(), email: self.email.clone(), diff --git a/src/main.rs b/src/main.rs index 9c0b8af..61c647d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -222,7 +222,16 @@ fn main() -> Result<()> { return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend); } Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => { - return msg_handlers::read(seq, text_mime, raw, headers, mbox, &mut printer, backend); + return msg_handlers::read( + seq, + text_mime, + raw, + headers, + mbox, + &account_config, + &mut printer, + backend, + ); } Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { return msg_handlers::reply( diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 4a2855a..3c1c60e 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -729,11 +729,29 @@ impl Msg { /// message is like a template, except that: /// - headers part is customizable (can be omitted if empty filter given in argument) /// - body type is customizable (plain or html) - pub fn to_readable_string(&self, text_mime: &str, headers: Vec<&str>) -> Result { - let mut readable_msg = String::new(); + pub fn to_readable_string( + &self, + text_mime: &str, + headers: Vec<&str>, + config: &AccountConfig, + ) -> Result { + let mut all_headers = vec![]; + for h in config.read_headers.iter() { + let h = h.to_lowercase(); + if !all_headers.contains(&h) { + all_headers.push(h) + } + } + for h in headers.iter() { + let h = h.to_lowercase(); + if !all_headers.contains(&h) { + all_headers.push(h) + } + } - for h in headers { - match h.to_lowercase().as_str() { + let mut readable_msg = String::new(); + for h in all_headers { + match h.as_str() { "message-id" => match self.message_id { Some(ref message_id) if !message_id.is_empty() => { readable_msg.push_str(&format!("Message-Id: {}\n", message_id)); @@ -833,9 +851,8 @@ impl TryInto for &Msg { #[cfg(test)] mod tests { - use std::iter::FromIterator; - use mailparse::SingleInfo; + use std::iter::FromIterator; use crate::msg::Addr; @@ -982,6 +999,7 @@ mod tests { #[test] fn test_to_readable() { + let config = AccountConfig::default(); let msg = Msg { parts: Parts(vec![Part::TextPlain(TextPlainPart { content: String::from("hello, world!"), @@ -989,21 +1007,22 @@ mod tests { ..Msg::default() }; - // empty msg, empty headers + // empty msg headers, empty headers, empty config assert_eq!( "hello, world!", - msg.to_readable_string("plain", vec![]).unwrap() + msg.to_readable_string("plain", vec![], &config).unwrap() ); - // empty msg, basic headers + // empty msg headers, basic headers assert_eq!( "hello, world!", - msg.to_readable_string("plain", vec!["from", "date", "custom-header"]) + msg.to_readable_string("plain", vec!["From", "DATE", "custom-hEader"], &config) .unwrap() ); - // empty msg, subject header + // empty msg headers, multiple subject headers assert_eq!( "Subject: \n\nhello, world!", - msg.to_readable_string("plain", vec!["subject"]).unwrap() + msg.to_readable_string("plain", vec!["subject", "Subject", "SUBJECT"], &config) + .unwrap() ); let msg = Msg { @@ -1023,26 +1042,39 @@ mod tests { ..Msg::default() }; - // header present in msg headers + // header present in msg headers, empty config assert_eq!( "From: \"Test\" \n\nhello, world!", - msg.to_readable_string("plain", vec!["from"]).unwrap() - ); - // header present but empty in msg headers - assert_eq!( - "hello, world!", - msg.to_readable_string("plain", vec!["cc"]).unwrap() - ); - // custom header present in msg headers - assert_eq!( - "Custom-Header: custom value\n\nhello, world!", - msg.to_readable_string("plain", vec!["custom-header"]) + msg.to_readable_string("plain", vec!["from"], &config) .unwrap() ); - // custom header present in msg headers (case insensitivity) + // header present but empty in msg headers, empty config + assert_eq!( + "hello, world!", + msg.to_readable_string("plain", vec!["cc"], &config) + .unwrap() + ); + // multiple same custom headers present in msg headers, empty + // config assert_eq!( "Custom-Header: custom value\n\nhello, world!", - msg.to_readable_string("plain", vec!["CUSTOM-hEaDer"]) + msg.to_readable_string("plain", vec!["custom-header", "cuSTom-HeaDer"], &config) + .unwrap() + ); + + let config = AccountConfig { + read_headers: vec![ + "CusTOM-heaDER".into(), + "Subject".into(), + "from".into(), + "cc".into(), + ], + ..AccountConfig::default() + }; + // header present but empty in msg headers, empty config + assert_eq!( + "Custom-Header: custom value\nSubject: \nFrom: \"Test\" \nMessage-Id: \n\nhello, world!", + msg.to_readable_string("plain", vec!["cc", "message-ID"], &config) .unwrap() ); } diff --git a/src/msg/msg_handlers.rs b/src/msg/msg_handlers.rs index 4728565..80520c5 100644 --- a/src/msg/msg_handlers.rs +++ b/src/msg/msg_handlers.rs @@ -209,6 +209,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( raw: bool, headers: Vec<&str>, mbox: &str, + config: &AccountConfig, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -218,7 +219,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( // Emails don't always have valid utf8. Using "lossy" to display what we can. String::from_utf8_lossy(&msg.raw).into_owned() } else { - msg.to_readable_string(text_mime, headers)? + msg.to_readable_string(text_mime, headers, config)? }) } From 1d969a0d3ada5fde2b3b185bb81c14061fd7618f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 17:01:05 +0100 Subject: [PATCH 16/18] simplify msg header decoding --- src/config/deserialized_account_config.rs | 2 +- src/msg/msg_entity.rs | 59 ++++++++++------------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 626b86d..8ada7f9 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -56,7 +56,7 @@ macro_rules! make_account_config { /// Represents the text/plain format as defined in the /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) pub format: Option, - /// Overrides the default headers displayed at the top of + /// Represents the default headers displayed at the top of /// the read message. #[serde(default)] pub read_headers: Vec, diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 3c1c60e..55cfa75 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, FixedOffset}; use convert_case::{Case, Casing}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; -use log::{debug, info, trace, warn}; +use log::{info, trace, warn}; use regex::Regex; use std::{ collections::{HashMap, HashSet}, @@ -31,6 +31,8 @@ use crate::{ }, }; +const DATE_TIME_FORMAT: &str = "%d-%b-%Y %H:%M:%S %z"; + /// Representation of a message. #[derive(Debug, Clone, Default)] pub struct Msg { @@ -73,8 +75,9 @@ impl Msg { .collect() } - /// Folds string body from all plain text parts into a single string body. If no plain text - /// parts are found, HTML parts are used instead. The result is sanitized (all HTML markup is + /// Folds string body from all plain text parts into a single + /// string body. If no plain text parts are found, HTML parts are + /// used instead. The result is sanitized (all HTML markup is /// removed). pub fn fold_text_plain_parts(&self) -> String { let (plain, html) = self.parts.iter().fold( @@ -142,7 +145,8 @@ impl Msg { } } - /// Fold string body from all HTML parts into a single string body. + /// Fold string body from all HTML parts into a single string + /// body. fn fold_text_html_parts(&self) -> String { let text_parts = self .parts @@ -160,8 +164,9 @@ impl Msg { text_parts } - /// Fold string body from all text parts into a single string body. The mime allows users to - /// choose between plain text parts and html text parts. + /// Fold string body from all text parts into a single string + /// body. The mime allows users to choose between plain text parts + /// and html text parts. pub fn fold_text_parts(&self, text_mime: &str) -> String { if text_mime == "html" { self.fold_text_html_parts() @@ -651,42 +656,32 @@ impl Msg { parsed_mail: mailparse::ParsedMail<'_>, config: &AccountConfig, ) -> Result { - info!("begin: building message from parsed mail"); + trace!(">> build message from parsed mail"); trace!("parsed mail: {:?}", parsed_mail); let mut msg = Msg::default(); - - debug!("parsing headers"); for header in parsed_mail.get_headers() { + trace!(">> parse header {:?}", header); + let key = header.get_key(); - debug!("header key: {:?}", key); + trace!("header key: {:?}", key); let val = header.get_value(); - let val = String::from_utf8(header.get_value_raw().to_vec()) - .map(|val| val.trim().to_string()) - .context(format!( - "cannot decode value {:?} from header {:?}", - key, val - ))?; - debug!("header value: {:?}", val); + trace!("header value: {:?}", val); match key.to_lowercase().as_str() { "message-id" => msg.message_id = Some(val), "in-reply-to" => msg.in_reply_to = Some(val), "subject" => { - msg.subject = rfc2047_decoder::decode(val.as_bytes())?; + msg.subject = val; } "date" => { - // TODO: use date format instead - // https://github.com/jonhoo/rust-imap/blob/afbc5118f251da4e3f6a1e560e749c0700020b54/src/types/fetch.rs#L16 - msg.date = DateTime::parse_from_rfc2822( - val.split_at(val.find(" (").unwrap_or_else(|| val.len())).0, - ) - .map_err(|err| { - warn!("cannot parse message date {:?}, skipping it", val); - err - }) - .ok(); + msg.date = DateTime::parse_from_str(&val, DATE_TIME_FORMAT) + .map_err(|err| { + warn!("cannot parse message date {:?}, skipping it", val); + err + }) + .ok(); } "from" => { msg.from = from_slice_to_addrs(val) @@ -709,19 +704,17 @@ impl Msg { .context(format!("cannot parse header {:?}", key))? } key => { - msg.headers.insert( - key.to_lowercase(), - rfc2047_decoder::decode(val.as_bytes()).unwrap_or(val), - ); + msg.headers.insert(key.to_lowercase(), val); } } + trace!("<< parse header"); } msg.parts = Parts::from_parsed_mail(config, &parsed_mail) .context("cannot parsed message mime parts")?; trace!("message: {:?}", msg); - info!("end: building message from parsed mail"); + info!("<< build message from parsed mail"); Ok(msg) } From e5413fb902359b10ee5319bccbe42e3627515ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 17:45:56 +0100 Subject: [PATCH 17/18] improve msg date readable format --- src/msg/msg_entity.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 55cfa75..ffe87b6 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -1,6 +1,6 @@ use ammonia; use anyhow::{anyhow, Context, Error, Result}; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, Local, TimeZone, Utc}; use convert_case::{Case, Casing}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; @@ -31,8 +31,6 @@ use crate::{ }, }; -const DATE_TIME_FORMAT: &str = "%d-%b-%Y %H:%M:%S %z"; - /// Representation of a message. #[derive(Debug, Clone, Default)] pub struct Msg { @@ -56,7 +54,7 @@ pub struct Msg { /// The internal date of the message. /// /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3 - pub date: Option>, + pub date: Option>, pub parts: Parts, pub encrypt: bool, @@ -234,7 +232,7 @@ impl Msg { let date = self .date .as_ref() - .map(|date| date.format("%d %b %Y, at %H:%M").to_string()) + .map(|date| date.format("%d %b %Y, at %H:%M (%z)").to_string()) .unwrap_or_else(|| "unknown date".into()); let sender = self .reply_to @@ -675,14 +673,15 @@ impl Msg { "subject" => { msg.subject = val; } - "date" => { - msg.date = DateTime::parse_from_str(&val, DATE_TIME_FORMAT) - .map_err(|err| { - warn!("cannot parse message date {:?}, skipping it", val); - err - }) - .ok(); - } + "date" => match mailparse::dateparse(&val) { + Ok(timestamp) => { + msg.date = Some(Utc.timestamp(timestamp, 0).with_timezone(&Local)) + } + Err(err) => { + warn!("cannot parse message date {:?}, skipping it", val); + warn!("{}", err); + } + }, "from" => { msg.from = from_slice_to_addrs(val) .context(format!("cannot parse header {:?}", key))? @@ -762,7 +761,7 @@ impl Msg { } "date" => { if let Some(ref date) = self.date { - readable_msg.push_str(&format!("Date: {}\n", date)); + readable_msg.push_str(&format!("Date: {}\n", date.to_rfc2822())); } } "from" => match self.from { From 4f6f88496251fd872027a04d35256a1650b43a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 12 Mar 2022 18:02:46 +0100 Subject: [PATCH 18/18] prepare release v0.5.9 --- CHANGELOG.md | 5 ++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76f862..29fe069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.9] - 2022-03-12 + ### Added - SMTP pre-send hook [#178] @@ -355,7 +357,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Password from command [#22] - Set up README [#20] -[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.8...HEAD +[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.9...HEAD +[0.5.9]: https://github.com/soywod/himalaya/compare/v0.5.8...v0.5.9 [0.5.8]: https://github.com/soywod/himalaya/compare/v0.5.7...v0.5.8 [0.5.7]: https://github.com/soywod/himalaya/compare/v0.5.6...v0.5.7 [0.5.6]: https://github.com/soywod/himalaya/compare/v0.5.5...v0.5.6 diff --git a/Cargo.lock b/Cargo.lock index 9dd6278..171ec19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,7 +442,7 @@ dependencies = [ [[package]] name = "himalaya" -version = "0.5.8" +version = "0.5.9" dependencies = [ "ammonia", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 4cde60e..c4010d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "himalaya" description = "Command-line interface for email management" -version = "0.5.8" +version = "0.5.9" authors = ["soywod "] edition = "2018" license-file = "LICENSE"