From 0ddcce22e69a9589169d1108c330916c9cf0587e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Fri, 15 Apr 2022 20:50:05 +0100 Subject: [PATCH 01/24] check the global config for notify-cmd (#362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This brings it in line with how notify-query works and how the wiki defines it to also be a global option. Co-authored-by: Clément DOUIN --- cli/src/config/account_config.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/src/config/account_config.rs b/cli/src/config/account_config.rs index 0d32a19..22a0052 100644 --- a/cli/src/config/account_config.rs +++ b/cli/src/config/account_config.rs @@ -146,7 +146,11 @@ impl<'a> AccountConfig { downloads_dir, sig, default_page_size, - notify_cmd: base_account.notify_cmd.clone(), + notify_cmd: base_account + .notify_cmd + .as_ref() + .or_else(|| config.notify_cmd.as_ref()) + .cloned(), notify_query: base_account .notify_query .as_ref() From 6d154abcb5f48a4591b71672006783f037885233 Mon Sep 17 00:00:00 2001 From: ugla Date: Sat, 16 Apr 2022 13:50:49 +0200 Subject: [PATCH 02/24] add `tpl_args` for `write` subcommand (#361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément DOUIN --- cli/src/main.rs | 3 ++- cli/src/msg/msg_args.rs | 6 ++++-- cli/src/msg/msg_entity.rs | 11 ++++++----- cli/src/msg/msg_handlers.rs | 29 +++++++++++++++++++++++++---- cli/src/msg/tpl_args.rs | 2 +- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 66f3900..ab2a944 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -277,8 +277,9 @@ fn main() -> Result<()> { Some(msg_args::Cmd::Send(raw_msg)) => { return msg_handlers::send(raw_msg, &account_config, &mut printer, backend, &mut smtp); } - Some(msg_args::Cmd::Write(atts, encrypt)) => { + Some(msg_args::Cmd::Write(tpl, atts, encrypt)) => { return msg_handlers::write( + tpl, atts, encrypt, &account_config, diff --git a/cli/src/msg/msg_args.rs b/cli/src/msg/msg_args.rs index 32e02b6..169b05a 100644 --- a/cli/src/msg/msg_args.rs +++ b/cli/src/msg/msg_args.rs @@ -42,7 +42,7 @@ pub enum Cmd<'a> { Search(Query, MaxTableWidth, Option, Page), Sort(Criteria, Query, MaxTableWidth, Option, Page), Send(RawMsg<'a>), - Write(AttachmentPaths<'a>, Encrypt), + Write(tpl_args::TplOverride<'a>, AttachmentPaths<'a>, Encrypt), Flag(Option>), Tpl(Option>), @@ -261,7 +261,8 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("attachments paths: {:?}", attachment_paths); let encrypt = m.is_present("encrypt"); debug!("encrypt: {}", encrypt); - return Ok(Some(Cmd::Write(attachment_paths, encrypt))); + let tpl = tpl_args::TplOverride::from(m); + return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt))); } if let Some(m) = m.subcommand_matches("template") { @@ -412,6 +413,7 @@ pub fn subcmds<'a>() -> Vec> { ), SubCommand::with_name("write") .about("Writes a new message") + .args(&tpl_args::tpl_args()) .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("send") diff --git a/cli/src/msg/msg_entity.rs b/cli/src/msg/msg_entity.rs index ffe87b6..9978d46 100644 --- a/cli/src/msg/msg_entity.rs +++ b/cli/src/msg/msg_entity.rs @@ -327,14 +327,15 @@ impl Msg { Ok(self) } - fn _edit_with_editor(&self, account: &AccountConfig) -> Result { - let tpl = self.to_tpl(TplOverride::default(), account)?; + fn _edit_with_editor(&self, tpl: TplOverride, account: &AccountConfig) -> Result { + let tpl = self.to_tpl(tpl, account)?; let tpl = editor::open_with_tpl(tpl)?; Self::from_tpl(&tpl) } pub fn edit_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( mut self, + tpl: TplOverride, account: &AccountConfig, printer: &mut P, backend: Box<&'a mut B>, @@ -353,7 +354,7 @@ impl Msg { break; } PreEditChoice::Discard => { - self.merge_with(self._edit_with_editor(account)?); + self.merge_with(self._edit_with_editor(tpl.clone(), account)?); break; } PreEditChoice::Quit => return Ok(backend), @@ -365,7 +366,7 @@ impl Msg { } } } else { - self.merge_with(self._edit_with_editor(account)?); + self.merge_with(self._edit_with_editor(tpl.clone(), account)?); } loop { @@ -386,7 +387,7 @@ impl Msg { break; } Ok(PostEditChoice::Edit) => { - self.merge_with(self._edit_with_editor(account)?); + self.merge_with(self._edit_with_editor(tpl.clone(), account)?); continue; } Ok(PostEditChoice::LocalDraft) => { diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 80520c5..57ce255 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -21,6 +21,8 @@ use crate::{ smtp::SmtpService, }; +use super::tpl_args; + /// Downloads all message attachments to the user account downloads directory. pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, @@ -99,7 +101,13 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .into_forward(config)? .add_attachments(attachments_paths)? .encrypt(encrypt) - .edit_with_editor(config, printer, backend, smtp)?; + .edit_with_editor( + tpl_args::TplOverride::default(), + config, + printer, + backend, + smtp, + )?; Ok(()) } @@ -183,7 +191,13 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( }; trace!("message: {:?}", msg); - msg.edit_with_editor(config, printer, backend, smtp)?; + msg.edit_with_editor( + tpl_args::TplOverride::default(), + config, + printer, + backend, + smtp, + )?; Ok(()) } @@ -240,7 +254,13 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .into_reply(all, config)? .add_attachments(attachments_paths)? .encrypt(encrypt) - .edit_with_editor(config, printer, backend, smtp)? + .edit_with_editor( + tpl_args::TplOverride::default(), + config, + printer, + backend, + smtp, + )? .add_flags(mbox, seq, "replied") } @@ -364,6 +384,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( /// Compose a new message. pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( + tpl: tpl_args::TplOverride, attachments_paths: Vec<&str>, encrypt: bool, config: &AccountConfig, @@ -374,6 +395,6 @@ pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( Msg::default() .add_attachments(attachments_paths)? .encrypt(encrypt) - .edit_with_editor(config, printer, backend, smtp)?; + .edit_with_editor(tpl, config, printer, backend, smtp)?; Ok(()) } diff --git a/cli/src/msg/tpl_args.rs b/cli/src/msg/tpl_args.rs index e3254ff..90a9356 100644 --- a/cli/src/msg/tpl_args.rs +++ b/cli/src/msg/tpl_args.rs @@ -13,7 +13,7 @@ type ReplyAll = bool; type AttachmentPaths<'a> = Vec<&'a str>; type Tpl<'a> = &'a str; -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct TplOverride<'a> { pub subject: Option<&'a str>, pub from: Option>, From b7157573f2a829d9c30689bdee747113e6233e5c Mon Sep 17 00:00:00 2001 From: fabrixxm Date: Sat, 7 May 2022 22:13:08 +0200 Subject: [PATCH 03/24] default Content-Type to text/plain for not multipart messages (#357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Default Content-Type to text/plain for not multipart messages Parse body of messages without subparts as text/plain if Content-Type header is not set. * narrow check for defaulting to `text/plain` take message body as `text/plain` only if message has only one part and has no `Content-Type` header Co-authored-by: Clément DOUIN --- cli/src/msg/parts_entity.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cli/src/msg/parts_entity.rs b/cli/src/msg/parts_entity.rs index d4d0640..8c30f4f 100644 --- a/cli/src/msg/parts_entity.rs +++ b/cli/src/msg/parts_entity.rs @@ -55,7 +55,12 @@ impl Parts { part: &'a mailparse::ParsedMail<'a>, ) -> Result { let mut parts = vec![]; - build_parts_map_rec(account, part, &mut parts)?; + if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() { + let content = part.get_body().unwrap_or_default(); + parts.push(Part::TextPlain(TextPlainPart { content })) + } else { + build_parts_map_rec(account, part, &mut parts)?; + } Ok(Self(parts)) } } @@ -105,7 +110,7 @@ fn build_parts_map_rec( } else if ctype.starts_with("text/html") { parts.push(Part::TextHtml(TextHtmlPart { content })) } - }; + } } }; } else { From 4d91a5d74e9b2e58fee65348935181602000eff8 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 7 May 2022 20:29:09 +0000 Subject: [PATCH 04/24] table: reset color after cell (#372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément DOUIN --- cli/src/ui/table.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cli/src/ui/table.rs b/cli/src/ui/table.rs index 0c97a13..9d9559a 100644 --- a/cli/src/ui/table.rs +++ b/cli/src/ui/table.rs @@ -134,7 +134,14 @@ impl Print for Cell { .context(format!(r#"cannot apply colors to cell "{}""#, self.value))?; // Writes the colorized cell to stdout - write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value)) + write!(writer, "{}", self.value) + .context(format!(r#"cannot print cell "{}""#, self.value))?; + + // Resets color after cell + writer + .reset() + .context(format!(r#"cannot reset color in cell "{}""#, self.value))?; + write!(writer, "").context(format!(r#"cannot print cell "{}""#, self.value)) } } From 5a2d7fa6b540334848c8dfcb018616dc8845e919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20G=C3=BCnzler?= Date: Sat, 7 May 2022 22:54:49 +0200 Subject: [PATCH 05/24] always reset colors settings on the output stream after writing (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is according to: https://docs.rs/termcolor/1.1.2/termcolor/#example-using-standardstream Not resetting the color settings on the stream will leak the style to the shell otherwise. This can be observed when listing mailboxes prior to this patch. Signed-off-by: Robert Günzler Co-authored-by: Clément DOUIN --- cli/src/output/print.rs | 6 ++++-- cli/src/ui/table.rs | 10 ++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cli/src/output/print.rs b/cli/src/output/print.rs index a843501..8faf0a4 100644 --- a/cli/src/output/print.rs +++ b/cli/src/output/print.rs @@ -8,12 +8,14 @@ pub trait Print { impl Print for &str { fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - writeln!(writer, "{}", self).context("cannot write string to writer") + writeln!(writer, "{}", self).context("cannot write string to writer")?; + Ok(writer.reset()?) } } impl Print for String { fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { - self.as_str().print(writer) + self.as_str().print(writer)?; + Ok(writer.reset()?) } } diff --git a/cli/src/ui/table.rs b/cli/src/ui/table.rs index 9d9559a..2a9f22a 100644 --- a/cli/src/ui/table.rs +++ b/cli/src/ui/table.rs @@ -134,14 +134,8 @@ impl Print for Cell { .context(format!(r#"cannot apply colors to cell "{}""#, self.value))?; // Writes the colorized cell to stdout - write!(writer, "{}", self.value) - .context(format!(r#"cannot print cell "{}""#, self.value))?; - - // Resets color after cell - writer - .reset() - .context(format!(r#"cannot reset color in cell "{}""#, self.value))?; - write!(writer, "").context(format!(r#"cannot print cell "{}""#, self.value)) + write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))?; + Ok(writer.reset()?) } } From ba8ef9adf6effc40674765f995d19e9cfd1a8636 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Mon, 23 May 2022 22:41:29 +0000 Subject: [PATCH 06/24] fix(config/imap): get first line for password (#374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(config/imap): get first line for password Fixes #373 * fix(config/smtp): get first line password Co-authored-by: Clément DOUIN --- cli/src/config/account_config.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/src/config/account_config.rs b/cli/src/config/account_config.rs index 22a0052..29cb3a0 100644 --- a/cli/src/config/account_config.rs +++ b/cli/src/config/account_config.rs @@ -238,11 +238,12 @@ impl<'a> AccountConfig { /// Builds the user account SMTP credentials. pub fn smtp_creds(&self) -> Result { let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?; - let passwd = passwd - .trim_end_matches(|c| c == '\r' || c == '\n') - .to_owned(); + let passwd = passwd.lines().next().context("cannot find password")?; - Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd)) + Ok(SmtpCredentials::new( + self.smtp_login.to_owned(), + passwd.to_owned(), + )) } /// Encrypts a file. @@ -374,10 +375,8 @@ impl ImapBackendConfig { /// Gets the IMAP password of the user account. pub fn imap_passwd(&self) -> Result { let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?; - let passwd = passwd - .trim_end_matches(|c| c == '\r' || c == '\n') - .to_owned(); - Ok(passwd) + let passwd = passwd.lines().next().context("cannot find password")?; + Ok(passwd.to_string()) } } From 0696f36f05dfea51a62db9dfeddea21a26956614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20G=C3=BCnzler?= Date: Sat, 28 May 2022 09:49:40 +0200 Subject: [PATCH 07/24] vim: msg_id is a uuid string (#383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit change the id parser according to the uuid spec: https://datatracker.ietf.org/doc/html/rfc4122#section-3 and I get this error when using the vim plugin: Error: cannot find maildir message by short hash "0" at "/path/to/my/INBOX" Caused by: 0: cannot find maildir message id from short hash "0" 1: the short hash "0" matches more than one hash: 030598120934103c456ce08338886728, 06edb10a55efb89de45d8560aee33c8e Signed-off-by: Robert Günzler --- vim/autoload/himalaya/msg.vim | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/vim/autoload/himalaya/msg.vim b/vim/autoload/himalaya/msg.vim index 9384c9d..e9e03b8 100644 --- a/vim/autoload/himalaya/msg.vim +++ b/vim/autoload/himalaya/msg.vim @@ -3,7 +3,7 @@ let s:trim = function("himalaya#shared#utils#trim") let s:cli = function("himalaya#shared#cli#call") let s:plain_req = function("himalaya#request#plain") -let s:msg_id = 0 +let s:msg_id = "" let s:draft = "" let s:attachment_paths = [] @@ -47,16 +47,17 @@ function! himalaya#msg#read() try let pos = getpos(".") let s:msg_id = s:get_focused_msg_id() + if empty(s:msg_id) || s:msg_id == "HASH" | return | endif let account = himalaya#account#curr() let mbox = himalaya#mbox#curr_mbox() let msg = s:cli( - \"--account %s --mailbox %s read %d", + \"--account %s --mailbox %s read %s", \[shellescape(account), shellescape(mbox), s:msg_id], - \printf("Fetching message %d", s:msg_id), + \printf("Fetching message %s", s:msg_id), \1, \) call s:close_open_buffers('Himalaya read message') - execute printf("silent! botright new Himalaya read message [%d]", s:msg_id) + execute printf("silent! botright new Himalaya read message [%s]", s:msg_id) setlocal modifiable silent execute "%d" call append(0, split(substitute(msg, "\r", "", "g"), "\n")) @@ -98,12 +99,12 @@ function! himalaya#msg#reply() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s template reply %d", + \"--account %s --mailbox %s template reply %s", \[shellescape(account), shellescape(mbox), msg_id], \"Fetching reply template", \0, \) - execute printf("silent! edit Himalaya reply [%d]", msg_id) + execute printf("silent! edit Himalaya reply [%s]", msg_id) call append(0, split(substitute(msg, "\r", "", "g"), "\n")) silent execute "$d" setlocal filetype=himalaya-msg-write @@ -124,12 +125,12 @@ function! himalaya#msg#reply_all() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s template reply %d --all", + \"--account %s --mailbox %s template reply %s --all", \[shellescape(account), shellescape(mbox), msg_id], \"Fetching reply all template", \0 \) - execute printf("silent! edit Himalaya reply all [%d]", msg_id) + execute printf("silent! edit Himalaya reply all [%s]", msg_id) call append(0, split(substitute(msg, "\r", "", "g"), "\n")) silent execute "$d" setlocal filetype=himalaya-msg-write @@ -150,12 +151,12 @@ function! himalaya#msg#forward() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s template forward %d", + \"--account %s --mailbox %s template forward %s", \[shellescape(account), shellescape(mbox), msg_id], \"Fetching forward template", \0 \) - execute printf("silent! edit Himalaya forward [%d]", msg_id) + execute printf("silent! edit Himalaya forward [%s]", msg_id) call append(0, split(substitute(msg, "\r", "", "g"), "\n")) silent execute "$d" setlocal filetype=himalaya-msg-write @@ -180,7 +181,7 @@ function! himalaya#msg#_copy(target_mbox) let account = himalaya#account#curr() let source_mbox = himalaya#mbox#curr_mbox() let msg = s:cli( - \"--account %s --mailbox %s copy %d %s", + \"--account %s --mailbox %s copy %s %s", \[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)], \"Copying message", \1, @@ -201,14 +202,14 @@ endfunction function! himalaya#msg#_move(target_mbox) try let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let choice = input(printf("Are you sure you want to move the message %d? (y/N) ", msg_id)) + let choice = input(printf("Are you sure you want to move the message %s? (y/N) ", msg_id)) redraw | echo if choice != "y" | return | endif let pos = getpos(".") let account = himalaya#account#curr() let source_mbox = himalaya#mbox#curr_mbox() let msg = s:cli( - \"--account %s --mailbox %s move %d %s", + \"--account %s --mailbox %s move %s %s", \[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)], \"Moving message", \1, @@ -294,7 +295,7 @@ function! himalaya#msg#attachments() let mbox = himalaya#mbox#curr_mbox() let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id let msg = s:cli( - \"--account %s --mailbox %s attachments %d", + \"--account %s --mailbox %s attachments %s", \[shellescape(account), shellescape(mbox), msg_id], \"Downloading attachments", \0 @@ -375,7 +376,7 @@ function! s:bufwidth() endfunction function! s:get_msg_id(line) - return matchstr(a:line, '[0-9]*') + return matchstr(a:line, '[0-9a-zA-Z]*') endfunction function! s:get_focused_msg_id() From bed5a3856b71c5d7d6c1633b2d32af70b6a6561c Mon Sep 17 00:00:00 2001 From: TornaxO7 <50843046+TornaxO7@users.noreply.github.com> Date: Sat, 28 May 2022 14:10:38 +0200 Subject: [PATCH 08/24] improve gitignore files (#385) * modified gitignore * fixing gitignore * reomving the himalaya.iml file * applied cargo fmt * fixed typo in .gitignore and removed an entry in it * adding gitignore to cli/ * reducing .gitignore in cli to one line --- .gitignore | 42 ++++++++++++++++++++++++++++++++++--- cli/.gitignore | 1 + cli/src/msg/parts_entity.rs | 3 ++- cli/src/ui/table.rs | 3 ++- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 cli/.gitignore diff --git a/.gitignore b/.gitignore index 2d69402..e4a6d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,46 @@ # Cargo build directory -/target +target/ +debug/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb # Nix build directory -/result -/result-lib +result +result-* # Direnv /.envrc /.direnv + + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea/ + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +## Others +.metadata/ diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/cli/src/msg/parts_entity.rs b/cli/src/msg/parts_entity.rs index 8c30f4f..9b371a0 100644 --- a/cli/src/msg/parts_entity.rs +++ b/cli/src/msg/parts_entity.rs @@ -55,7 +55,8 @@ impl Parts { part: &'a mailparse::ParsedMail<'a>, ) -> Result { let mut parts = vec![]; - if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() { + if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() + { let content = part.get_body().unwrap_or_default(); parts.push(Part::TextPlain(TextPlainPart { content })) } else { diff --git a/cli/src/ui/table.rs b/cli/src/ui/table.rs index 2a9f22a..5342699 100644 --- a/cli/src/ui/table.rs +++ b/cli/src/ui/table.rs @@ -134,7 +134,8 @@ impl Print for Cell { .context(format!(r#"cannot apply colors to cell "{}""#, self.value))?; // Writes the colorized cell to stdout - write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))?; + write!(writer, "{}", self.value) + .context(format!(r#"cannot print cell "{}""#, self.value))?; Ok(writer.reset()?) } } From b6643be03f3fd08c0a9fef30fa477e58d00b072d Mon Sep 17 00:00:00 2001 From: TornaxO7 <50843046+TornaxO7@users.noreply.github.com> Date: Sat, 28 May 2022 14:36:32 +0200 Subject: [PATCH 09/24] add rust toolchain.toml (#386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * modified gitignore * fixing gitignore * reomving the himalaya.iml file * applied cargo fmt * adding rust-toolchain * restoring the .gitignore file * make nix use rust-toolchain.toml * add back rustfmt and rust-analyzer to buildInputs I opened an issue to see if its the correct behaviour from the overlay: https://github.com/oxalica/rust-overlay/issues/88. * adding clippy to rust-toolchain.toml Co-authored-by: Clément DOUIN --- flake.nix | 31 +++++++++++-------------------- rust-toolchain.toml | 3 +++ 2 files changed, 14 insertions(+), 20 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/flake.nix b/flake.nix index d7c0014..061930b 100644 --- a/flake.nix +++ b/flake.nix @@ -16,19 +16,8 @@ (system: let name = "himalaya"; - pkgs = import nixpkgs { - inherit system; - overlays = [ - rust-overlay.overlay - (self: super: { - # Because rust-overlay bundles multiple rust packages - # into one derivation, specify that mega-bundle here, - # so that crate2nix will use them automatically. - rustc = self.rust-bin.stable.latest.default; - cargo = self.rust-bin.stable.latest.default; - }) - ]; - }; + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; in rec { # nix build @@ -68,17 +57,19 @@ # nix develop devShell = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; inputsFrom = builtins.attrValues self.packages.${system}; buildInputs = with pkgs; [ - cargo - cargo-watch - trunk - ripgrep - rust-analyzer - rustfmt + # Nix LSP + formatter rnix-lsp nixpkgs-fmt + + # Rust env + (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) + cargo-watch + rust-analyzer + rustfmt + + # Notmuch notmuch ]; }; diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..055c9f0 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.58.1" +components = ["cargo", "rustc", "rustfmt", "rust-analysis", "clippy"] From cc918e0eee58fd966c0d365f9e41d1822a9b513a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 20 Mar 2022 22:40:03 +0100 Subject: [PATCH 10/24] fix license and readme file path in cargo.toml --- cli/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6cda92b..045f366 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,8 +4,8 @@ description = "Command-line interface for email management" version = "0.5.10" authors = ["soywod "] edition = "2018" -license-file = "LICENSE" -readme = "README.md" +license-file = "../LICENSE" +readme = "../README.md" categories = ["command-line-interface", "command-line-utilities", "email"] keywords = ["cli", "mail", "email", "client", "imap"] homepage = "https://github.com/soywod/himalaya/wiki" From 0e98def513fe9f1db0ccda16dbcf2cb20b5fbc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 29 Mar 2022 20:58:02 +0200 Subject: [PATCH 11/24] msg: add imap flag aliases --- cli/src/backends/imap/imap_flag.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/backends/imap/imap_flag.rs b/cli/src/backends/imap/imap_flag.rs index 87bb8b9..40fdb67 100644 --- a/cli/src/backends/imap/imap_flag.rs +++ b/cli/src/backends/imap/imap_flag.rs @@ -22,9 +22,9 @@ impl From<&str> for ImapFlag { fn from(flag_str: &str) -> Self { match flag_str { "seen" => ImapFlag::Seen, - "answered" => ImapFlag::Answered, + "answered" | "replied" => ImapFlag::Answered, "flagged" => ImapFlag::Flagged, - "deleted" => ImapFlag::Deleted, + "deleted" | "trashed" => ImapFlag::Deleted, "draft" => ImapFlag::Draft, "recent" => ImapFlag::Recent, "maycreate" | "may-create" => ImapFlag::MayCreate, From 3f5feed0ff1abdc6163800f9082d90d2b85ca431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 28 May 2022 16:59:12 +0200 Subject: [PATCH 12/24] extract account and config from cli to lib (#340) --- Cargo.lock | 35 ++++++++ cli/Cargo.toml | 1 + cli/src/backends/imap/imap_backend.rs | 2 +- cli/src/backends/maildir/maildir_backend.rs | 2 +- cli/src/backends/notmuch/notmuch_backend.rs | 2 +- cli/src/config/account.rs | 3 +- cli/src/config/account_handlers.rs | 11 +-- cli/src/lib.rs | 15 ---- cli/src/main.rs | 12 +-- cli/src/mbox/mbox_handlers.rs | 2 +- cli/src/msg/msg_entity.rs | 4 +- cli/src/msg/msg_handlers.rs | 2 +- cli/src/msg/parts_entity.rs | 3 +- cli/src/msg/tpl_handlers.rs | 2 +- cli/src/output/print_table.rs | 3 +- cli/src/smtp/smtp_service.rs | 5 +- cli/src/ui/table.rs | 6 +- lib/Cargo.toml | 20 +++++ .../src/account}/account_config.rs | 88 ++++++++++++------- .../account}/deserialized_account_config.rs | 2 +- .../src/account}/deserialized_config.rs | 47 ++++++---- {cli/src/config => lib/src/account}/format.rs | 0 {cli/src/config => lib/src/account}/hooks.rs | 0 lib/src/account/mod.rs | 16 ++++ lib/src/lib.rs | 10 +-- lib/src/process.rs | 25 ++++++ tests/test_notmuch_backend.rs | 4 +- 27 files changed, 219 insertions(+), 103 deletions(-) rename {cli/src/config => lib/src/account}/account_config.rs (87%) rename {cli/src/config => lib/src/account}/deserialized_account_config.rs (99%) rename {cli/src/config => lib/src/account}/deserialized_config.rs (72%) rename {cli/src/config => lib/src/account}/format.rs (100%) rename {cli/src/config => lib/src/account}/hooks.rs (100%) create mode 100644 lib/src/account/mod.rs create mode 100644 lib/src/process.rs diff --git a/Cargo.lock b/Cargo.lock index ad142eb..cc51987 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ dependencies = [ "convert_case", "env_logger", "erased-serde", + "himalaya-lib", "html-escape", "imap", "imap-proto", @@ -468,6 +469,20 @@ dependencies = [ [[package]] name = "himalaya-lib" version = "0.1.0" +dependencies = [ + "imap", + "imap-proto", + "lettre", + "log", + "maildir", + "mailparse", + "md5", + "notmuch", + "serde", + "shellexpand", + "thiserror", + "toml", +] [[package]] name = "hostname" @@ -1365,6 +1380,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.1.44" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 045f366..50fea80 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -31,6 +31,7 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions" convert_case = "0.5.0" env_logger = "0.8.3" erased-serde = "0.3.18" +himalaya-lib = { path = "../lib" } html-escape = "0.2.9" lettre = { version = "0.10.0-rc.1", features = ["serde"] } log = "0.4.14" diff --git a/cli/src/backends/imap/imap_backend.rs b/cli/src/backends/imap/imap_backend.rs index f6319e5..9f9bc5c 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/cli/src/backends/imap/imap_backend.rs @@ -3,6 +3,7 @@ //! This module contains the definition of the IMAP backend. use anyhow::{anyhow, Context, Result}; +use himalaya_lib::account::{AccountConfig, ImapBackendConfig}; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; use std::{ @@ -16,7 +17,6 @@ use crate::{ backends::{ imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes, }, - config::{AccountConfig, ImapBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, output::run_cmd, diff --git a/cli/src/backends/maildir/maildir_backend.rs b/cli/src/backends/maildir/maildir_backend.rs index a2e85a5..a83423d 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/cli/src/backends/maildir/maildir_backend.rs @@ -4,12 +4,12 @@ //! traits implementation. use anyhow::{anyhow, Context, Result}; +use himalaya_lib::account::{AccountConfig, MaildirBackendConfig}; use log::{debug, info, trace}; use std::{convert::TryInto, env, fs, path::PathBuf}; use crate::{ backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, - config::{AccountConfig, MaildirBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, }; diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/cli/src/backends/notmuch/notmuch_backend.rs index 37e559a..66a2c14 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/cli/src/backends/notmuch/notmuch_backend.rs @@ -1,11 +1,11 @@ use std::{convert::TryInto, fs}; use anyhow::{anyhow, Context, Result}; +use himalaya_lib::account::{AccountConfig, NotmuchBackendConfig}; use log::{debug, info, trace}; use crate::{ backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, - config::{AccountConfig, NotmuchBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, }; diff --git a/cli/src/config/account.rs b/cli/src/config/account.rs index d593b8f..3a11deb 100644 --- a/cli/src/config/account.rs +++ b/cli/src/config/account.rs @@ -12,8 +12,9 @@ use std::{ ops::Deref, }; +use himalaya_lib::account::DeserializedAccountConfig; + use crate::{ - config::DeserializedAccountConfig, output::{PrintTable, PrintTableOpts, WriteColor}, ui::{Cell, Row, Table}, }; diff --git a/cli/src/config/account_handlers.rs b/cli/src/config/account_handlers.rs index 4ee2c57..80059d9 100644 --- a/cli/src/config/account_handlers.rs +++ b/cli/src/config/account_handlers.rs @@ -3,10 +3,11 @@ //! This module gathers all account actions triggered by the CLI. use anyhow::Result; +use himalaya_lib::account::{AccountConfig, DeserializedConfig}; use log::{info, trace}; use crate::{ - config::{AccountConfig, Accounts, DeserializedConfig}, + config::Accounts, output::{PrintTableOpts, PrinterService}, }; @@ -36,13 +37,13 @@ pub fn list<'a, P: PrinterService>( #[cfg(test)] mod tests { + use himalaya_lib::account::{ + AccountConfig, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig, + }; use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator}; use termcolor::ColorSpec; - use crate::{ - config::{DeserializedAccountConfig, DeserializedImapAccountConfig}, - output::{Print, PrintTable, WriteColor}, - }; + use crate::output::{Print, PrintTable, WriteColor}; use super::*; diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 32384d5..2e0a581 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -107,12 +107,6 @@ pub mod smtp { } pub mod config { - pub mod deserialized_config; - pub use deserialized_config::*; - - pub mod deserialized_account_config; - pub use deserialized_account_config::*; - pub mod config_args; pub mod account_args; @@ -120,15 +114,6 @@ pub mod config { pub mod account; pub use account::*; - - pub mod account_config; - pub use account_config::*; - - pub mod format; - pub use format::*; - - pub mod hooks; - pub use hooks::*; } pub mod compl; diff --git a/cli/src/main.rs b/cli/src/main.rs index ab2a944..10c7382 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,14 +1,14 @@ use anyhow::Result; +use himalaya_lib::account::{ + AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER, +}; use std::{convert::TryFrom, env}; use url::Url; use himalaya::{ backends::Backend, compl::{compl_args, compl_handlers}, - config::{ - account_args, account_handlers, config_args, AccountConfig, BackendConfig, - DeserializedConfig, DEFAULT_INBOX_FOLDER, - }, + config::{account_args, account_handlers, config_args}, mbox::{mbox_args, mbox_handlers}, msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers}, output::{output_args, OutputFmt, StdoutPrinter}, @@ -22,7 +22,9 @@ use himalaya::backends::{imap_args, imap_handlers, ImapBackend}; use himalaya::backends::MaildirBackend; #[cfg(feature = "notmuch-backend")] -use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig}; +use himalaya::backends::NotmuchBackend; +#[cfg(feature = "notmuch-backend")] +use himalaya_lib::account::MaildirBackendConfig; fn create_app<'a>() -> clap::App<'a, 'a> { let app = clap::App::new(env!("CARGO_PKG_NAME")) diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index 4a110e9..332db46 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -3,11 +3,11 @@ //! This module gathers all mailbox actions triggered by the CLI. use anyhow::Result; +use himalaya_lib::account::AccountConfig; use log::{info, trace}; use crate::{ backends::Backend, - config::AccountConfig, output::{PrintTableOpts, PrinterService}, }; diff --git a/cli/src/msg/msg_entity.rs b/cli/src/msg/msg_entity.rs index 9978d46..0ee49ce 100644 --- a/cli/src/msg/msg_entity.rs +++ b/cli/src/msg/msg_entity.rs @@ -2,6 +2,9 @@ use ammonia; use anyhow::{anyhow, Context, Error, Result}; use chrono::{DateTime, Local, TimeZone, Utc}; use convert_case::{Case, Casing}; +use himalaya_lib::account::{ + AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM, +}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use log::{info, trace, warn}; @@ -18,7 +21,6 @@ use uuid::Uuid; use crate::{ backends::Backend, - 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, Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 57ce255..0c2e0c0 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result}; use atty::Stream; +use himalaya_lib::account::{AccountConfig, DEFAULT_SENT_FOLDER}; use log::{debug, info, trace}; use mailparse::addrparse; use std::{ @@ -15,7 +16,6 @@ use url::Url; use crate::{ backends::Backend, - config::{AccountConfig, DEFAULT_SENT_FOLDER}, msg::{Msg, Part, Parts, TextPlainPart}, output::{PrintTableOpts, PrinterService}, smtp::SmtpService, diff --git a/cli/src/msg/parts_entity.rs b/cli/src/msg/parts_entity.rs index 9b371a0..36eac50 100644 --- a/cli/src/msg/parts_entity.rs +++ b/cli/src/msg/parts_entity.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Context, Result}; +use himalaya_lib::account::AccountConfig; use mailparse::MailHeaderMap; use serde::Serialize; use std::{ @@ -7,8 +8,6 @@ use std::{ }; use uuid::Uuid; -use crate::config::AccountConfig; - #[derive(Debug, Clone, Default, Serialize)] pub struct TextPlainPart { pub content: String, diff --git a/cli/src/msg/tpl_handlers.rs b/cli/src/msg/tpl_handlers.rs index b2db225..16953b1 100644 --- a/cli/src/msg/tpl_handlers.rs +++ b/cli/src/msg/tpl_handlers.rs @@ -4,11 +4,11 @@ use anyhow::Result; use atty::Stream; +use himalaya_lib::account::AccountConfig; use std::io::{self, BufRead}; use crate::{ backends::Backend, - config::AccountConfig, msg::{Msg, TplOverride}, output::PrinterService, smtp::SmtpService, diff --git a/cli/src/output/print_table.rs b/cli/src/output/print_table.rs index 45557b9..007c2da 100644 --- a/cli/src/output/print_table.rs +++ b/cli/src/output/print_table.rs @@ -1,9 +1,8 @@ use anyhow::Result; +use himalaya_lib::account::Format; use std::io; use termcolor::{self, StandardStream}; -use crate::config::Format; - pub trait WriteColor: io::Write + termcolor::WriteColor {} impl WriteColor for StandardStream {} diff --git a/cli/src/smtp/smtp_service.rs b/cli/src/smtp/smtp_service.rs index 13ea6ce..e417afb 100644 --- a/cli/src/smtp/smtp_service.rs +++ b/cli/src/smtp/smtp_service.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use himalaya_lib::account::AccountConfig; use lettre::{ self, transport::smtp::{ @@ -9,7 +10,7 @@ use lettre::{ }; use std::convert::TryInto; -use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd}; +use crate::{msg::Msg, output::pipe_cmd}; pub trait SmtpService { fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; @@ -62,7 +63,7 @@ impl SmtpService for LettreService<'_> { 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))? + .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() diff --git a/cli/src/ui/table.rs b/cli/src/ui/table.rs index 5342699..2e84209 100644 --- a/cli/src/ui/table.rs +++ b/cli/src/ui/table.rs @@ -5,15 +5,13 @@ //! [builder design pattern]: https://refactoring.guru/design-patterns/builder use anyhow::{Context, Result}; +use himalaya_lib::account::Format; use log::trace; use termcolor::{Color, ColorSpec}; use terminal_size; use unicode_width::UnicodeWidthStr; -use crate::{ - config::Format, - output::{Print, PrintTableOpts, WriteColor}, -}; +use crate::output::{Print, PrintTableOpts, WriteColor}; /// Defines the default terminal size. /// This is used when the size cannot be determined by the `terminal_size` crate. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8316d53..4bfae01 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -3,4 +3,24 @@ name = "himalaya-lib" version = "0.1.0" edition = "2021" +[features] +imap-backend = ["imap", "imap-proto"] +maildir-backend = ["maildir", "md5"] +notmuch-backend = ["notmuch", "maildir-backend"] +default = ["imap-backend", "maildir-backend"] + [dependencies] +lettre = { version = "0.10.0-rc.1", features = ["serde"] } +log = "0.4.14" +mailparse = "0.13.6" +serde = { version = "1.0.118", features = ["derive"] } +shellexpand = "2.1.0" +thiserror = "1.0.31" +toml = "0.5.8" + +# [optional] +imap = { version = "=3.0.0-alpha.4", optional = true } +imap-proto = { version = "0.14.3", optional = true } +maildir = { version = "0.6.1", optional = true } +md5 = { version = "0.7.0", optional = true } +notmuch = { version = "0.7.1", optional = true } diff --git a/cli/src/config/account_config.rs b/lib/src/account/account_config.rs similarity index 87% rename from cli/src/config/account_config.rs rename to lib/src/account/account_config.rs index 29cb3a0..5e7b951 100644 --- a/cli/src/config/account_config.rs +++ b/lib/src/account/account_config.rs @@ -1,10 +1,35 @@ -use anyhow::{anyhow, Context, Result}; use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; use log::{debug, info, trace}; use mailparse::MailAddr; -use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; +use shellexpand; +use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf, result}; +use thiserror::Error; -use crate::{config::*, output::run_cmd}; +use crate::process::{run_cmd, ProcessError}; + +use super::*; + +#[derive(Error, Debug)] +pub enum AccountError { + #[error("cannot find default account")] + FindDefaultAccountError, + #[error("cannot find account \"{0}\"")] + FindAccountError(String), + #[error("cannot shell expand")] + ShellExpandError(#[from] shellexpand::LookupError), + #[error("cannot parse account address")] + ParseAccountAddressError(#[from] mailparse::MailParseError), + #[error("cannot find account address from \"{0}\"")] + FindAccountAddressError(String), + #[error("cannot parse download file name from \"{0}\"")] + ParseDownloadFileNameError(PathBuf), + #[error("cannot find password")] + FindPasswordError, + #[error(transparent)] + RunCmdError(#[from] ProcessError), +} + +type Result = result::Result; /// Represents the user account. #[derive(Debug, Default, Clone)] @@ -62,7 +87,8 @@ pub struct AccountConfig { } impl<'a> AccountConfig { - /// tries to create an account from a config and an optional account name. + /// Tries to create an account from a config and an optional + /// account name. pub fn from_config_and_opt_account_name( config: &'a DeserializedConfig, account_name: Option<&str>, @@ -87,12 +113,12 @@ impl<'a> AccountConfig { } }) .map(|(name, account)| (name.to_owned(), account)) - .ok_or_else(|| anyhow!("cannot find default account")), + .ok_or_else(|| AccountError::FindDefaultAccountError), Some(name) => config .accounts .get(name) .map(|account| (name.to_owned(), account)) - .ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)), + .ok_or_else(|| AccountError::FindAccountError(name.to_owned())), }?; let base_account = account.to_base(); @@ -225,20 +251,19 @@ impl<'a> AccountConfig { format!("{} <{}>", self.display_name, self.email) }; - Ok(mailparse::addrparse(&addr) - .context(format!( - "cannot parse account address {:?}", - self.display_name - ))? + Ok(mailparse::addrparse(&addr)? .first() - .ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))? + .ok_or_else(|| AccountError::FindAccountAddressError(addr.into()))? .clone()) } /// Builds the user account SMTP credentials. pub fn smtp_creds(&self) -> Result { - let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?; - let passwd = passwd.lines().next().context("cannot find password")?; + let passwd = run_cmd(&self.smtp_passwd_cmd)?; + let passwd = passwd + .lines() + .next() + .ok_or_else(|| AccountError::FindPasswordError)?; Ok(SmtpCredentials::new( self.smtp_login.to_owned(), @@ -250,10 +275,7 @@ impl<'a> AccountConfig { pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result> { if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() { let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path); - run_cmd(&encrypt_file_cmd).map(Some).context(format!( - "cannot run pgp encrypt command {:?}", - encrypt_file_cmd - )) + Ok(run_cmd(&encrypt_file_cmd).map(Some)?) } else { Ok(None) } @@ -263,10 +285,7 @@ impl<'a> AccountConfig { pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result> { if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() { let decrypt_file_cmd = format!("{} {:?}", cmd, path); - run_cmd(&decrypt_file_cmd).map(Some).context(format!( - "cannot run pgp decrypt command {:?}", - decrypt_file_cmd - )) + Ok(run_cmd(&decrypt_file_cmd).map(Some)?) } else { Ok(None) } @@ -276,13 +295,10 @@ impl<'a> AccountConfig { pub fn get_download_file_path>(&self, file_name: S) -> Result { let file_path = self.downloads_dir.join(file_name.as_ref()); self.get_unique_download_file_path(&file_path, |path, _count| path.is_file()) - .context(format!( - "cannot get download file path of {:?}", - file_name.as_ref() - )) } - /// Gets the unique download path from a file name by adding suffixes in case of name conflicts. + /// Gets the unique download path from a file name by adding + /// suffixes in case of name conflicts. pub fn get_unique_download_file_path( &self, original_file_path: &PathBuf, @@ -303,7 +319,9 @@ impl<'a> AccountConfig { .file_stem() .and_then(OsStr::to_str) .map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) - .ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?, + .ok_or_else(|| { + AccountError::ParseDownloadFileNameError(file_path.to_owned()) + })?, )); } @@ -323,7 +341,7 @@ impl<'a> AccountConfig { .unwrap_or(default_cmd); debug!("run command: {}", cmd); - run_cmd(&cmd).context("cannot run notify cmd")?; + run_cmd(&cmd)?; Ok(()) } @@ -335,9 +353,8 @@ impl<'a> AccountConfig { .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)) + let mbox = shellexpand::full(mbox).map(String::from)?; + Ok(mbox) } } @@ -374,8 +391,11 @@ pub struct ImapBackendConfig { impl ImapBackendConfig { /// Gets the IMAP password of the user account. pub fn imap_passwd(&self) -> Result { - let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?; - let passwd = passwd.lines().next().context("cannot find password")?; + let passwd = run_cmd(&self.imap_passwd_cmd)?; + let passwd = passwd + .lines() + .next() + .ok_or_else(|| AccountError::FindPasswordError)?; Ok(passwd.to_string()) } } diff --git a/cli/src/config/deserialized_account_config.rs b/lib/src/account/deserialized_account_config.rs similarity index 99% rename from cli/src/config/deserialized_account_config.rs rename to lib/src/account/deserialized_account_config.rs index 8ada7f9..7539a41 100644 --- a/cli/src/config/deserialized_account_config.rs +++ b/lib/src/account/deserialized_account_config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; -use crate::config::{Format, Hooks}; +use crate::account::{Format, Hooks}; pub trait ToDeserializedBaseAccountConfig { fn to_base(&self) -> DeserializedBaseAccountConfig; diff --git a/cli/src/config/deserialized_config.rs b/lib/src/account/deserialized_config.rs similarity index 72% rename from cli/src/config/deserialized_config.rs rename to lib/src/account/deserialized_config.rs index e26b5a0..500d00a 100644 --- a/cli/src/config/deserialized_config.rs +++ b/lib/src/account/deserialized_config.rs @@ -1,10 +1,10 @@ -use anyhow::{Context, Result}; -use log::{debug, info, trace}; +use log::{debug, trace}; use serde::Deserialize; -use std::{collections::HashMap, env, fs, path::PathBuf}; +use std::{collections::HashMap, env, fs, io, path::PathBuf, result}; +use thiserror::Error; use toml; -use crate::config::DeserializedAccountConfig; +use crate::account::DeserializedAccountConfig; pub const DEFAULT_PAGE_SIZE: usize = 10; pub const DEFAULT_SIG_DELIM: &str = "-- \n"; @@ -13,6 +13,18 @@ pub const DEFAULT_INBOX_FOLDER: &str = "INBOX"; pub const DEFAULT_SENT_FOLDER: &str = "Sent"; pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; +#[derive(Error, Debug)] +pub enum DeserializeConfigError { + #[error("cannot read config file")] + ReadConfigFile(#[from] io::Error), + #[error("cannot parse config file")] + ParseConfigFile(#[from] toml::de::Error), + #[error("cannot read environment variable")] + ReadEnvVar(#[from] env::VarError), +} + +type Result = result::Result; + /// Represents the user config file. #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -42,32 +54,35 @@ pub struct DeserializedConfig { impl DeserializedConfig { /// Tries to create a config from an optional path. pub fn from_opt_path(path: Option<&str>) -> Result { - info!("begin: try to parse config from path"); + trace!(">> parse config from path"); debug!("path: {:?}", path); + let path = path.map(|s| s.into()).unwrap_or(Self::path()?); - let content = fs::read_to_string(path).context("cannot read config file")?; - let config = toml::from_str(&content).context("cannot parse config file")?; - info!("end: try to parse config from path"); + let content = fs::read_to_string(path)?; + let config = toml::from_str(&content)?; + trace!("config: {:?}", config); + trace!("<< parse config from path"); Ok(config) } - /// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable. + /// Tries to get the XDG config file path from XDG_CONFIG_HOME + /// environment variable. fn path_from_xdg() -> Result { - let path = - env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?; + let path = env::var("XDG_CONFIG_HOME")?; let path = PathBuf::from(path).join("himalaya").join("config.toml"); Ok(path) } - /// Tries to get the XDG config file path from HOME environment variable. + /// Tries to get the XDG config file path from HOME environment + /// variable. fn path_from_xdg_alt() -> Result { let home_var = if cfg!(target_family = "windows") { "USERPROFILE" } else { "HOME" }; - let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?; + let path = env::var(home_var)?; let path = PathBuf::from(path) .join(".config") .join("himalaya") @@ -75,14 +90,15 @@ impl DeserializedConfig { Ok(path) } - /// Tries to get the .himalayarc config file path from HOME environment variable. + /// Tries to get the .himalayarc config file path from HOME + /// environment variable. fn path_from_home() -> Result { let home_var = if cfg!(target_family = "windows") { "USERPROFILE" } else { "HOME" }; - let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?; + let path = env::var(home_var)?; let path = PathBuf::from(path).join(".himalayarc"); Ok(path) } @@ -92,6 +108,5 @@ impl DeserializedConfig { Self::path_from_xdg() .or_else(|_| Self::path_from_xdg_alt()) .or_else(|_| Self::path_from_home()) - .context("cannot find config path") } } diff --git a/cli/src/config/format.rs b/lib/src/account/format.rs similarity index 100% rename from cli/src/config/format.rs rename to lib/src/account/format.rs diff --git a/cli/src/config/hooks.rs b/lib/src/account/hooks.rs similarity index 100% rename from cli/src/config/hooks.rs rename to lib/src/account/hooks.rs diff --git a/lib/src/account/mod.rs b/lib/src/account/mod.rs new file mode 100644 index 0000000..5409db8 --- /dev/null +++ b/lib/src/account/mod.rs @@ -0,0 +1,16 @@ +pub mod deserialized_config; +pub use deserialized_config::*; + +pub mod deserialized_account_config; +pub use deserialized_account_config::*; + +// pub mod account_handlers; + +pub mod account_config; +pub use account_config::*; + +pub mod format; +pub use format::*; + +pub mod hooks; +pub use hooks::*; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1b4a90c..9c8fa74 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,8 +1,2 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} +pub mod account; +mod process; diff --git a/lib/src/process.rs b/lib/src/process.rs new file mode 100644 index 0000000..3f19104 --- /dev/null +++ b/lib/src/process.rs @@ -0,0 +1,25 @@ +use log::debug; +use std::{io, process::Command, result, string}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ProcessError { + #[error("cannot run command")] + RunCmdError(#[from] io::Error), + #[error("cannot parse command output")] + ParseCmdOutputError(#[from] string::FromUtf8Error), +} + +type Result = result::Result; + +pub fn run_cmd(cmd: &str) -> Result { + debug!("running command: {}", cmd); + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(&["/C", cmd]).output() + } else { + Command::new("sh").arg("-c").arg(cmd).output() + }?; + + Ok(String::from_utf8(output.stdout)?) +} diff --git a/tests/test_notmuch_backend.rs b/tests/test_notmuch_backend.rs index 161fb41..dbd44c5 100644 --- a/tests/test_notmuch_backend.rs +++ b/tests/test_notmuch_backend.rs @@ -1,11 +1,13 @@ #[cfg(feature = "notmuch-backend")] use std::{collections::HashMap, env, fs, iter::FromIterator}; + #[cfg(feature = "notmuch-backend")] use himalaya::{ backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, - config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, }; +#[cfg(feature = "notmuch-backend")] +use himalaya_lib::account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig} #[cfg(feature = "notmuch-backend")] #[test] From a0461d84ba254f1d45b01548f86f345a2ad2f02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 28 May 2022 20:03:27 +0200 Subject: [PATCH 13/24] use default rust toolchain components --- flake.nix | 3 +-- rust-toolchain.toml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 061930b..79b4819 100644 --- a/flake.nix +++ b/flake.nix @@ -58,7 +58,7 @@ # nix develop devShell = pkgs.mkShell { inputsFrom = builtins.attrValues self.packages.${system}; - buildInputs = with pkgs; [ + nativeBuildInputs = with pkgs; [ # Nix LSP + formatter rnix-lsp nixpkgs-fmt @@ -67,7 +67,6 @@ (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) cargo-watch rust-analyzer - rustfmt # Notmuch notmuch diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 055c9f0..89378c5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,2 @@ [toolchain] channel = "1.58.1" -components = ["cargo", "rustc", "rustfmt", "rust-analysis", "clippy"] From 7c01f88006effd22905ddfe2d635ac4f436d1eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 29 May 2022 12:36:10 +0200 Subject: [PATCH 14/24] make Backend::get_mboxes return struct instead of trait (#340) This step was necessary to move logic from CLI to lib. Indeed, the trait returned by get_mboxes needed to implement Table, which is related to the CLI module only. --- cli/Cargo.toml | 2 +- cli/src/backends/backend.rs | 8 +- cli/src/backends/imap/imap_backend.rs | 46 ++++-- cli/src/backends/imap/imap_mbox.rs | 154 -------------------- cli/src/backends/imap/imap_mbox_attr.rs | 119 --------------- cli/src/backends/maildir/maildir_backend.rs | 50 +++++-- cli/src/backends/maildir/maildir_mbox.rs | 144 ------------------ cli/src/backends/notmuch/notmuch_backend.rs | 32 ++-- cli/src/backends/notmuch/notmuch_mbox.rs | 83 ----------- cli/src/lib.rs | 15 +- cli/src/mbox/mbox.rs | 20 ++- cli/src/mbox/mbox_handlers.rs | 21 +-- cli/src/mbox/mboxes.rs | 16 ++ flake.nix | 1 + lib/Cargo.toml | 2 +- lib/src/lib.rs | 4 +- lib/src/mbox/mbox.rs | 19 +++ lib/src/mbox/mboxes.rs | 25 ++++ lib/src/mbox/mod.rs | 5 + rust-toolchain.toml | 2 +- 20 files changed, 193 insertions(+), 575 deletions(-) delete mode 100644 cli/src/backends/imap/imap_mbox.rs delete mode 100644 cli/src/backends/imap/imap_mbox_attr.rs delete mode 100644 cli/src/backends/maildir/maildir_mbox.rs delete mode 100644 cli/src/backends/notmuch/notmuch_mbox.rs create mode 100644 cli/src/mbox/mboxes.rs create mode 100644 lib/src/mbox/mbox.rs create mode 100644 lib/src/mbox/mboxes.rs create mode 100644 lib/src/mbox/mod.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 50fea80..f974cc9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,7 +20,7 @@ section = "mail" imap-backend = ["imap", "imap-proto"] maildir-backend = ["maildir", "md5"] notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend"] +default = ["imap-backend", "maildir-backend", "notmuch-backend"] [dependencies] ammonia = "3.1.2" diff --git a/cli/src/backends/backend.rs b/cli/src/backends/backend.rs index d00ad1f..d6cb7b4 100644 --- a/cli/src/backends/backend.rs +++ b/cli/src/backends/backend.rs @@ -4,11 +4,9 @@ //! custom backend implementations. use anyhow::Result; +use himalaya_lib::mbox::Mboxes; -use crate::{ - mbox::Mboxes, - msg::{Envelopes, Msg}, -}; +use crate::msg::{Envelopes, Msg}; pub trait Backend<'a> { fn connect(&mut self) -> Result<()> { @@ -16,7 +14,7 @@ pub trait Backend<'a> { } fn add_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_mboxes(&mut self) -> Result>; + fn get_mboxes(&mut self) -> Result; fn del_mbox(&mut self, mbox: &str) -> Result<()>; fn get_envelopes( &mut self, diff --git a/cli/src/backends/imap/imap_backend.rs b/cli/src/backends/imap/imap_backend.rs index 9f9bc5c..f54d066 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/cli/src/backends/imap/imap_backend.rs @@ -3,7 +3,11 @@ //! This module contains the definition of the IMAP backend. use anyhow::{anyhow, Context, Result}; -use himalaya_lib::account::{AccountConfig, ImapBackendConfig}; +use himalaya_lib::{ + account::{AccountConfig, ImapBackendConfig}, + mbox::{Mbox, Mboxes}, +}; +use imap::types::NameAttribute; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; use std::{ @@ -14,10 +18,7 @@ use std::{ }; use crate::{ - backends::{ - imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes, - }, - mbox::Mboxes, + backends::{imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes}, msg::{Envelopes, Msg}, output::run_cmd, }; @@ -211,13 +212,38 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .context(format!("cannot create imap mailbox {:?}", mbox)) } - fn get_mboxes(&mut self) -> Result> { - let mboxes: ImapMboxes = self + fn get_mboxes(&mut self) -> Result { + trace!(">> get imap mailboxes"); + + let imap_mboxes = self .sess()? .list(Some(""), Some("*")) - .context("cannot list mailboxes")? - .into(); - Ok(Box::new(mboxes)) + .context("cannot list mailboxes")?; + let mboxes = Mboxes { + mboxes: imap_mboxes + .iter() + .map(|imap_mbox| Mbox { + delim: imap_mbox.delimiter().unwrap_or_default().into(), + name: imap_mbox.name().into(), + desc: imap_mbox + .attributes() + .iter() + .map(|attr| match attr { + NameAttribute::Marked => "Marked", + NameAttribute::Unmarked => "Unmarked", + NameAttribute::NoSelect => "NoSelect", + NameAttribute::NoInferiors => "NoInferiors", + NameAttribute::Custom(custom) => custom.trim_start_matches('\\'), + }) + .collect::>() + .join(", "), + }) + .collect(), + }; + + trace!("imap mailboxes: {:?}", mboxes); + trace!("<< get imap mailboxes"); + Ok(mboxes) } fn del_mbox(&mut self, mbox: &str) -> Result<()> { diff --git a/cli/src/backends/imap/imap_mbox.rs b/cli/src/backends/imap/imap_mbox.rs deleted file mode 100644 index 223ab6c..0000000 --- a/cli/src/backends/imap/imap_mbox.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! IMAP mailbox module. -//! -//! This module provides IMAP types and conversion utilities related -//! to the mailbox. - -use anyhow::Result; -use serde::Serialize; -use std::fmt::{self, Display}; -use std::ops::Deref; - -use crate::mbox::Mboxes; -use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -use super::ImapMboxAttrs; - -/// Represents a list of IMAP mailboxes. -#[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.mboxes - } -} - -impl PrintTable for ImapMboxes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -impl Mboxes for ImapMboxes { - // -} - -/// Represents the IMAP mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct ImapMbox { - /// Represents the mailbox hierarchie delimiter. - pub delim: String, - - /// Represents the mailbox name. - pub name: String, - - /// Represents the mailbox attributes. - pub attrs: ImapMboxAttrs, -} - -impl ImapMbox { - pub fn new(name: &str) -> Self { - Self { - name: name.into(), - ..Self::default() - } - } -} - -impl Display for ImapMbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for ImapMbox { - fn head() -> Row { - Row::new() - .cell(Cell::new("DELIM").bold().underline().white()) - .cell(Cell::new("NAME").bold().underline().white()) - .cell( - Cell::new("ATTRIBUTES") - .shrinkable() - .bold() - .underline() - .white(), - ) - } - - fn row(&self) -> Row { - Row::new() - .cell(Cell::new(&self.delim).white()) - .cell(Cell::new(&self.name).green()) - .cell(Cell::new(&self.attrs.to_string()).shrinkable().blue()) - } -} - -#[cfg(test)] -mod tests { - use crate::backends::ImapMboxAttr; - - use super::*; - - #[test] - fn it_should_create_new_mbox() { - assert_eq!(ImapMbox::default(), ImapMbox::new("")); - assert_eq!( - ImapMbox { - name: "INBOX".into(), - ..ImapMbox::default() - }, - ImapMbox::new("INBOX") - ); - } - - #[test] - fn it_should_display_mbox() { - let default_mbox = ImapMbox::default(); - assert_eq!("", default_mbox.to_string()); - - let new_mbox = ImapMbox::new("INBOX"); - assert_eq!("INBOX", new_mbox.to_string()); - - let full_mbox = ImapMbox { - delim: ".".into(), - name: "Sent".into(), - attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]), - }; - assert_eq!("Sent", full_mbox.to_string()); - } -} - -/// Represents a list of raw mailboxes returned by the `imap` crate. -pub type RawImapMboxes = imap::types::ZeroCopy>; - -impl<'a> From for ImapMboxes { - fn from(raw_mboxes: RawImapMboxes) -> Self { - Self { - mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(), - } - } -} - -/// Represents the raw mailbox returned by the `imap` crate. -pub type RawImapMbox = imap::types::Name; - -impl<'a> From<&'a RawImapMbox> for ImapMbox { - fn from(raw_mbox: &'a RawImapMbox) -> Self { - Self { - delim: raw_mbox.delimiter().unwrap_or_default().into(), - name: raw_mbox.name().into(), - attrs: raw_mbox.attributes().into(), - } - } -} diff --git a/cli/src/backends/imap/imap_mbox_attr.rs b/cli/src/backends/imap/imap_mbox_attr.rs deleted file mode 100644 index 208d067..0000000 --- a/cli/src/backends/imap/imap_mbox_attr.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! IMAP mailbox attribute module. -//! -//! This module provides IMAP types and conversion utilities related -//! to the mailbox attribute. - -/// Represents the raw mailbox attribute returned by the `imap` crate. -pub use imap::types::NameAttribute as RawImapMboxAttr; -use std::{ - fmt::{self, Display}, - ops::Deref, -}; - -/// Represents the attributes of the mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct ImapMboxAttrs(pub Vec); - -impl Deref for ImapMboxAttrs { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for ImapMboxAttrs { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut glue = ""; - for attr in self.iter() { - write!(f, "{}{}", glue, attr)?; - glue = ", "; - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] -pub enum ImapMboxAttr { - NoInferiors, - NoSelect, - Marked, - Unmarked, - Custom(String), -} - -/// Makes the attribute displayable. -impl Display for ImapMboxAttr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"), - ImapMboxAttr::NoSelect => write!(f, "NoSelect"), - ImapMboxAttr::Marked => write!(f, "Marked"), - ImapMboxAttr::Unmarked => write!(f, "Unmarked"), - ImapMboxAttr::Custom(custom) => write!(f, "{}", custom), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_should_display_attrs() { - macro_rules! attrs_from { - ($($attr:expr),*) => { - ImapMboxAttrs(vec![$($attr,)*]).to_string() - }; - } - - let empty_attr = attrs_from![]; - let single_attr = attrs_from![ImapMboxAttr::NoInferiors]; - let multiple_attrs = attrs_from![ - ImapMboxAttr::Custom("AttrCustom".into()), - ImapMboxAttr::NoInferiors - ]; - - assert_eq!("", empty_attr); - assert_eq!("NoInferiors", single_attr); - assert!(multiple_attrs.contains("NoInferiors")); - assert!(multiple_attrs.contains("AttrCustom")); - assert!(multiple_attrs.contains(",")); - } - - #[test] - fn it_should_display_attr() { - macro_rules! attr_from { - ($attr:ident) => { - ImapMboxAttr::$attr.to_string() - }; - ($custom:literal) => { - ImapMboxAttr::Custom($custom.into()).to_string() - }; - } - - assert_eq!("NoInferiors", attr_from![NoInferiors]); - assert_eq!("NoSelect", attr_from![NoSelect]); - assert_eq!("Marked", attr_from![Marked]); - assert_eq!("Unmarked", attr_from![Unmarked]); - assert_eq!("CustomAttr", attr_from!["CustomAttr"]); - } -} - -impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs { - fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self { - Self(raw_attrs.iter().map(ImapMboxAttr::from).collect()) - } -} - -impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr { - fn from(attr: &'a RawImapMboxAttr<'a>) -> Self { - match attr { - RawImapMboxAttr::NoInferiors => Self::NoInferiors, - RawImapMboxAttr::NoSelect => Self::NoSelect, - RawImapMboxAttr::Marked => Self::Marked, - RawImapMboxAttr::Unmarked => Self::Unmarked, - RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()), - } - } -} diff --git a/cli/src/backends/maildir/maildir_backend.rs b/cli/src/backends/maildir/maildir_backend.rs index a83423d..1f6b61f 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/cli/src/backends/maildir/maildir_backend.rs @@ -4,13 +4,15 @@ //! traits implementation. use anyhow::{anyhow, Context, Result}; -use himalaya_lib::account::{AccountConfig, MaildirBackendConfig}; +use himalaya_lib::{ + account::{AccountConfig, MaildirBackendConfig}, + mbox::{Mbox, Mboxes}, +}; use log::{debug, info, trace}; -use std::{convert::TryInto, env, fs, path::PathBuf}; +use std::{convert::TryInto, env, ffi::OsStr, fs, path::PathBuf}; use crate::{ - backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, - mbox::Mboxes, + backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags}, msg::{Envelopes, Msg}, }; @@ -85,17 +87,39 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn get_mboxes(&mut self) -> Result> { - info!(">> get maildir dirs"); + fn get_mboxes(&mut self) -> Result { + trace!(">> get maildir mailboxes"); - let dirs: MaildirMboxes = - self.mdir.list_subdirs().try_into().with_context(|| { - format!("cannot parse maildir dirs from {:?}", self.mdir.path()) - })?; - trace!("dirs: {:?}", dirs); + let mut mboxes = Mboxes::default(); + for (name, desc) in &self.account_config.mailboxes { + mboxes.push(Mbox { + delim: String::from("/"), + name: name.into(), + desc: desc.into(), + }) + } + for entry in self.mdir.list_subdirs() { + let dir = entry?; + let dirname = dir.path().file_name(); + mboxes.push(Mbox { + delim: String::from("/"), + name: dirname + .and_then(OsStr::to_str) + .and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) }) + .ok_or_else(|| { + anyhow!( + "cannot parse maildir subdirectory name from path {:?}", + dirname + ) + })? + .into(), + ..Mbox::default() + }); + } - info!("<< get maildir dirs"); - Ok(Box::new(dirs)) + trace!("maildir mailboxes: {:?}", mboxes); + trace!("<< get maildir mailboxes"); + Ok(mboxes) } fn del_mbox(&mut self, dir: &str) -> Result<()> { diff --git a/cli/src/backends/maildir/maildir_mbox.rs b/cli/src/backends/maildir/maildir_mbox.rs deleted file mode 100644 index 3f2ec2f..0000000 --- a/cli/src/backends/maildir/maildir_mbox.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Maildir mailbox module. -//! -//! This module provides Maildir types and conversion utilities -//! related to the mailbox - -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - ffi::OsStr, - fmt::{self, Display}, - ops::Deref, -}; - -use crate::{ - mbox::Mboxes, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of Maildir mailboxes. -#[derive(Debug, Default, serde::Serialize)] -pub struct MaildirMboxes { - #[serde(rename = "response")] - pub mboxes: Vec, -} - -impl Deref for MaildirMboxes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.mboxes - } -} - -impl PrintTable for MaildirMboxes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -impl Mboxes for MaildirMboxes { - // -} - -/// Represents the mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct MaildirMbox { - /// Represents the mailbox name. - pub name: String, -} - -impl MaildirMbox { - pub fn new(name: &str) -> Self { - Self { name: name.into() } - } -} - -impl Display for MaildirMbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for MaildirMbox { - fn head() -> Row { - Row::new().cell(Cell::new("SUBDIR").bold().underline().white()) - } - - fn row(&self) -> Row { - Row::new().cell(Cell::new(&self.name).green()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_should_create_new_mbox() { - assert_eq!(MaildirMbox::default(), MaildirMbox::new("")); - assert_eq!( - MaildirMbox { - name: "INBOX".into(), - ..MaildirMbox::default() - }, - MaildirMbox::new("INBOX") - ); - } - - #[test] - fn it_should_display_mbox() { - let default_mbox = MaildirMbox::default(); - assert_eq!("", default_mbox.to_string()); - - let new_mbox = MaildirMbox::new("INBOX"); - assert_eq!("INBOX", new_mbox.to_string()); - - let full_mbox = MaildirMbox { - name: "Sent".into(), - }; - assert_eq!("Sent", full_mbox.to_string()); - } -} - -/// Represents a list of raw mailboxes returned by the `maildir` crate. -pub type RawMaildirMboxes = maildir::MaildirEntries; - -impl TryFrom for MaildirMboxes { - type Error = Error; - - fn try_from(mail_entries: RawMaildirMboxes) -> Result { - let mut mboxes = vec![]; - for entry in mail_entries { - mboxes.push(entry?.try_into()?); - } - Ok(MaildirMboxes { mboxes }) - } -} - -/// Represents the raw mailbox returned by the `maildir` crate. -pub type RawMaildirMbox = maildir::Maildir; - -impl TryFrom for MaildirMbox { - type Error = Error; - - fn try_from(mail_entry: RawMaildirMbox) -> Result { - let subdir_name = mail_entry.path().file_name(); - Ok(Self { - name: subdir_name - .and_then(OsStr::to_str) - .and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) }) - .ok_or_else(|| { - anyhow!( - "cannot parse maildir subdirectory name from path {:?}", - subdir_name, - ) - })? - .into(), - }) - } -} diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/cli/src/backends/notmuch/notmuch_backend.rs index 66a2c14..03cbd26 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/cli/src/backends/notmuch/notmuch_backend.rs @@ -1,12 +1,14 @@ use std::{convert::TryInto, fs}; use anyhow::{anyhow, Context, Result}; -use himalaya_lib::account::{AccountConfig, NotmuchBackendConfig}; +use himalaya_lib::{ + account::{AccountConfig, NotmuchBackendConfig}, + mbox::{Mbox, Mboxes}, +}; use log::{debug, info, trace}; use crate::{ - backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, - mbox::Mboxes, + backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes}, msg::{Envelopes, Msg}, }; @@ -115,20 +117,22 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { )) } - fn get_mboxes(&mut self) -> Result> { - info!(">> get notmuch virtual mailboxes"); + fn get_mboxes(&mut self) -> Result { + trace!(">> get notmuch virtual mailboxes"); - let mut mboxes: Vec<_> = self - .account_config - .mailboxes - .iter() - .map(|(k, v)| NotmuchMbox::new(k, v)) - .collect(); - trace!("virtual mailboxes: {:?}", mboxes); + let mut mboxes = Mboxes::default(); + for (name, desc) in &self.account_config.mailboxes { + mboxes.push(Mbox { + name: name.into(), + desc: desc.into(), + ..Mbox::default() + }) + } mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); - info!("<< get notmuch virtual mailboxes"); - Ok(Box::new(NotmuchMboxes { mboxes })) + trace!("notmuch virtual mailboxes: {:?}", mboxes); + trace!("<< get notmuch virtual mailboxes"); + Ok(mboxes) } fn del_mbox(&mut self, _mbox: &str) -> Result<()> { diff --git a/cli/src/backends/notmuch/notmuch_mbox.rs b/cli/src/backends/notmuch/notmuch_mbox.rs deleted file mode 100644 index 2fe1262..0000000 --- a/cli/src/backends/notmuch/notmuch_mbox.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Notmuch mailbox module. -//! -//! This module provides Notmuch types and conversion utilities -//! related to the mailbox - -use anyhow::Result; -use std::{ - fmt::{self, Display}, - ops::Deref, -}; - -use crate::{ - mbox::Mboxes, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of Notmuch mailboxes. -#[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchMboxes { - #[serde(rename = "response")] - pub mboxes: Vec, -} - -impl Deref for NotmuchMboxes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.mboxes - } -} - -impl PrintTable for NotmuchMboxes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -impl Mboxes for NotmuchMboxes { - // -} - -/// Represents the notmuch virtual mailbox. -#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] -pub struct NotmuchMbox { - /// Represents the virtual mailbox name. - pub name: String, - - /// Represents the query associated to the virtual mailbox name. - pub query: String, -} - -impl NotmuchMbox { - pub fn new(name: &str, query: &str) -> Self { - Self { - name: name.into(), - query: query.into(), - } - } -} - -impl Display for NotmuchMbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for NotmuchMbox { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").bold().underline().white()) - .cell(Cell::new("QUERY").bold().underline().white()) - } - - fn row(&self) -> Row { - Row::new() - .cell(Cell::new(&self.name).white()) - .cell(Cell::new(&self.query).green()) - } -} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 2e0a581..144794f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,6 +2,9 @@ pub mod mbox { pub mod mbox; pub use mbox::*; + pub mod mboxes; + pub use mboxes::*; + pub mod mbox_args; pub mod mbox_handlers; } @@ -49,12 +52,6 @@ pub mod backends { pub mod imap_handlers; - pub mod imap_mbox; - pub use imap_mbox::*; - - pub mod imap_mbox_attr; - pub use imap_mbox_attr::*; - pub mod imap_envelope; pub use imap_envelope::*; @@ -72,9 +69,6 @@ pub mod backends { pub mod maildir_backend; pub use maildir_backend::*; - pub mod maildir_mbox; - pub use maildir_mbox::*; - pub mod maildir_envelope; pub use maildir_envelope::*; @@ -90,9 +84,6 @@ pub mod backends { pub mod notmuch_backend; pub use notmuch_backend::*; - pub mod notmuch_mbox; - pub use notmuch_mbox::*; - pub mod notmuch_envelope; pub use notmuch_envelope::*; } diff --git a/cli/src/mbox/mbox.rs b/cli/src/mbox/mbox.rs index 45f7713..e98e743 100644 --- a/cli/src/mbox/mbox.rs +++ b/cli/src/mbox/mbox.rs @@ -1,7 +1,19 @@ -use std::fmt; +use himalaya_lib::mbox::Mbox; -use crate::output::PrintTable; +use crate::ui::{Cell, Row, Table}; -pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable { - // +impl Table for Mbox { + fn head() -> Row { + Row::new() + .cell(Cell::new("DELIM").bold().underline().white()) + .cell(Cell::new("NAME").bold().underline().white()) + .cell(Cell::new("DESC").bold().underline().white()) + } + + fn row(&self) -> Row { + Row::new() + .cell(Cell::new(&self.delim).white()) + .cell(Cell::new(&self.name).blue()) + .cell(Cell::new(&self.desc).green()) + } } diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index 332db46..f8f5c5e 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -22,7 +22,8 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let mboxes = backend.get_mboxes()?; trace!("mailboxes: {:?}", mboxes); printer.print_table( - mboxes, + // TODO: remove Box + Box::new(mboxes), PrintTableOpts { format: &config.format, max_width, @@ -32,12 +33,11 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( #[cfg(test)] mod tests { + use himalaya_lib::mbox::{Mbox, Mboxes}; use std::{fmt::Debug, io}; use termcolor::ColorSpec; use crate::{ - backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes}, - mbox::Mboxes, msg::{Envelopes, Msg}, output::{Print, PrintTable, WriteColor}, }; @@ -114,24 +114,19 @@ mod tests { fn add_mbox(&mut self, _: &str) -> Result<()> { unimplemented!(); } - fn get_mboxes(&mut self) -> Result> { - Ok(Box::new(ImapMboxes { + fn get_mboxes(&mut self) -> Result { + Ok(Mboxes { mboxes: vec![ - ImapMbox { + Mbox { delim: "/".into(), name: "INBOX".into(), - attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]), }, - ImapMbox { + Mbox { 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/cli/src/mbox/mboxes.rs b/cli/src/mbox/mboxes.rs new file mode 100644 index 0000000..9a032ff --- /dev/null +++ b/cli/src/mbox/mboxes.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use himalaya_lib::mbox::Mboxes; + +use crate::{ + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::Table, +}; + +impl PrintTable for Mboxes { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} diff --git a/flake.nix b/flake.nix index 79b4819..c3c7af2 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,7 @@ # nix develop devShell = pkgs.mkShell { + RUSTUP_TOOLCHAIN = "stable"; inputsFrom = builtins.attrValues self.packages.${system}; nativeBuildInputs = with pkgs; [ # Nix LSP + formatter diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 4bfae01..51d2f2c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" imap-backend = ["imap", "imap-proto"] maildir-backend = ["maildir", "md5"] notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend"] +default = ["imap-backend", "maildir-backend", "notmuch-backend"] [dependencies] lettre = { version = "0.10.0-rc.1", features = ["serde"] } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 9c8fa74..87eb169 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,2 +1,4 @@ -pub mod account; mod process; + +pub mod account; +pub mod mbox; diff --git a/lib/src/mbox/mbox.rs b/lib/src/mbox/mbox.rs new file mode 100644 index 0000000..b14606c --- /dev/null +++ b/lib/src/mbox/mbox.rs @@ -0,0 +1,19 @@ +use serde::Serialize; +use std::fmt; + +/// Represents the mailbox. +#[derive(Debug, Default, PartialEq, Eq, Serialize)] +pub struct Mbox { + /// Represents the mailbox hierarchie delimiter. + pub delim: String, + /// Represents the mailbox name. + pub name: String, + /// Represents the mailbox description. + pub desc: String, +} + +impl fmt::Display for Mbox { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/lib/src/mbox/mboxes.rs b/lib/src/mbox/mboxes.rs new file mode 100644 index 0000000..161479e --- /dev/null +++ b/lib/src/mbox/mboxes.rs @@ -0,0 +1,25 @@ +use serde::Serialize; +use std::ops::{Deref, DerefMut}; + +use super::Mbox; + +/// Represents the list of mailboxes. +#[derive(Debug, Default, Serialize)] +pub struct Mboxes { + #[serde(rename = "response")] + pub mboxes: Vec, +} + +impl Deref for Mboxes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.mboxes + } +} + +impl DerefMut for Mboxes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.mboxes + } +} diff --git a/lib/src/mbox/mod.rs b/lib/src/mbox/mod.rs new file mode 100644 index 0000000..5efcf73 --- /dev/null +++ b/lib/src/mbox/mod.rs @@ -0,0 +1,5 @@ +mod mbox; +pub use mbox::*; + +mod mboxes; +pub use mboxes::*; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 89378c5..292fe49 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.58.1" +channel = "stable" From e1c92d3f5767a79600114f5f30e90a02ada35790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 29 May 2022 14:04:35 +0200 Subject: [PATCH 15/24] make Backend::add_msg return String instead of trait (#340) This step was necessary to move logic from CLI to lib. --- cli/src/backends/backend.rs | 2 +- cli/src/backends/imap/imap_backend.rs | 4 ++-- cli/src/backends/maildir/maildir_backend.rs | 4 ++-- cli/src/backends/notmuch/notmuch_backend.rs | 4 ++-- cli/src/mbox/mbox_handlers.rs | 10 ++++++---- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cli/src/backends/backend.rs b/cli/src/backends/backend.rs index d6cb7b4..92dc363 100644 --- a/cli/src/backends/backend.rs +++ b/cli/src/backends/backend.rs @@ -30,7 +30,7 @@ pub trait Backend<'a> { page_size: usize, page: usize, ) -> Result>; - fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result>; + fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result; fn get_msg(&mut self, mbox: &str, id: &str) -> Result; fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; diff --git a/cli/src/backends/imap/imap_backend.rs b/cli/src/backends/imap/imap_backend.rs index f54d066..76ed158 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/cli/src/backends/imap/imap_backend.rs @@ -342,7 +342,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { Ok(Box::new(envelopes)) } - fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result> { + fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { let flags: ImapFlags = flags.into(); self.sess()? .append(mbox, msg) @@ -354,7 +354,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))? .exists; - Ok(Box::new(last_seq)) + Ok(last_seq.to_string()) } fn get_msg(&mut self, mbox: &str, seq: &str) -> Result { diff --git a/cli/src/backends/maildir/maildir_backend.rs b/cli/src/backends/maildir/maildir_backend.rs index 1f6b61f..0acba73 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/cli/src/backends/maildir/maildir_backend.rs @@ -215,7 +215,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { )) } - fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result> { + fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result { info!(">> add maildir message"); debug!("dir: {:?}", dir); debug!("flags: {:?}", flags); @@ -246,7 +246,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { })?; info!("<< add maildir message"); - Ok(Box::new(hash)) + Ok(hash) } fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result { diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/cli/src/backends/notmuch/notmuch_backend.rs index 03cbd26..8f71faf 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/cli/src/backends/notmuch/notmuch_backend.rs @@ -197,7 +197,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(envelopes) } - fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result> { + fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result { info!(">> add notmuch envelopes"); debug!("tags: {:?}", tags); @@ -251,7 +251,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { .with_context(|| format!("cannot add flags to notmuch message {:?}", id))?; info!("<< add notmuch envelopes"); - Ok(Box::new(hash)) + Ok(hash) } fn get_msg(&mut self, _: &str, short_hash: &str) -> Result { diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index f8f5c5e..b22a943 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -120,10 +120,12 @@ mod tests { Mbox { delim: "/".into(), name: "INBOX".into(), + desc: "desc".into(), }, Mbox { delim: "/".into(), name: "Sent".into(), + desc: "desc".into(), }, ], }) @@ -144,7 +146,7 @@ mod tests { ) -> Result> { unimplemented!() } - fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result> { + fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result { unimplemented!() } fn get_msg(&mut self, _: &str, _: &str) -> Result { @@ -179,9 +181,9 @@ mod tests { assert_eq!( concat![ "\n", - "DELIM │NAME │ATTRIBUTES \n", - "/ │INBOX │NoSelect \n", - "/ │Sent │NoInferiors, HasNoChildren \n", + "DELIM │NAME │DESC \n", + "/ │INBOX │desc \n", + "/ │Sent │desc \n", "\n" ], printer.writer.content From ca67780341d429edfebb1743b298d77ab9e081a6 Mon Sep 17 00:00:00 2001 From: TornaxO7 <50843046+TornaxO7@users.noreply.github.com> Date: Sat, 4 Jun 2022 11:17:29 +0200 Subject: [PATCH 16/24] updating the version of lettre (#389) --- Cargo.lock | 15 +++++++++++++-- lib/Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc51987..2967628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "email-encoding" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b91dddc343e7eaa27f9764e5bffe57370d957017fdd75244f5045e829a8441" +dependencies = [ + "base64", + "memchr 2.4.1", +] + [[package]] name = "encoding_rs" version = "0.8.30" @@ -605,11 +615,12 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.0-rc.4" +version = "0.10.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d8da8f34d086b081c9cc3b57d3bb3b51d16fc06b5c848a188e2f14d58ac2a5" +checksum = "2f6c70001f7ee6c93b6687a06607c7a38f9a7ae460139a496c23da21e95bc289" dependencies = [ "base64", + "email-encoding", "fastrand", "futures-util", "hostname", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 51d2f2c..fecd793 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,7 +10,7 @@ notmuch-backend = ["notmuch", "maildir-backend"] default = ["imap-backend", "maildir-backend", "notmuch-backend"] [dependencies] -lettre = { version = "0.10.0-rc.1", features = ["serde"] } +lettre = { version = "0.10.0-rc.6", features = ["serde"] } log = "0.4.14" mailparse = "0.13.6" serde = { version = "1.0.118", features = ["derive"] } From 8f667def0ca620f90ec17d0e9f91f0d625802928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 4 Jun 2022 12:34:03 +0200 Subject: [PATCH 17/24] move envelopes and flags to lib refactor maildir envelopes/flags refactor notmuch envelopes --- cli/Cargo.toml | 2 +- cli/src/backends/backend.rs | 13 +- cli/src/backends/imap/imap_backend.rs | 52 ++-- cli/src/backends/imap/imap_envelope.rs | 240 +++++------------- cli/src/backends/imap/imap_envelopes.rs | 15 ++ cli/src/backends/imap/imap_flag.rs | 160 +----------- cli/src/backends/imap/imap_flags.rs | 22 ++ cli/src/backends/maildir/maildir_backend.rs | 41 +-- cli/src/backends/maildir/maildir_envelope.rs | 228 ++++------------- cli/src/backends/maildir/maildir_envelopes.rs | 25 ++ cli/src/backends/maildir/maildir_flag.rs | 136 +--------- cli/src/backends/maildir/maildir_flags.rs | 7 + cli/src/backends/notmuch/notmuch_backend.rs | 27 +- cli/src/backends/notmuch/notmuch_envelope.rs | 222 +++++----------- cli/src/backends/notmuch/notmuch_envelopes.rs | 18 ++ cli/src/lib.rs | 18 ++ cli/src/msg/envelope.rs | 33 ++- cli/src/msg/envelopes.rs | 16 ++ cli/src/msg/msg_handlers.rs | 6 +- lib/Cargo.toml | 2 +- lib/src/lib.rs | 1 + lib/src/msg/envelope.rs | 21 ++ lib/src/msg/envelopes.rs | 25 ++ lib/src/msg/flag.rs | 27 ++ lib/src/msg/flags.rs | 89 +++++++ lib/src/msg/mod.rs | 11 + 26 files changed, 578 insertions(+), 879 deletions(-) create mode 100644 cli/src/backends/imap/imap_envelopes.rs create mode 100644 cli/src/backends/imap/imap_flags.rs create mode 100644 cli/src/backends/maildir/maildir_envelopes.rs create mode 100644 cli/src/backends/maildir/maildir_flags.rs create mode 100644 cli/src/backends/notmuch/notmuch_envelopes.rs create mode 100644 cli/src/msg/envelopes.rs create mode 100644 lib/src/msg/envelope.rs create mode 100644 lib/src/msg/envelopes.rs create mode 100644 lib/src/msg/flag.rs create mode 100644 lib/src/msg/flags.rs create mode 100644 lib/src/msg/mod.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f974cc9..50fea80 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,7 +20,7 @@ section = "mail" imap-backend = ["imap", "imap-proto"] maildir-backend = ["maildir", "md5"] notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend", "notmuch-backend"] +default = ["imap-backend", "maildir-backend"] [dependencies] ammonia = "3.1.2" diff --git a/cli/src/backends/backend.rs b/cli/src/backends/backend.rs index 92dc363..0162d05 100644 --- a/cli/src/backends/backend.rs +++ b/cli/src/backends/backend.rs @@ -4,9 +4,9 @@ //! custom backend implementations. use anyhow::Result; -use himalaya_lib::mbox::Mboxes; +use himalaya_lib::{mbox::Mboxes, msg::Envelopes}; -use crate::msg::{Envelopes, Msg}; +use crate::msg::Msg; pub trait Backend<'a> { fn connect(&mut self) -> Result<()> { @@ -16,12 +16,7 @@ pub trait Backend<'a> { fn add_mbox(&mut self, mbox: &str) -> Result<()>; fn get_mboxes(&mut self) -> Result; fn del_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_envelopes( - &mut self, - mbox: &str, - page_size: usize, - page: usize, - ) -> Result>; + fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result; fn search_envelopes( &mut self, mbox: &str, @@ -29,7 +24,7 @@ pub trait Backend<'a> { sort: &str, page_size: usize, page: usize, - ) -> Result>; + ) -> Result; fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result; fn get_msg(&mut self, mbox: &str, id: &str) -> Result; fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; diff --git a/cli/src/backends/imap/imap_backend.rs b/cli/src/backends/imap/imap_backend.rs index 76ed158..71e9a32 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/cli/src/backends/imap/imap_backend.rs @@ -6,25 +6,22 @@ use anyhow::{anyhow, Context, Result}; use himalaya_lib::{ account::{AccountConfig, ImapBackendConfig}, mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags}, }; use imap::types::NameAttribute; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; -use std::{ - collections::HashSet, - convert::{TryFrom, TryInto}, - net::TcpStream, - thread, -}; +use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; use crate::{ - backends::{imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes}, - msg::{Envelopes, Msg}, + backends::{ + from_imap_fetch, from_imap_fetches, imap::msg_sort_criterion::SortCriteria, + into_imap_flags, Backend, + }, + msg::Msg, output::run_cmd, }; -use super::ImapFlags; - type ImapSess = imap::Session>; pub struct ImapBackend<'a> { @@ -148,7 +145,7 @@ impl<'a> ImapBackend<'a> { .context("cannot fetch new messages enveloppe")?; for fetch in fetches.iter() { - let msg = ImapEnvelope::try_from(fetch)?; + let msg = from_imap_fetch(fetch)?; let uid = fetch.uid.ok_or_else(|| { anyhow!("cannot retrieve message {}'s UID", fetch.message) })?; @@ -252,12 +249,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .context(format!("cannot delete imap mailbox {:?}", mbox)) } - fn get_envelopes( - &mut self, - mbox: &str, - page_size: usize, - page: usize, - ) -> Result> { + fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result { let last_seq = self .sess()? .select(mbox) @@ -265,7 +257,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .exists as usize; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let range = if page_size > 0 { @@ -282,8 +274,8 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .context(format!("cannot fetch messages within range {:?}", range))?; - let envelopes: ImapEnvelopes = fetches.try_into()?; - Ok(Box::new(envelopes)) + + from_imap_fetches(fetches) } fn search_envelopes( @@ -293,7 +285,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { sort: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { let last_seq = self .sess()? .select(mbox) @@ -301,7 +293,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .exists; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let begin = page * page_size; @@ -330,7 +322,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .collect() }; if seqs.is_empty() { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let range = seqs[begin..end.min(seqs.len())].join(","); @@ -338,15 +330,15 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .context(format!("cannot fetch messages within range {:?}", range))?; - let envelopes: ImapEnvelopes = fetches.try_into()?; - Ok(Box::new(envelopes)) + + from_imap_fetches(fetches) } fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .append(mbox, msg) - .flags(>>>::into(flags)) + .flags(into_imap_flags(&flags)) .finish() .context(format!("cannot append message to {:?}", mbox))?; let last_seq = self @@ -396,7 +388,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))?; @@ -410,7 +402,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))?; @@ -421,7 +413,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))?; diff --git a/cli/src/backends/imap/imap_envelope.rs b/cli/src/backends/imap/imap_envelope.rs index d087eb1..8a90e3e 100644 --- a/cli/src/backends/imap/imap_envelope.rs +++ b/cli/src/backends/imap/imap_envelope.rs @@ -3,185 +3,79 @@ //! This module provides IMAP types and conversion utilities related //! to the envelope. -use anyhow::{anyhow, Context, Error, Result}; -use std::{convert::TryFrom, ops::Deref}; +use anyhow::{anyhow, Context, Result}; +use himalaya_lib::msg::Envelope; -use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -use super::{ImapFlag, ImapFlags}; - -/// Represents a list of IMAP envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct ImapEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for ImapEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl PrintTable for ImapEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -// impl Envelopes for ImapEnvelopes { -// // -// } - -/// Represents the IMAP envelope. The envelope is just a message -/// subset, and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct ImapEnvelope { - /// Represents the sequence number of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2 - pub id: u32, - - /// Represents the flags attached to the message. - pub flags: ImapFlags, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the internal date of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3 - pub date: Option, -} - -impl Table for ImapEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("ID").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let id = self.id.to_string(); - let flags = self.flags.to_symbols_string(); - let unseen = !self.flags.contains(&ImapFlag::Seen); - let subject = &self.subject; - let sender = &self.sender; - let date = self.date.as_deref().unwrap_or_default(); - Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopes returned by the `imap` crate. -pub type RawImapEnvelopes = imap::types::ZeroCopy>; - -impl TryFrom for ImapEnvelopes { - type Error = Error; - - fn try_from(raw_envelopes: RawImapEnvelopes) -> Result { - let mut envelopes = vec![]; - for raw_envelope in raw_envelopes.iter().rev() { - envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?); - } - Ok(Self { envelopes }) - } -} +use super::from_imap_flags; /// Represents the raw envelope returned by the `imap` crate. -pub type RawImapEnvelope = imap::types::Fetch; +pub type ImapFetch = imap::types::Fetch; -impl TryFrom<&RawImapEnvelope> for ImapEnvelope { - type Error = Error; +pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { + let envelope = fetch + .envelope() + .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; - fn try_from(fetch: &RawImapEnvelope) -> Result { - let envelope = fetch - .envelope() - .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; + let id = fetch.message.to_string(); - // Get the sequence number - let id = fetch.message; + let flags = from_imap_flags(fetch.flags()); - // Get the flags - let flags = ImapFlags::try_from(fetch.flags())?; - - // Get the subject - let subject = envelope - .subject - .as_ref() - .map(|subj| { - rfc2047_decoder::decode(subj).context(format!( - "cannot decode subject of message {}", - fetch.message - )) - }) - .unwrap_or_else(|| Ok(String::default()))?; - - // Get the sender - let sender = envelope - .sender - .as_ref() - .and_then(|addrs| addrs.get(0)) - .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) - .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?; - let sender = if let Some(ref name) = sender.name { - rfc2047_decoder::decode(&name.to_vec()).context(format!( - "cannot decode sender's name of message {}", - fetch.message, - ))? - } else { - let mbox = sender - .mailbox - .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) - .and_then(|mbox| { - rfc2047_decoder::decode(&mbox.to_vec()).context(format!( - "cannot decode sender's mailbox of message {}", - fetch.message, - )) - })?; - let host = sender - .host - .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) - .and_then(|host| { - rfc2047_decoder::decode(&host.to_vec()).context(format!( - "cannot decode sender's host of message {}", - fetch.message, - )) - })?; - format!("{}@{}", mbox, host) - }; - - // Get the internal date - let date = fetch - .internal_date() - .map(|date| date.naive_local().to_string()); - - Ok(Self { - id, - flags, - subject, - sender, - date, + let subject = envelope + .subject + .as_ref() + .map(|subj| { + rfc2047_decoder::decode(subj).context(format!( + "cannot decode subject of message {}", + fetch.message + )) }) - } + .unwrap_or_else(|| Ok(String::default()))?; + + let sender = envelope + .sender + .as_ref() + .and_then(|addrs| addrs.get(0)) + .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) + .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?; + let sender = if let Some(ref name) = sender.name { + rfc2047_decoder::decode(&name.to_vec()).context(format!( + "cannot decode sender's name of message {}", + fetch.message, + ))? + } else { + let mbox = sender + .mailbox + .as_ref() + .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) + .and_then(|mbox| { + rfc2047_decoder::decode(&mbox.to_vec()).context(format!( + "cannot decode sender's mailbox of message {}", + fetch.message, + )) + })?; + let host = sender + .host + .as_ref() + .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) + .and_then(|host| { + rfc2047_decoder::decode(&host.to_vec()).context(format!( + "cannot decode sender's host of message {}", + fetch.message, + )) + })?; + format!("{}@{}", mbox, host) + }; + + let date = fetch + .internal_date() + .map(|date| date.naive_local().to_string()); + + Ok(Envelope { + id: id.clone(), + internal_id: id, + flags, + subject, + sender, + date, + }) } diff --git a/cli/src/backends/imap/imap_envelopes.rs b/cli/src/backends/imap/imap_envelopes.rs new file mode 100644 index 0000000..d0e6591 --- /dev/null +++ b/cli/src/backends/imap/imap_envelopes.rs @@ -0,0 +1,15 @@ +use anyhow::{Context, Result}; +use himalaya_lib::msg::Envelopes; + +use crate::backends::{imap::from_imap_fetch, ImapFetch}; + +/// Represents the list of raw envelopes returned by the `imap` crate. +pub type ImapFetches = imap::types::ZeroCopy>; + +pub fn from_imap_fetches(fetches: ImapFetches) -> Result { + let mut envelopes = Envelopes::default(); + for fetch in fetches.iter().rev() { + envelopes.push(from_imap_fetch(fetch).context("cannot parse imap fetch")?); + } + Ok(envelopes) +} diff --git a/cli/src/backends/imap/imap_flag.rs b/cli/src/backends/imap/imap_flag.rs index 40fdb67..1a24ab0 100644 --- a/cli/src/backends/imap/imap_flag.rs +++ b/cli/src/backends/imap/imap_flag.rs @@ -1,151 +1,15 @@ -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - fmt, - ops::Deref, -}; +use himalaya_lib::msg::Flag; -/// Represents the imap flag variants. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub enum ImapFlag { - Seen, - Answered, - Flagged, - Deleted, - Draft, - Recent, - MayCreate, - Custom(String), -} - -impl From<&str> for ImapFlag { - fn from(flag_str: &str) -> Self { - match flag_str { - "seen" => ImapFlag::Seen, - "answered" | "replied" => ImapFlag::Answered, - "flagged" => ImapFlag::Flagged, - "deleted" | "trashed" => ImapFlag::Deleted, - "draft" => ImapFlag::Draft, - "recent" => ImapFlag::Recent, - "maycreate" | "may-create" => ImapFlag::MayCreate, - flag_str => ImapFlag::Custom(flag_str.into()), - } - } -} - -impl TryFrom<&imap::types::Flag<'_>> for ImapFlag { - type Error = Error; - - fn try_from(flag: &imap::types::Flag<'_>) -> Result { - Ok(match flag { - imap::types::Flag::Seen => ImapFlag::Seen, - imap::types::Flag::Answered => ImapFlag::Answered, - imap::types::Flag::Flagged => ImapFlag::Flagged, - imap::types::Flag::Deleted => ImapFlag::Deleted, - imap::types::Flag::Draft => ImapFlag::Draft, - imap::types::Flag::Recent => ImapFlag::Recent, - imap::types::Flag::MayCreate => ImapFlag::MayCreate, - imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()), - _ => return Err(anyhow!("cannot parse imap flag")), - }) - } -} - -/// Represents the imap flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)] -pub struct ImapFlags(pub Vec); - -impl ImapFlags { - /// Builds a symbols string - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&ImapFlag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&ImapFlag::Answered) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&ImapFlag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl Deref for ImapFlags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for ImapFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut glue = ""; - - for flag in &self.0 { - write!(f, "{}", glue)?; - match flag { - ImapFlag::Seen => write!(f, "\\Seen")?, - ImapFlag::Answered => write!(f, "\\Answered")?, - ImapFlag::Flagged => write!(f, "\\Flagged")?, - ImapFlag::Deleted => write!(f, "\\Deleted")?, - ImapFlag::Draft => write!(f, "\\Draft")?, - ImapFlag::Recent => write!(f, "\\Recent")?, - ImapFlag::MayCreate => write!(f, "\\MayCreate")?, - ImapFlag::Custom(custom) => write!(f, "{}", custom)?, - } - glue = " "; - } - - Ok(()) - } -} - -impl<'a> Into>> for ImapFlags { - fn into(self) -> Vec> { - self.0 - .into_iter() - .map(|flag| match flag { - ImapFlag::Seen => imap::types::Flag::Seen, - ImapFlag::Answered => imap::types::Flag::Answered, - ImapFlag::Flagged => imap::types::Flag::Flagged, - ImapFlag::Deleted => imap::types::Flag::Deleted, - ImapFlag::Draft => imap::types::Flag::Draft, - ImapFlag::Recent => imap::types::Flag::Recent, - ImapFlag::MayCreate => imap::types::Flag::MayCreate, - ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()), - }) - .collect() - } -} - -impl From<&str> for ImapFlags { - fn from(flags_str: &str) -> Self { - ImapFlags( - flags_str - .split_whitespace() - .map(|flag_str| flag_str.trim().into()) - .collect(), - ) - } -} - -impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags { - type Error = Error; - - fn try_from(flags: &[imap::types::Flag<'_>]) -> Result { - let mut f = vec![]; - for flag in flags { - f.push(flag.try_into()?); - } - Ok(Self(f)) +pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag { + match imap_flag { + imap::types::Flag::Seen => Flag::Seen, + imap::types::Flag::Answered => Flag::Answered, + imap::types::Flag::Flagged => Flag::Flagged, + imap::types::Flag::Deleted => Flag::Deleted, + imap::types::Flag::Draft => Flag::Draft, + imap::types::Flag::Recent => Flag::Recent, + imap::types::Flag::MayCreate => Flag::Custom(String::from("MayCreate")), + imap::types::Flag::Custom(flag) => Flag::Custom(flag.to_string()), + flag => Flag::Custom(flag.to_string()), } } diff --git a/cli/src/backends/imap/imap_flags.rs b/cli/src/backends/imap/imap_flags.rs new file mode 100644 index 0000000..186328a --- /dev/null +++ b/cli/src/backends/imap/imap_flags.rs @@ -0,0 +1,22 @@ +use himalaya_lib::msg::{Flag, Flags}; + +use super::from_imap_flag; + +pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec> { + flags + .iter() + .map(|flag| match flag { + Flag::Seen => imap::types::Flag::Seen, + Flag::Answered => imap::types::Flag::Answered, + Flag::Flagged => imap::types::Flag::Flagged, + Flag::Deleted => imap::types::Flag::Deleted, + Flag::Draft => imap::types::Flag::Draft, + Flag::Recent => imap::types::Flag::Recent, + Flag::Custom(flag) => imap::types::Flag::Custom(flag.into()), + }) + .collect() +} + +pub fn from_imap_flags(imap_flags: &[imap::types::Flag<'_>]) -> Flags { + imap_flags.iter().map(from_imap_flag).collect() +} diff --git a/cli/src/backends/maildir/maildir_backend.rs b/cli/src/backends/maildir/maildir_backend.rs index 0acba73..2c4e2d0 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/cli/src/backends/maildir/maildir_backend.rs @@ -7,13 +7,14 @@ use anyhow::{anyhow, Context, Result}; use himalaya_lib::{ account::{AccountConfig, MaildirBackendConfig}, mbox::{Mbox, Mboxes}, + msg::Envelopes, }; use log::{debug, info, trace}; -use std::{convert::TryInto, env, ffi::OsStr, fs, path::PathBuf}; +use std::{env, ffi::OsStr, fs, path::PathBuf}; use crate::{ - backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags}, - msg::{Envelopes, Msg}, + backends::{maildir_envelopes, Backend, IdMapper}, + msg::Msg, }; /// Represents the maildir backend. @@ -136,12 +137,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn get_envelopes( - &mut self, - dir: &str, - page_size: usize, - page: usize, - ) -> Result> { + fn get_envelopes(&mut self, dir: &str, page_size: usize, page: usize) -> Result { info!(">> get maildir envelopes"); debug!("dir: {:?}", dir); debug!("page size: {:?}", page_size); @@ -153,9 +149,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { // Reads envelopes from the "cur" folder of the selected // maildir. - let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| { - format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) - })?; + let mut envelopes = + maildir_envelopes::from_maildir_entries(mdir.list_cur()).with_context(|| { + format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) + })?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -185,7 +182,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mut mapper = IdMapper::new(mdir.path())?; let entries = envelopes .iter() - .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) .collect(); mapper.append(entries)? }; @@ -194,10 +191,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { // Shorten envelopes hash. envelopes .iter_mut() - .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); info!("<< get maildir envelopes"); - Ok(Box::new(envelopes)) + Ok(envelopes) } fn search_envelopes( @@ -207,7 +204,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { _sort: &str, _page_size: usize, _page: usize, - ) -> Result> { + ) -> Result { info!(">> search maildir envelopes"); info!("<< search maildir envelopes"); Err(anyhow!( @@ -223,9 +220,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) .with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?; @@ -426,9 +420,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? @@ -457,9 +448,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? @@ -488,9 +476,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? diff --git a/cli/src/backends/maildir/maildir_envelope.rs b/cli/src/backends/maildir/maildir_envelope.rs index 10cb3be..8cc9884 100644 --- a/cli/src/backends/maildir/maildir_envelope.rs +++ b/cli/src/backends/maildir/maildir_envelope.rs @@ -1,194 +1,72 @@ -//! Maildir mailbox module. -//! -//! This module provides Maildir types and conversion utilities -//! related to the envelope - -use anyhow::{anyhow, Context, Error, Result}; +use anyhow::{anyhow, Context, Result}; use chrono::DateTime; +use himalaya_lib::msg::Envelope; use log::trace; -use std::{ - convert::{TryFrom, TryInto}, - ops::{Deref, DerefMut}, -}; use crate::{ - backends::{MaildirFlag, MaildirFlags}, + backends::maildir_flags, msg::{from_slice_to_addrs, Addr}, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, }; -/// Represents a list of envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct MaildirEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for MaildirEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl DerefMut for MaildirEnvelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} - -impl PrintTable for MaildirEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -// impl Envelopes for MaildirEnvelopes { -// // -// } - -/// Represents the envelope. The envelope is just a message subset, -/// and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct MaildirEnvelope { - /// Represents the id of the message. - pub id: String, - - /// Represents the MD5 hash of the message id. - pub hash: String, - - /// Represents the flags of the message. - pub flags: MaildirFlags, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the date of the message. - pub date: String, -} - -impl Table for MaildirEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("HASH").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let hash = self.hash.clone(); - let unseen = !self.flags.contains(&MaildirFlag::Seen); - let flags = self.flags.to_symbols_string(); - let subject = &self.subject; - let sender = &self.sender; - let date = &self.date; - Row::new() - .cell(Cell::new(hash).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopees returned by the `maildir` crate. -pub type RawMaildirEnvelopes = maildir::MailEntries; - -impl<'a> TryFrom for MaildirEnvelopes { - type Error = Error; - - fn try_from(mail_entries: RawMaildirEnvelopes) -> Result { - let mut envelopes = vec![]; - for entry in mail_entries { - let envelope: MaildirEnvelope = entry - .context("cannot decode maildir mail entry")? - .try_into() - .context("cannot parse maildir mail entry")?; - envelopes.push(envelope); - } - - Ok(MaildirEnvelopes { envelopes }) - } -} - /// Represents the raw envelope returned by the `maildir` crate. -pub type RawMaildirEnvelope = maildir::MailEntry; +pub type MaildirEnvelope = maildir::MailEntry; -impl<'a> TryFrom for MaildirEnvelope { - type Error = Error; +pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { + trace!(">> build envelope from maildir parsed mail"); - fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result { - trace!(">> build envelope from maildir parsed mail"); + let mut envelope = Envelope::default(); - let mut envelope = Self::default(); + envelope.internal_id = entry.id().to_owned(); + envelope.id = format!("{:x}", md5::compute(&envelope.internal_id)); + envelope.flags = maildir_flags::from_maildir_entry(&entry); - envelope.id = mail_entry.id().into(); - envelope.hash = format!("{:x}", md5::compute(&envelope.id)); - envelope.flags = (&mail_entry) - .try_into() - .context("cannot parse maildir flags")?; + let parsed_mail = entry.parsed().context("cannot parse maildir mail entry")?; - let parsed_mail = mail_entry - .parsed() - .context("cannot parse maildir mail entry")?; + trace!(">> parse headers"); + for h in parsed_mail.get_headers() { + let k = h.get_key(); + trace!("header key: {:?}", k); - trace!(">> parse headers"); - for h in parsed_mail.get_headers() { - let k = h.get_key(); - trace!("header key: {:?}", k); + let v = rfc2047_decoder::decode(h.get_value_raw()) + .context(format!("cannot decode value from header {:?}", k))?; + trace!("header value: {:?}", v); - let v = rfc2047_decoder::decode(h.get_value_raw()) - .context(format!("cannot decode value from header {:?}", k))?; - trace!("header value: {:?}", v); - - match k.to_lowercase().as_str() { - "date" => { - envelope.date = - DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) - .context(format!("cannot parse maildir message date {:?}", v))? - .naive_local() - .to_string(); - } - "subject" => { - envelope.subject = v.into(); - } - "from" => { - envelope.sender = from_slice_to_addrs(v) - .context(format!("cannot parse header {:?}", k))? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => { - group_name.to_owned() - } - }) - .ok_or_else(|| anyhow!("cannot find sender"))?; - } - _ => (), + match k.to_lowercase().as_str() { + "date" => { + envelope.date = + DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) + .map(|date| date.naive_local().to_string()) + .ok() } + "subject" => { + envelope.subject = v.into(); + } + "from" => { + envelope.sender = from_slice_to_addrs(v) + .context(format!("cannot parse header {:?}", k))? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => { + group_name.to_owned() + } + }) + .ok_or_else(|| anyhow!("cannot find sender"))?; + } + _ => (), } - trace!("<< parse headers"); - - trace!("envelope: {:?}", envelope); - trace!("<< build envelope from maildir parsed mail"); - Ok(envelope) } + trace!("<< parse headers"); + + trace!("envelope: {:?}", envelope); + trace!("<< build envelope from maildir parsed mail"); + Ok(envelope) } diff --git a/cli/src/backends/maildir/maildir_envelopes.rs b/cli/src/backends/maildir/maildir_envelopes.rs new file mode 100644 index 0000000..4921227 --- /dev/null +++ b/cli/src/backends/maildir/maildir_envelopes.rs @@ -0,0 +1,25 @@ +//! Maildir mailbox module. +//! +//! This module provides Maildir types and conversion utilities +//! related to the envelope + +use himalaya_lib::msg::Envelopes; +use anyhow::{Result, Context}; + +use super::maildir_envelope; + +/// Represents a list of raw envelopees returned by the `maildir` crate. +pub type MaildirEnvelopes = maildir::MailEntries; + +pub fn from_maildir_entries(mail_entries: MaildirEnvelopes) -> Result { + let mut envelopes = Envelopes::default(); + for entry in mail_entries { + envelopes.push( + maildir_envelope::from_maildir_entry( + entry.context("cannot decode maildir mail entry")?, + ) + .context("cannot parse maildir mail entry")?, + ); + } + Ok(envelopes) +} diff --git a/cli/src/backends/maildir/maildir_flag.rs b/cli/src/backends/maildir/maildir_flag.rs index 1c97cce..b868e8a 100644 --- a/cli/src/backends/maildir/maildir_flag.rs +++ b/cli/src/backends/maildir/maildir_flag.rs @@ -1,129 +1,13 @@ -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - ops::Deref, -}; +use himalaya_lib::msg::Flag; -/// Represents the maildir flag variants. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub enum MaildirFlag { - Passed, - Replied, - Seen, - Trashed, - Draft, - Flagged, - Custom(char), -} - -/// Represents the maildir flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)] -pub struct MaildirFlags(pub Vec); - -impl MaildirFlags { - /// Builds a symbols string - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&MaildirFlag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&MaildirFlag::Replied) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&MaildirFlag::Passed) { - "↗" - } else { - " " - }); - flags.push_str(if self.contains(&MaildirFlag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl Deref for MaildirFlags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ToString for MaildirFlags { - fn to_string(&self) -> String { - self.0 - .iter() - .map(|flag| { - let flag_char: char = flag.into(); - flag_char - }) - .collect() - } -} - -impl TryFrom<&str> for MaildirFlags { - type Error = Error; - - fn try_from(flags_str: &str) -> Result { - let mut flags = vec![]; - for flag_str in flags_str.split_whitespace() { - flags.push(flag_str.trim().try_into()?); - } - Ok(MaildirFlags(flags)) - } -} - -impl From<&maildir::MailEntry> for MaildirFlags { - fn from(mail_entry: &maildir::MailEntry) -> Self { - let mut flags = vec![]; - for c in mail_entry.flags().chars() { - flags.push(match c { - 'P' => MaildirFlag::Passed, - 'R' => MaildirFlag::Replied, - 'S' => MaildirFlag::Seen, - 'T' => MaildirFlag::Trashed, - 'D' => MaildirFlag::Draft, - 'F' => MaildirFlag::Flagged, - custom => MaildirFlag::Custom(custom), - }) - } - Self(flags) - } -} - -impl Into for &MaildirFlag { - fn into(self) -> char { - match self { - MaildirFlag::Passed => 'P', - MaildirFlag::Replied => 'R', - MaildirFlag::Seen => 'S', - MaildirFlag::Trashed => 'T', - MaildirFlag::Draft => 'D', - MaildirFlag::Flagged => 'F', - MaildirFlag::Custom(custom) => *custom, - } - } -} - -impl TryFrom<&str> for MaildirFlag { - type Error = Error; - - fn try_from(flag_str: &str) -> Result { - match flag_str { - "passed" => Ok(MaildirFlag::Passed), - "replied" => Ok(MaildirFlag::Replied), - "seen" => Ok(MaildirFlag::Seen), - "trashed" => Ok(MaildirFlag::Trashed), - "draft" => Ok(MaildirFlag::Draft), - "flagged" => Ok(MaildirFlag::Flagged), - flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)), - } +pub fn from_char(c: char) -> Flag { + match c { + 'R' => Flag::Answered, + 'S' => Flag::Seen, + 'T' => Flag::Deleted, + 'D' => Flag::Draft, + 'F' => Flag::Flagged, + 'P' => Flag::Custom(String::from("Passed")), + flag => Flag::Custom(flag.to_string()), } } diff --git a/cli/src/backends/maildir/maildir_flags.rs b/cli/src/backends/maildir/maildir_flags.rs new file mode 100644 index 0000000..538c90a --- /dev/null +++ b/cli/src/backends/maildir/maildir_flags.rs @@ -0,0 +1,7 @@ +use himalaya_lib::msg::Flags; + +use super::maildir_flag; + +pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { + entry.flags().chars().map(maildir_flag::from_char).collect() +} diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/cli/src/backends/notmuch/notmuch_backend.rs index 8f71faf..b9bcc12 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/cli/src/backends/notmuch/notmuch_backend.rs @@ -1,15 +1,16 @@ -use std::{convert::TryInto, fs}; +use std::fs; use anyhow::{anyhow, Context, Result}; use himalaya_lib::{ account::{AccountConfig, NotmuchBackendConfig}, mbox::{Mbox, Mboxes}, + msg::Envelopes, }; use log::{debug, info, trace}; use crate::{ - backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes}, - msg::{Envelopes, Msg}, + backends::{notmuch_envelopes, Backend, IdMapper, MaildirBackend}, + msg::Msg, }; /// Represents the Notmuch backend. @@ -53,16 +54,16 @@ impl<'a> NotmuchBackend<'a> { query: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { // Gets envelopes matching the given Notmuch query. let query_builder = self .db .create_query(query) .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let mut envelopes: NotmuchEnvelopes = query_builder - .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))? - .try_into() + let mut envelopes = + notmuch_envelopes::from_notmuch_msgs(query_builder.search_messages().with_context( + || format!("cannot find notmuch envelopes from query {:?}", query), + )?) .with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -93,7 +94,7 @@ impl<'a> NotmuchBackend<'a> { let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?; let entries = envelopes .iter() - .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) .collect(); mapper.append(entries)? }; @@ -102,9 +103,9 @@ impl<'a> NotmuchBackend<'a> { // Shorten envelopes hash. envelopes .iter_mut() - .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); - Ok(Box::new(envelopes)) + Ok(envelopes) } } @@ -148,7 +149,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { virt_mbox: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { info!(">> get notmuch envelopes"); debug!("virtual mailbox: {:?}", virt_mbox); debug!("page size: {:?}", page_size); @@ -174,7 +175,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { _sort: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { info!(">> search notmuch envelopes"); debug!("virtual mailbox: {:?}", virt_mbox); debug!("query: {:?}", query); diff --git a/cli/src/backends/notmuch/notmuch_envelope.rs b/cli/src/backends/notmuch/notmuch_envelope.rs index 626d949..4d53ba2 100644 --- a/cli/src/backends/notmuch/notmuch_envelope.rs +++ b/cli/src/backends/notmuch/notmuch_envelope.rs @@ -3,178 +3,72 @@ //! This module provides Notmuch types and conversion utilities //! related to the envelope -use anyhow::{anyhow, Context, Error, Result}; +use anyhow::{anyhow, Context, Result}; use chrono::DateTime; +use himalaya_lib::msg::{Envelope, Flag}; use log::{info, trace}; -use std::{ - convert::{TryFrom, TryInto}, - ops::{Deref, DerefMut}, -}; -use crate::{ - msg::{from_slice_to_addrs, Addr}, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for NotmuchEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl DerefMut for NotmuchEnvelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} - -impl PrintTable for NotmuchEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -/// Represents the envelope. The envelope is just a message subset, -/// and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct NotmuchEnvelope { - /// Represents the id of the message. - pub id: String, - - /// Represents the MD5 hash of the message id. - pub hash: String, - - /// Represents the tags of the message. - pub flags: Vec, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the date of the message. - pub date: String, -} - -impl Table for NotmuchEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("HASH").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let hash = self.hash.to_string(); - let unseen = !self.flags.contains(&String::from("unread")); - let flags = String::new(); - let subject = &self.subject; - let sender = &self.sender; - let date = &self.date; - Row::new() - .cell(Cell::new(hash).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopees returned by the `notmuch` crate. -pub type RawNotmuchEnvelopes = notmuch::Messages; - -impl<'a> TryFrom for NotmuchEnvelopes { - type Error = Error; - - fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result { - let mut envelopes = vec![]; - for raw_envelope in raw_envelopes { - let envelope: NotmuchEnvelope = raw_envelope - .try_into() - .context("cannot parse notmuch mail entry")?; - envelopes.push(envelope); - } - Ok(NotmuchEnvelopes { envelopes }) - } -} +use crate::msg::{from_slice_to_addrs, Addr}; /// Represents the raw envelope returned by the `notmuch` crate. pub type RawNotmuchEnvelope = notmuch::Message; -impl<'a> TryFrom for NotmuchEnvelope { - type Error = Error; +pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { + info!("begin: try building envelope from notmuch parsed mail"); - fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result { - info!("begin: try building envelope from notmuch parsed mail"); + let internal_id = raw_envelope.id().to_string(); + let id = format!("{:x}", md5::compute(&internal_id)); + let subject = raw_envelope + .header("subject") + .context("cannot get header \"Subject\" from notmuch message")? + .unwrap_or_default() + .to_string(); + let sender = raw_envelope + .header("from") + .context("cannot get header \"From\" from notmuch message")? + .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", internal_id))? + .to_string(); + let sender = from_slice_to_addrs(sender)? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), + }) + .ok_or_else(|| anyhow!("cannot find sender"))?; + let date = raw_envelope + .header("date") + .context("cannot get header \"Date\" from notmuch message")? + .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", internal_id))? + .to_string(); + let date = DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) + .context(format!( + "cannot parse message date {:?} of notmuch message {:?}", + date, internal_id + )) + .map(|date| date.naive_local().to_string()) + .ok(); - let id = raw_envelope.id().to_string(); - let hash = format!("{:x}", md5::compute(&id)); - let subject = raw_envelope - .header("subject") - .context("cannot get header \"Subject\" from notmuch message")? - .unwrap_or_default() - .to_string(); - let sender = raw_envelope - .header("from") - .context("cannot get header \"From\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", id))? - .to_string(); - let sender = from_slice_to_addrs(sender)? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), - }) - .ok_or_else(|| anyhow!("cannot find sender"))?; - let date = raw_envelope - .header("date") - .context("cannot get header \"Date\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))? - .to_string(); - let date = - DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) - .context(format!( - "cannot parse message date {:?} of notmuch message {:?}", - date, id - ))? - .naive_local() - .to_string(); + let envelope = Envelope { + id, + internal_id, + flags: raw_envelope + .tags() + .map(|tag| Flag::Custom(tag.to_string())) + .collect(), + subject, + sender, + date, + }; + trace!("envelope: {:?}", envelope); - let envelope = Self { - id, - hash, - flags: raw_envelope.tags().collect(), - subject, - sender, - date, - }; - trace!("envelope: {:?}", envelope); - - info!("end: try building envelope from notmuch parsed mail"); - Ok(envelope) - } + info!("end: try building envelope from notmuch parsed mail"); + Ok(envelope) } diff --git a/cli/src/backends/notmuch/notmuch_envelopes.rs b/cli/src/backends/notmuch/notmuch_envelopes.rs new file mode 100644 index 0000000..7235a46 --- /dev/null +++ b/cli/src/backends/notmuch/notmuch_envelopes.rs @@ -0,0 +1,18 @@ +use anyhow::{Context, Result}; +use himalaya_lib::msg::Envelopes; + +use super::notmuch_envelope; + +/// Represents a list of raw envelopees returned by the `notmuch` +/// crate. +pub type RawNotmuchEnvelopes = notmuch::Messages; + +pub fn from_notmuch_msgs(msgs: RawNotmuchEnvelopes) -> Result { + let mut envelopes = Envelopes::default(); + for msg in msgs { + let envelope = + notmuch_envelope::from_notmuch_msg(msg).context("cannot parse notmuch message")?; + envelopes.push(envelope); + } + Ok(envelopes) +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 144794f..634a52f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -13,6 +13,9 @@ pub mod msg { pub mod envelope; pub use envelope::*; + pub mod envelopes; + pub use envelopes::*; + pub mod msg_args; pub mod msg_handlers; @@ -52,9 +55,15 @@ pub mod backends { pub mod imap_handlers; + pub mod imap_envelopes; + pub use imap_envelopes::*; + pub mod imap_envelope; pub use imap_envelope::*; + pub mod imap_flags; + pub use imap_flags::*; + pub mod imap_flag; pub use imap_flag::*; @@ -69,9 +78,15 @@ pub mod backends { pub mod maildir_backend; pub use maildir_backend::*; + pub mod maildir_envelopes; + pub use maildir_envelopes::*; + pub mod maildir_envelope; pub use maildir_envelope::*; + pub mod maildir_flags; + pub use maildir_flags::*; + pub mod maildir_flag; pub use maildir_flag::*; } @@ -84,6 +99,9 @@ pub mod backends { pub mod notmuch_backend; pub use notmuch_backend::*; + pub mod notmuch_envelopes; + pub use notmuch_envelopes::*; + pub mod notmuch_envelope; pub use notmuch_envelope::*; } diff --git a/cli/src/msg/envelope.rs b/cli/src/msg/envelope.rs index 91c5c04..2f96dd9 100644 --- a/cli/src/msg/envelope.rs +++ b/cli/src/msg/envelope.rs @@ -1,13 +1,30 @@ -use std::{any, fmt}; +use himalaya_lib::msg::{Envelope, Flag}; -use crate::output::PrintTable; +use crate::ui::{Cell, Row, Table}; -pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any { - fn as_any(&self) -> &dyn any::Any; -} +impl Table for Envelope { + fn head() -> Row { + Row::new() + .cell(Cell::new("ID").bold().underline().white()) + .cell(Cell::new("FLAGS").bold().underline().white()) + .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) + .cell(Cell::new("SENDER").bold().underline().white()) + .cell(Cell::new("DATE").bold().underline().white()) + } -impl Envelopes for T { - fn as_any(&self) -> &dyn any::Any { - self + fn row(&self) -> Row { + let id = self.id.to_string(); + let flags = self.flags.to_symbols_string(); + let unseen = !self.flags.contains(&Flag::Seen); + let subject = &self.subject; + let sender = &self.sender; + let date = self.date.as_deref().unwrap_or_default(); + + Row::new() + .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(flags).bold_if(unseen).white()) + .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) + .cell(Cell::new(sender).bold_if(unseen).blue()) + .cell(Cell::new(date).bold_if(unseen).yellow()) } } diff --git a/cli/src/msg/envelopes.rs b/cli/src/msg/envelopes.rs new file mode 100644 index 0000000..0a524a9 --- /dev/null +++ b/cli/src/msg/envelopes.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use himalaya_lib::msg::Envelopes; + +use crate::{ + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::Table, +}; + +impl PrintTable for Envelopes { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 0c2e0c0..3011427 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -126,7 +126,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = imap.get_envelopes(mbox, page_size, page)?; trace!("envelopes: {:?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -310,7 +310,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?; trace!("messages: {:#?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -335,7 +335,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?; trace!("envelopes: {:#?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fecd793..b28fb40 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" imap-backend = ["imap", "imap-proto"] maildir-backend = ["maildir", "md5"] notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend", "notmuch-backend"] +default = ["imap-backend", "maildir-backend"] [dependencies] lettre = { version = "0.10.0-rc.6", features = ["serde"] } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 87eb169..d8398a8 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,3 +2,4 @@ mod process; pub mod account; pub mod mbox; +pub mod msg; diff --git a/lib/src/msg/envelope.rs b/lib/src/msg/envelope.rs new file mode 100644 index 0000000..cc04ee3 --- /dev/null +++ b/lib/src/msg/envelope.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +use super::Flags; + +/// Represents the message envelope. The envelope is just a message +/// subset, and is mostly used for listings. +#[derive(Debug, Default, Clone, Serialize)] +pub struct Envelope { + /// Represents the message identifier. + pub id: String, + /// Represents the internal message identifier. + pub internal_id: String, + /// Represents the message flags. + pub flags: Flags, + /// Represents the subject of the message. + pub subject: String, + /// Represents the first sender of the message. + pub sender: String, + /// Represents the internal date of the message. + pub date: Option, +} diff --git a/lib/src/msg/envelopes.rs b/lib/src/msg/envelopes.rs new file mode 100644 index 0000000..9cf85c9 --- /dev/null +++ b/lib/src/msg/envelopes.rs @@ -0,0 +1,25 @@ +use serde::Serialize; +use std::ops; + +use super::Envelope; + +/// Represents the list of envelopes. +#[derive(Debug, Default, Serialize)] +pub struct Envelopes { + #[serde(rename = "response")] + pub envelopes: Vec, +} + +impl ops::Deref for Envelopes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.envelopes + } +} + +impl ops::DerefMut for Envelopes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.envelopes + } +} diff --git a/lib/src/msg/flag.rs b/lib/src/msg/flag.rs new file mode 100644 index 0000000..1d37e18 --- /dev/null +++ b/lib/src/msg/flag.rs @@ -0,0 +1,27 @@ +use serde::Serialize; + +/// Represents the flag variants. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum Flag { + Seen, + Answered, + Flagged, + Deleted, + Draft, + Recent, + Custom(String), +} + +impl From<&str> for Flag { + fn from(flag_str: &str) -> Self { + match flag_str { + "seen" => Flag::Seen, + "answered" | "replied" => Flag::Answered, + "flagged" => Flag::Flagged, + "deleted" | "trashed" => Flag::Deleted, + "draft" => Flag::Draft, + "recent" => Flag::Recent, + flag => Flag::Custom(flag.into()), + } + } +} diff --git a/lib/src/msg/flags.rs b/lib/src/msg/flags.rs new file mode 100644 index 0000000..50db6ce --- /dev/null +++ b/lib/src/msg/flags.rs @@ -0,0 +1,89 @@ +use std::{fmt, ops}; + +use serde::Serialize; + +use super::Flag; + +/// Represents the list of flags. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub struct Flags(pub Vec); + +impl Flags { + /// Builds a symbols string. + pub fn to_symbols_string(&self) -> String { + let mut flags = String::new(); + flags.push_str(if self.contains(&Flag::Seen) { + " " + } else { + "✷" + }); + flags.push_str(if self.contains(&Flag::Answered) { + "↵" + } else { + " " + }); + flags.push_str(if self.contains(&Flag::Flagged) { + "⚑" + } else { + " " + }); + flags + } +} + +impl ops::Deref for Flags { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for Flags { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Display for Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut glue = ""; + + for flag in &self.0 { + write!(f, "{}", glue)?; + match flag { + Flag::Seen => write!(f, "\\Seen")?, + Flag::Answered => write!(f, "\\Answered")?, + Flag::Flagged => write!(f, "\\Flagged")?, + Flag::Deleted => write!(f, "\\Deleted")?, + Flag::Draft => write!(f, "\\Draft")?, + Flag::Recent => write!(f, "\\Recent")?, + Flag::Custom(flag) => write!(f, "{}", flag)?, + } + glue = " "; + } + + Ok(()) + } +} + +impl From<&str> for Flags { + fn from(flags: &str) -> Self { + Flags( + flags + .split_whitespace() + .map(|flag| flag.trim().into()) + .collect(), + ) + } +} + +impl FromIterator for Flags { + fn from_iter>(iter: T) -> Self { + let mut flags = Flags::default(); + for flag in iter { + flags.push(flag); + } + flags + } +} diff --git a/lib/src/msg/mod.rs b/lib/src/msg/mod.rs new file mode 100644 index 0000000..d3bec40 --- /dev/null +++ b/lib/src/msg/mod.rs @@ -0,0 +1,11 @@ +mod flag; +pub use flag::*; + +mod flags; +pub use flags::*; + +mod envelope; +pub use envelope::*; + +mod envelopes; +pub use envelopes::*; From 3c5379b24db6d32ca58c58cf7149d75c63959619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 5 Jun 2022 17:14:57 +0200 Subject: [PATCH 18/24] fix tests --- cli/src/mbox/mbox_handlers.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index b22a943..aed639f 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -33,12 +33,15 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( #[cfg(test)] mod tests { - use himalaya_lib::mbox::{Mbox, Mboxes}; + use himalaya_lib::{ + mbox::{Mbox, Mboxes}, + msg::Envelopes, + }; use std::{fmt::Debug, io}; use termcolor::ColorSpec; use crate::{ - msg::{Envelopes, Msg}, + msg::Msg, output::{Print, PrintTable, WriteColor}, }; @@ -133,7 +136,7 @@ mod tests { fn del_mbox(&mut self, _: &str) -> Result<()> { unimplemented!(); } - fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result> { + fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result { unimplemented!() } fn search_envelopes( @@ -143,7 +146,7 @@ mod tests { _: &str, _: usize, _: usize, - ) -> Result> { + ) -> Result { unimplemented!() } fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result { From 3b2991ae56c89d196adeb5e714ab4bd6287a3a64 Mon Sep 17 00:00:00 2001 From: TornaxO7 <50843046+TornaxO7@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:51:34 +0200 Subject: [PATCH 19/24] bumping lettre to 0.10.0-rc.7 (#391) * bumping lettre to 0.10.0-rc.7 * executed `cargo build` --- Cargo.lock | 23 ++++++++++++++++++++--- cli/Cargo.toml | 2 +- lib/Cargo.toml | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2967628..7ccbc8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,12 @@ dependencies = [ "memchr 2.4.1", ] +[[package]] +name = "email_address" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8684b7c9cb4857dfa1e5b9629ef584ba618c9b93bae60f58cb23f4f271d0468e" + [[package]] name = "encoding_rs" version = "0.8.30" @@ -615,12 +621,13 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.0-rc.6" +version = "0.10.0-rc.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f6c70001f7ee6c93b6687a06607c7a38f9a7ae460139a496c23da21e95bc289" +checksum = "0f7e87d9d44162eea7abd87b1a7540fcb10d5e58e8bb4f173178f3dc6e453944" dependencies = [ "base64", "email-encoding", + "email_address", "fastrand", "futures-util", "hostname", @@ -631,8 +638,8 @@ dependencies = [ "nom 7.1.1", "once_cell", "quoted_printable", - "regex", "serde", + "socket2", ] [[package]] @@ -1283,6 +1290,16 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "string_cache" version = "0.8.3" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 50fea80..25b638c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,7 +33,7 @@ env_logger = "0.8.3" erased-serde = "0.3.18" himalaya-lib = { path = "../lib" } html-escape = "0.2.9" -lettre = { version = "0.10.0-rc.1", features = ["serde"] } +lettre = { version = "0.10.0-rc.7", features = ["serde"] } log = "0.4.14" mailparse = "0.13.6" native-tls = "0.2.8" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b28fb40..68f0916 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,7 +10,7 @@ notmuch-backend = ["notmuch", "maildir-backend"] default = ["imap-backend", "maildir-backend"] [dependencies] -lettre = { version = "0.10.0-rc.6", features = ["serde"] } +lettre = { version = "0.10.0-rc.7", features = ["serde"] } log = "0.4.14" mailparse = "0.13.6" serde = { version = "1.0.118", features = ["derive"] } From a5c4fdaac6864c1adfd311acd171699fe5517eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 26 Jun 2022 21:45:25 +0200 Subject: [PATCH 20/24] move backend to lib folder (#340) --- Cargo.lock | 9 + Cargo.toml | 2 +- cli/src/backends/maildir/maildir_flag.rs | 13 - cli/src/backends/maildir/maildir_flags.rs | 7 - cli/src/{backends => }/imap/imap_args.rs | 0 cli/src/{backends => }/imap/imap_envelopes.rs | 7 +- cli/src/{backends => }/imap/imap_handlers.rs | 9 +- cli/src/lib.rs | 91 +----- cli/src/main.rs | 21 +- cli/src/mbox/mbox_handlers.rs | 47 ++- cli/src/msg/flag_handlers.rs | 3 +- cli/src/msg/msg_args.rs | 10 +- cli/src/msg/msg_handlers.rs | 54 ++-- cli/src/msg/msg_utils.rs | 15 - cli/src/msg/tpl_args.rs | 41 +-- cli/src/msg/tpl_handlers.rs | 13 +- cli/src/smtp/smtp_service.rs | 4 +- cli/src/ui/editor.rs | 109 ++++++- lib/Cargo.toml | 9 + lib/src/account/account_config.rs | 56 ++-- .../backends => lib/src/backend}/backend.rs | 44 ++- .../backends => lib/src/backend}/id_mapper.rs | 83 ++--- lib/src/backend/imap/error.rs | 86 +++++ .../src/backend}/imap/imap_backend.rs | 126 ++++---- .../src/backend}/imap/imap_envelope.rs | 43 ++- lib/src/backend/imap/imap_envelopes.rs | 18 ++ .../src/backend}/imap/imap_flag.rs | 2 +- .../src/backend}/imap/imap_flags.rs | 7 +- .../src/backend}/imap/msg_sort_criterion.rs | 5 +- lib/src/backend/maildir/error.rs | 49 +++ .../src/backend}/maildir/maildir_backend.rs | 293 +++++------------- .../src/backend}/maildir/maildir_envelope.rs | 16 +- .../src/backend}/maildir/maildir_envelopes.rs | 18 +- lib/src/backend/maildir/maildir_flag.rs | 24 ++ lib/src/backend/maildir/maildir_flags.rs | 11 + lib/src/backend/mod.rs | 73 +++++ lib/src/backend/notmuch/error.rs | 49 +++ .../src/backend}/notmuch/notmuch_backend.rs | 194 +++--------- .../src/backend}/notmuch/notmuch_envelope.rs | 27 +- .../src/backend}/notmuch/notmuch_envelopes.rs | 6 +- lib/src/lib.rs | 1 + .../msg/addr_entity.rs => lib/src/msg/addr.rs | 9 +- lib/src/msg/error.rs | 56 ++++ lib/src/msg/flags.rs | 3 +- lib/src/msg/mod.rs | 18 ++ .../msg/msg_entity.rs => lib/src/msg/msg.rs | 163 ++-------- lib/src/msg/msg_utils.rs | 24 ++ .../parts_entity.rs => lib/src/msg/parts.rs | 31 +- lib/src/msg/tpl.rs | 15 + lib/src/process.rs | 29 +- .../emails/alice-to-patrick-encrypted.eml | 0 .../tests}/emails/alice-to-patrick.eml | 0 {tests => lib/tests}/keys/alice.asc | 0 {tests => lib/tests}/keys/alice.pub.asc | 0 {tests => lib/tests}/keys/patrick.asc | 0 {tests => lib/tests}/keys/patrick.pub.asc | 0 {tests => lib/tests}/test_imap_backend.rs | 11 +- {tests => lib/tests}/test_maildir_backend.rs | 41 ++- {tests => lib/tests}/test_notmuch_backend.rs | 44 ++- 59 files changed, 1125 insertions(+), 1014 deletions(-) delete mode 100644 cli/src/backends/maildir/maildir_flag.rs delete mode 100644 cli/src/backends/maildir/maildir_flags.rs rename cli/src/{backends => }/imap/imap_args.rs (100%) rename cli/src/{backends => }/imap/imap_envelopes.rs (82%) rename cli/src/{backends => }/imap/imap_handlers.rs (58%) delete mode 100644 cli/src/msg/msg_utils.rs rename {cli/src/backends => lib/src/backend}/backend.rs (61%) rename {cli/src/backends => lib/src/backend}/id_mapper.rs (56%) create mode 100644 lib/src/backend/imap/error.rs rename {cli/src/backends => lib/src/backend}/imap/imap_backend.rs (78%) rename {cli/src/backends => lib/src/backend}/imap/imap_envelope.rs (53%) create mode 100644 lib/src/backend/imap/imap_envelopes.rs rename {cli/src/backends => lib/src/backend}/imap/imap_flag.rs (95%) rename {cli/src/backends => lib/src/backend}/imap/imap_flags.rs (91%) rename {cli/src/backends => lib/src/backend}/imap/msg_sort_criterion.rs (95%) create mode 100644 lib/src/backend/maildir/error.rs rename {cli/src/backends => lib/src/backend}/maildir/maildir_backend.rs (50%) rename {cli/src/backends => lib/src/backend}/maildir/maildir_envelope.rs (83%) rename {cli/src/backends => lib/src/backend}/maildir/maildir_envelopes.rs (52%) create mode 100644 lib/src/backend/maildir/maildir_flag.rs create mode 100644 lib/src/backend/maildir/maildir_flags.rs create mode 100644 lib/src/backend/mod.rs create mode 100644 lib/src/backend/notmuch/error.rs rename {cli/src/backends => lib/src/backend}/notmuch/notmuch_backend.rs (61%) rename {cli/src/backends => lib/src/backend}/notmuch/notmuch_envelope.rs (69%) rename {cli/src/backends => lib/src/backend}/notmuch/notmuch_envelopes.rs (66%) rename cli/src/msg/addr_entity.rs => lib/src/msg/addr.rs (91%) create mode 100644 lib/src/msg/error.rs rename cli/src/msg/msg_entity.rs => lib/src/msg/msg.rs (84%) create mode 100644 lib/src/msg/msg_utils.rs rename cli/src/msg/parts_entity.rs => lib/src/msg/parts.rs (82%) create mode 100644 lib/src/msg/tpl.rs rename {tests => lib/tests}/emails/alice-to-patrick-encrypted.eml (100%) rename {tests => lib/tests}/emails/alice-to-patrick.eml (100%) rename {tests => lib/tests}/keys/alice.asc (100%) rename {tests => lib/tests}/keys/alice.pub.asc (100%) rename {tests => lib/tests}/keys/patrick.asc (100%) rename {tests => lib/tests}/keys/patrick.pub.asc (100%) rename {tests => lib/tests}/test_imap_backend.rs (84%) rename {tests => lib/tests}/test_maildir_backend.rs (66%) rename {tests => lib/tests}/test_notmuch_backend.rs (64%) diff --git a/Cargo.lock b/Cargo.lock index 7ccbc8e..adda362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,6 +486,10 @@ dependencies = [ name = "himalaya-lib" version = "0.1.0" dependencies = [ + "ammonia", + "chrono", + "convert_case", + "html-escape", "imap", "imap-proto", "lettre", @@ -493,11 +497,16 @@ dependencies = [ "maildir", "mailparse", "md5", + "native-tls", "notmuch", + "regex", + "rfc2047-decoder", "serde", "shellexpand", "thiserror", "toml", + "tree_magic", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2f84d20..de6b2bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["lib", "cli"] \ No newline at end of file +members = ["lib", "cli"] diff --git a/cli/src/backends/maildir/maildir_flag.rs b/cli/src/backends/maildir/maildir_flag.rs deleted file mode 100644 index b868e8a..0000000 --- a/cli/src/backends/maildir/maildir_flag.rs +++ /dev/null @@ -1,13 +0,0 @@ -use himalaya_lib::msg::Flag; - -pub fn from_char(c: char) -> Flag { - match c { - 'R' => Flag::Answered, - 'S' => Flag::Seen, - 'T' => Flag::Deleted, - 'D' => Flag::Draft, - 'F' => Flag::Flagged, - 'P' => Flag::Custom(String::from("Passed")), - flag => Flag::Custom(flag.to_string()), - } -} diff --git a/cli/src/backends/maildir/maildir_flags.rs b/cli/src/backends/maildir/maildir_flags.rs deleted file mode 100644 index 538c90a..0000000 --- a/cli/src/backends/maildir/maildir_flags.rs +++ /dev/null @@ -1,7 +0,0 @@ -use himalaya_lib::msg::Flags; - -use super::maildir_flag; - -pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { - entry.flags().chars().map(maildir_flag::from_char).collect() -} diff --git a/cli/src/backends/imap/imap_args.rs b/cli/src/imap/imap_args.rs similarity index 100% rename from cli/src/backends/imap/imap_args.rs rename to cli/src/imap/imap_args.rs diff --git a/cli/src/backends/imap/imap_envelopes.rs b/cli/src/imap/imap_envelopes.rs similarity index 82% rename from cli/src/backends/imap/imap_envelopes.rs rename to cli/src/imap/imap_envelopes.rs index d0e6591..8095b9b 100644 --- a/cli/src/backends/imap/imap_envelopes.rs +++ b/cli/src/imap/imap_envelopes.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; -use himalaya_lib::msg::Envelopes; - -use crate::backends::{imap::from_imap_fetch, ImapFetch}; +use himalaya_lib::{ + backend::{from_imap_fetch, ImapFetch}, + msg::Envelopes, +}; /// Represents the list of raw envelopes returned by the `imap` crate. pub type ImapFetches = imap::types::ZeroCopy>; diff --git a/cli/src/backends/imap/imap_handlers.rs b/cli/src/imap/imap_handlers.rs similarity index 58% rename from cli/src/backends/imap/imap_handlers.rs rename to cli/src/imap/imap_handlers.rs index 3805909..f5ab439 100644 --- a/cli/src/backends/imap/imap_handlers.rs +++ b/cli/src/imap/imap_handlers.rs @@ -2,14 +2,13 @@ //! //! This module gathers all IMAP handlers triggered by the CLI. -use anyhow::Result; - -use crate::backends::ImapBackend; +use anyhow::{Context, Result}; +use himalaya_lib::backend::ImapBackend; pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { - imap.notify(keepalive, mbox) + imap.notify(keepalive, mbox).context("cannot imap notify") } pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { - imap.watch(keepalive, mbox) + imap.watch(keepalive, mbox).context("cannot imap watch") } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 634a52f..9fefb19 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,6 +9,15 @@ pub mod mbox { pub mod mbox_handlers; } +#[cfg(feature = "imap-backend")] +pub mod imap { + pub mod imap_args; + pub mod imap_handlers; + + pub mod imap_envelopes; + pub use imap_envelopes::*; +} + pub mod msg { pub mod envelope; pub use envelope::*; @@ -19,95 +28,13 @@ pub mod msg { pub mod msg_args; pub mod msg_handlers; - pub mod msg_utils; pub mod flag_args; pub mod flag_handlers; pub mod tpl_args; - pub use tpl_args::TplOverride; pub mod tpl_handlers; - - pub mod msg_entity; - pub use msg_entity::*; - - pub mod parts_entity; - pub use parts_entity::*; - - pub mod addr_entity; - pub use addr_entity::*; -} - -pub mod backends { - pub mod backend; - pub use backend::*; - - pub mod id_mapper; - pub use id_mapper::*; - - #[cfg(feature = "imap-backend")] - pub mod imap { - pub mod imap_args; - - pub mod imap_backend; - pub use imap_backend::*; - - pub mod imap_handlers; - - pub mod imap_envelopes; - pub use imap_envelopes::*; - - pub mod imap_envelope; - pub use imap_envelope::*; - - pub mod imap_flags; - pub use imap_flags::*; - - pub mod imap_flag; - pub use imap_flag::*; - - pub mod msg_sort_criterion; - } - - #[cfg(feature = "imap-backend")] - pub use self::imap::*; - - #[cfg(feature = "maildir-backend")] - pub mod maildir { - pub mod maildir_backend; - pub use maildir_backend::*; - - pub mod maildir_envelopes; - pub use maildir_envelopes::*; - - pub mod maildir_envelope; - pub use maildir_envelope::*; - - pub mod maildir_flags; - pub use maildir_flags::*; - - pub mod maildir_flag; - pub use maildir_flag::*; - } - - #[cfg(feature = "maildir-backend")] - pub use self::maildir::*; - - #[cfg(feature = "notmuch-backend")] - pub mod notmuch { - pub mod notmuch_backend; - pub use notmuch_backend::*; - - pub mod notmuch_envelopes; - pub use notmuch_envelopes::*; - - pub mod notmuch_envelope; - pub use notmuch_envelope::*; - } - - #[cfg(feature = "notmuch-backend")] - pub use self::notmuch::*; } pub mod smtp { diff --git a/cli/src/main.rs b/cli/src/main.rs index 10c7382..b3f834c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,12 +1,12 @@ -use anyhow::Result; -use himalaya_lib::account::{ - AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER, +use anyhow::{Context, Result}; +use himalaya_lib::{ + account::{AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER}, + backend::Backend, }; use std::{convert::TryFrom, env}; use url::Url; use himalaya::{ - backends::Backend, compl::{compl_args, compl_handlers}, config::{account_args, account_handlers, config_args}, mbox::{mbox_args, mbox_handlers}, @@ -16,15 +16,16 @@ use himalaya::{ }; #[cfg(feature = "imap-backend")] -use himalaya::backends::{imap_args, imap_handlers, ImapBackend}; +use himalaya::imap::{imap_args, imap_handlers}; + +#[cfg(feature = "imap-backend")] +use himalaya_lib::backend::ImapBackend; #[cfg(feature = "maildir-backend")] -use himalaya::backends::MaildirBackend; +use himalaya_lib::backend::MaildirBackend; #[cfg(feature = "notmuch-backend")] -use himalaya::backends::NotmuchBackend; -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::account::MaildirBackendConfig; +use himalaya_lib::{account::MaildirBackendConfig, backend::NotmuchBackend}; fn create_app<'a>() -> clap::App<'a, 'a> { let app = clap::App::new(env!("CARGO_PKG_NAME")) @@ -346,5 +347,5 @@ fn main() -> Result<()> { _ => (), } - backend.disconnect() + backend.disconnect().context("cannot disconnect") } diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index aed639f..c4d50ac 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -3,13 +3,10 @@ //! This module gathers all mailbox actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::account::AccountConfig; +use himalaya_lib::{account::AccountConfig, backend::Backend}; use log::{info, trace}; -use crate::{ - backends::Backend, - output::{PrintTableOpts, PrinterService}, -}; +use crate::output::{PrintTableOpts, PrinterService}; /// Lists all mailboxes. pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( @@ -34,16 +31,14 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( #[cfg(test)] mod tests { use himalaya_lib::{ + backend::{backend, Backend}, mbox::{Mbox, Mboxes}, - msg::Envelopes, + msg::{Envelopes, Msg}, }; use std::{fmt::Debug, io}; use termcolor::ColorSpec; - use crate::{ - msg::Msg, - output::{Print, PrintTable, WriteColor}, - }; + use crate::output::{Print, PrintTable, WriteColor}; use super::*; @@ -93,17 +88,17 @@ mod tests { &mut self, data: Box, opts: PrintTableOpts, - ) -> Result<()> { + ) -> anyhow::Result<()> { data.print_table(&mut self.writer, opts)?; Ok(()) } - fn print_str(&mut self, _data: T) -> Result<()> { + fn print_str(&mut self, _data: T) -> anyhow::Result<()> { unimplemented!() } fn print_struct( &mut self, _data: T, - ) -> Result<()> { + ) -> anyhow::Result<()> { unimplemented!() } fn is_json(&self) -> bool { @@ -114,10 +109,10 @@ mod tests { struct TestBackend; impl<'a> Backend<'a> for TestBackend { - fn add_mbox(&mut self, _: &str) -> Result<()> { + fn add_mbox(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn get_mboxes(&mut self) -> Result { + fn get_mboxes(&mut self) -> backend::Result { Ok(Mboxes { mboxes: vec![ Mbox { @@ -133,10 +128,10 @@ mod tests { ], }) } - fn del_mbox(&mut self, _: &str) -> Result<()> { + fn del_mbox(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result { + fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> backend::Result { unimplemented!() } fn search_envelopes( @@ -146,31 +141,31 @@ mod tests { _: &str, _: usize, _: usize, - ) -> Result { + ) -> backend::Result { unimplemented!() } - fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result { + fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result { unimplemented!() } - fn get_msg(&mut self, _: &str, _: &str) -> Result { + fn get_msg(&mut self, _: &str, _: &str) -> backend::Result { unimplemented!() } - fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn move_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn del_msg(&mut self, _: &str, _: &str) -> Result<()> { + fn del_msg(&mut self, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn add_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn set_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn del_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> { + fn del_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } } diff --git a/cli/src/msg/flag_handlers.rs b/cli/src/msg/flag_handlers.rs index 33ed696..686912e 100644 --- a/cli/src/msg/flag_handlers.rs +++ b/cli/src/msg/flag_handlers.rs @@ -3,8 +3,9 @@ //! This module gathers all flag actions triggered by the CLI. use anyhow::Result; +use himalaya_lib::backend::Backend; -use crate::{backends::Backend, output::PrinterService}; +use crate::output::PrinterService; /// Adds flags to all messages matching the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. diff --git a/cli/src/msg/msg_args.rs b/cli/src/msg/msg_args.rs index 169b05a..b5b7862 100644 --- a/cli/src/msg/msg_args.rs +++ b/cli/src/msg/msg_args.rs @@ -4,11 +4,15 @@ use anyhow::Result; use clap::{self, App, Arg, ArgMatches, SubCommand}; +use himalaya_lib::msg::TplOverride; use log::{debug, info, trace}; use crate::{ mbox::mbox_args, - msg::{flag_args, msg_args, tpl_args}, + msg::{ + flag_args, msg_args, + tpl_args::{self, from_args}, + }, ui::table_arg, }; @@ -42,7 +46,7 @@ pub enum Cmd<'a> { Search(Query, MaxTableWidth, Option, Page), Sort(Criteria, Query, MaxTableWidth, Option, Page), Send(RawMsg<'a>), - Write(tpl_args::TplOverride<'a>, AttachmentPaths<'a>, Encrypt), + Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt), Flag(Option>), Tpl(Option>), @@ -261,7 +265,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("attachments paths: {:?}", attachment_paths); let encrypt = m.is_present("encrypt"); debug!("encrypt: {}", encrypt); - let tpl = tpl_args::TplOverride::from(m); + let tpl = from_args(m); return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt))); } diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 3011427..b18c2c4 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -4,7 +4,11 @@ use anyhow::{Context, Result}; use atty::Stream; -use himalaya_lib::account::{AccountConfig, DEFAULT_SENT_FOLDER}; +use himalaya_lib::{ + account::{AccountConfig, DEFAULT_SENT_FOLDER}, + backend::Backend, + msg::{Msg, Part, Parts, TextPlainPart, TplOverride}, +}; use log::{debug, info, trace}; use mailparse::addrparse; use std::{ @@ -15,14 +19,11 @@ use std::{ use url::Url; use crate::{ - backends::Backend, - msg::{Msg, Part, Parts, TextPlainPart}, output::{PrintTableOpts, PrinterService}, smtp::SmtpService, + ui::editor, }; -use super::tpl_args; - /// Downloads all message attachments to the user account downloads directory. pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, @@ -96,18 +97,12 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - backend + let msg = backend .get_msg(mbox, seq)? .into_forward(config)? .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor( - tpl_args::TplOverride::default(), - config, - printer, - backend, - smtp, - )?; + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; Ok(()) } @@ -191,13 +186,7 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( }; trace!("message: {:?}", msg); - msg.edit_with_editor( - tpl_args::TplOverride::default(), - config, - printer, - backend, - smtp, - )?; + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; Ok(()) } @@ -249,19 +238,14 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - backend + let msg = backend .get_msg(mbox, seq)? .into_reply(all, config)? .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor( - tpl_args::TplOverride::default(), - config, - printer, - backend, - smtp, - )? - .add_flags(mbox, seq, "replied") + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)? + .add_flags(mbox, seq, "replied")?; + Ok(()) } /// Saves a raw message to the targetted mailbox. @@ -384,7 +368,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( /// Compose a new message. pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( - tpl: tpl_args::TplOverride, + tpl: TplOverride, attachments_paths: Vec<&str>, encrypt: bool, config: &AccountConfig, @@ -392,9 +376,9 @@ pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( backend: Box<&'a mut B>, smtp: &mut S, ) -> Result<()> { - Msg::default() + let msg = Msg::default() .add_attachments(attachments_paths)? - .encrypt(encrypt) - .edit_with_editor(tpl, config, printer, backend, smtp)?; + .encrypt(encrypt); + editor::edit_msg_with_editor(msg, tpl, config, printer, backend, smtp)?; Ok(()) } diff --git a/cli/src/msg/msg_utils.rs b/cli/src/msg/msg_utils.rs deleted file mode 100644 index b5ccacf..0000000 --- a/cli/src/msg/msg_utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use anyhow::{Context, Result}; -use log::{debug, trace}; -use std::{env, fs, path::PathBuf}; - -pub fn local_draft_path() -> PathBuf { - let path = env::temp_dir().join("himalaya-draft.eml"); - trace!("local draft path: {:?}", path); - path -} - -pub fn remove_local_draft() -> Result<()> { - let path = local_draft_path(); - debug!("remove draft path at {:?}", path); - fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path)) -} diff --git a/cli/src/msg/tpl_args.rs b/cli/src/msg/tpl_args.rs index 90a9356..5436ad0 100644 --- a/cli/src/msg/tpl_args.rs +++ b/cli/src/msg/tpl_args.rs @@ -4,6 +4,7 @@ use anyhow::Result; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; +use himalaya_lib::msg::TplOverride; use log::{debug, info, trace}; use crate::msg::msg_args; @@ -13,30 +14,16 @@ type ReplyAll = bool; type AttachmentPaths<'a> = Vec<&'a str>; type Tpl<'a> = &'a str; -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct TplOverride<'a> { - pub subject: Option<&'a str>, - pub from: Option>, - pub to: Option>, - pub cc: Option>, - pub bcc: Option>, - pub headers: Option>, - pub body: Option<&'a str>, - pub sig: Option<&'a str>, -} - -impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> { - fn from(matches: &'a ArgMatches<'a>) -> Self { - Self { - subject: matches.value_of("subject"), - from: matches.values_of("from").map(|v| v.collect()), - to: matches.values_of("to").map(|v| v.collect()), - cc: matches.values_of("cc").map(|v| v.collect()), - bcc: matches.values_of("bcc").map(|v| v.collect()), - headers: matches.values_of("headers").map(|v| v.collect()), - body: matches.value_of("body"), - sig: matches.value_of("signature"), - } +pub fn from_args<'a>(matches: &'a ArgMatches<'a>) -> TplOverride { + TplOverride { + subject: matches.value_of("subject"), + from: matches.values_of("from").map(|v| v.collect()), + to: matches.values_of("to").map(|v| v.collect()), + cc: matches.values_of("cc").map(|v| v.collect()), + bcc: matches.values_of("bcc").map(|v| v.collect()), + headers: matches.values_of("headers").map(|v| v.collect()), + body: matches.value_of("body"), + sig: matches.value_of("signature"), } } @@ -56,7 +43,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { if let Some(m) = m.subcommand_matches("new") { info!("new subcommand matched"); - let tpl = TplOverride::from(m); + let tpl = from_args(m); trace!("template override: {:?}", tpl); return Ok(Some(Cmd::New(tpl))); } @@ -67,7 +54,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("sequence: {}", seq); let all = m.is_present("reply-all"); debug!("reply all: {}", all); - let tpl = TplOverride::from(m); + let tpl = from_args(m); trace!("template override: {:?}", tpl); return Ok(Some(Cmd::Reply(seq, all, tpl))); } @@ -76,7 +63,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { info!("forward subcommand matched"); let seq = m.value_of("seq").unwrap(); debug!("sequence: {}", seq); - let tpl = TplOverride::from(m); + let tpl = from_args(m); trace!("template args: {:?}", tpl); return Ok(Some(Cmd::Forward(seq, tpl))); } diff --git a/cli/src/msg/tpl_handlers.rs b/cli/src/msg/tpl_handlers.rs index 16953b1..8d8ce14 100644 --- a/cli/src/msg/tpl_handlers.rs +++ b/cli/src/msg/tpl_handlers.rs @@ -4,15 +4,14 @@ use anyhow::Result; use atty::Stream; -use himalaya_lib::account::AccountConfig; +use himalaya_lib::{ + account::AccountConfig, + backend::Backend, + msg::{Msg, TplOverride}, +}; use std::io::{self, BufRead}; -use crate::{ - backends::Backend, - msg::{Msg, TplOverride}, - output::PrinterService, - smtp::SmtpService, -}; +use crate::{output::PrinterService, smtp::SmtpService}; /// Generate a new message template. pub fn new<'a, P: PrinterService>( diff --git a/cli/src/smtp/smtp_service.rs b/cli/src/smtp/smtp_service.rs index e417afb..c5c788d 100644 --- a/cli/src/smtp/smtp_service.rs +++ b/cli/src/smtp/smtp_service.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use himalaya_lib::account::AccountConfig; +use himalaya_lib::{account::AccountConfig, msg::Msg}; use lettre::{ self, transport::smtp::{ @@ -10,7 +10,7 @@ use lettre::{ }; use std::convert::TryInto; -use crate::{msg::Msg, output::pipe_cmd}; +use crate::output::pipe_cmd; pub trait SmtpService { fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; diff --git a/cli/src/ui/editor.rs b/cli/src/ui/editor.rs index 9e7a5c2..f940cef 100644 --- a/cli/src/ui/editor.rs +++ b/cli/src/ui/editor.rs @@ -1,11 +1,20 @@ use anyhow::{Context, Result}; -use log::debug; +use himalaya_lib::{ + account::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER}, + backend::Backend, + msg::{local_draft_path, remove_local_draft, Msg, TplOverride}, +}; +use log::{debug, info}; use std::{env, fs, process::Command}; -use crate::msg::msg_utils; +use crate::{ + output::PrinterService, + smtp::SmtpService, + ui::choice::{self, PostEditChoice, PreEditChoice}, +}; pub fn open_with_tpl(tpl: String) -> Result { - let path = msg_utils::local_draft_path(); + let path = local_draft_path(); debug!("create draft"); fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?; @@ -24,8 +33,100 @@ pub fn open_with_tpl(tpl: String) -> Result { } pub fn open_with_draft() -> Result { - let path = msg_utils::local_draft_path(); + let path = local_draft_path(); let tpl = fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; open_with_tpl(tpl) } + +fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &AccountConfig) -> Result { + let tpl = msg.to_tpl(tpl, account)?; + let tpl = open_with_tpl(tpl)?; + Msg::from_tpl(&tpl).context("cannot parse message from template") +} + +pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( + mut msg: Msg, + tpl: TplOverride, + account: &AccountConfig, + printer: &mut P, + backend: Box<&'a mut B>, + smtp: &mut S, +) -> Result> { + info!("start editing with editor"); + + let draft = local_draft_path(); + if draft.exists() { + loop { + match choice::pre_edit() { + Ok(choice) => match choice { + PreEditChoice::Edit => { + let tpl = open_with_draft()?; + msg.merge_with(Msg::from_tpl(&tpl)?); + break; + } + PreEditChoice::Discard => { + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + break; + } + PreEditChoice::Quit => return Ok(backend), + }, + Err(err) => { + println!("{}", err); + continue; + } + } + } + } else { + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + } + + loop { + match choice::post_edit() { + Ok(PostEditChoice::Send) => { + printer.print_str("Sending message…")?; + let sent_msg = smtp.send(account, &msg)?; + let sent_folder = account + .mailboxes + .get("sent") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_SENT_FOLDER); + printer.print_str(format!("Adding message to the {:?} folder…", sent_folder))?; + backend.add_msg(&sent_folder, &sent_msg, "seen")?; + remove_local_draft()?; + printer.print_struct("Done!")?; + break; + } + Ok(PostEditChoice::Edit) => { + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + continue; + } + Ok(PostEditChoice::LocalDraft) => { + printer.print_struct("Message successfully saved locally")?; + break; + } + Ok(PostEditChoice::RemoteDraft) => { + let tpl = msg.to_tpl(TplOverride::default(), account)?; + let draft_folder = account + .mailboxes + .get("draft") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_DRAFT_FOLDER); + backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; + remove_local_draft()?; + printer.print_struct(format!("Message successfully saved to {}", draft_folder))?; + break; + } + Ok(PostEditChoice::Discard) => { + remove_local_draft()?; + break; + } + Err(err) => { + println!("{}", err); + continue; + } + } + } + + Ok(backend) +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 68f0916..2549a13 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,13 +10,22 @@ notmuch-backend = ["notmuch", "maildir-backend"] default = ["imap-backend", "maildir-backend"] [dependencies] +ammonia = "3.1.2" +chrono = "0.4.19" +convert_case = "0.5.0" +html-escape = "0.2.9" lettre = { version = "0.10.0-rc.7", features = ["serde"] } log = "0.4.14" mailparse = "0.13.6" +native-tls = "0.2.8" +regex = "1.5.4" +rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } shellexpand = "2.1.0" thiserror = "1.0.31" toml = "0.5.8" +tree_magic = "0.2.3" +uuid = { version = "0.8", features = ["v4"] } # [optional] imap = { version = "=3.0.0-alpha.4", optional = true } diff --git a/lib/src/account/account_config.rs b/lib/src/account/account_config.rs index 5e7b951..7229bb9 100644 --- a/lib/src/account/account_config.rs +++ b/lib/src/account/account_config.rs @@ -5,12 +5,19 @@ use shellexpand; use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf, result}; use thiserror::Error; -use crate::process::{run_cmd, ProcessError}; +use crate::process; use super::*; #[derive(Error, Debug)] -pub enum AccountError { +pub enum Error { + #[error("cannot run encrypt file command")] + RunEncryptFileCmdError(#[source] process::Error), + #[error("cannot find pgp encrypt file command from config")] + FindPgpEncryptFileCmdError, + #[error("cannot find pgp decrypt file command from config")] + FindPgpDecryptFileCmdError, + #[error("cannot find default account")] FindDefaultAccountError, #[error("cannot find account \"{0}\"")] @@ -25,11 +32,17 @@ pub enum AccountError { ParseDownloadFileNameError(PathBuf), #[error("cannot find password")] FindPasswordError, - #[error(transparent)] - RunCmdError(#[from] ProcessError), + #[error("cannot get smtp password")] + GetSmtpPasswdError(#[source] process::Error), + #[error("cannot get imap password")] + GetImapPasswdError(#[source] process::Error), + #[error("cannot decrypt pgp file")] + DecryptPgpFileError(#[source] process::Error), + #[error("cannot run notify command")] + RunNotifyCmdError(#[source] process::Error), } -type Result = result::Result; +pub type Result = result::Result; /// Represents the user account. #[derive(Debug, Default, Clone)] @@ -113,12 +126,12 @@ impl<'a> AccountConfig { } }) .map(|(name, account)| (name.to_owned(), account)) - .ok_or_else(|| AccountError::FindDefaultAccountError), + .ok_or_else(|| Error::FindDefaultAccountError), Some(name) => config .accounts .get(name) .map(|account| (name.to_owned(), account)) - .ok_or_else(|| AccountError::FindAccountError(name.to_owned())), + .ok_or_else(|| Error::FindAccountError(name.to_owned())), }?; let base_account = account.to_base(); @@ -253,17 +266,17 @@ impl<'a> AccountConfig { Ok(mailparse::addrparse(&addr)? .first() - .ok_or_else(|| AccountError::FindAccountAddressError(addr.into()))? + .ok_or_else(|| Error::FindAccountAddressError(addr.into()))? .clone()) } /// Builds the user account SMTP credentials. pub fn smtp_creds(&self) -> Result { - let passwd = run_cmd(&self.smtp_passwd_cmd)?; + let passwd = process::run_cmd(&self.smtp_passwd_cmd).map_err(Error::GetSmtpPasswdError)?; let passwd = passwd .lines() .next() - .ok_or_else(|| AccountError::FindPasswordError)?; + .ok_or_else(|| Error::FindPasswordError)?; Ok(SmtpCredentials::new( self.smtp_login.to_owned(), @@ -272,22 +285,22 @@ impl<'a> AccountConfig { } /// Encrypts a file. - pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result> { + pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result { if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() { let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path); - Ok(run_cmd(&encrypt_file_cmd).map(Some)?) + Ok(process::run_cmd(&encrypt_file_cmd).map_err(Error::RunEncryptFileCmdError)?) } else { - Ok(None) + Err(Error::FindPgpEncryptFileCmdError) } } /// Decrypts a file. - pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result> { + pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result { if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() { let decrypt_file_cmd = format!("{} {:?}", cmd, path); - Ok(run_cmd(&decrypt_file_cmd).map(Some)?) + Ok(process::run_cmd(&decrypt_file_cmd).map_err(Error::DecryptPgpFileError)?) } else { - Ok(None) + Err(Error::FindPgpDecryptFileCmdError) } } @@ -319,9 +332,7 @@ impl<'a> AccountConfig { .file_stem() .and_then(OsStr::to_str) .map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) - .ok_or_else(|| { - AccountError::ParseDownloadFileNameError(file_path.to_owned()) - })?, + .ok_or_else(|| Error::ParseDownloadFileNameError(file_path.to_owned()))?, )); } @@ -340,8 +351,7 @@ impl<'a> AccountConfig { .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) .unwrap_or(default_cmd); - debug!("run command: {}", cmd); - run_cmd(&cmd)?; + process::run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?; Ok(()) } @@ -391,11 +401,11 @@ pub struct ImapBackendConfig { impl ImapBackendConfig { /// Gets the IMAP password of the user account. pub fn imap_passwd(&self) -> Result { - let passwd = run_cmd(&self.imap_passwd_cmd)?; + let passwd = process::run_cmd(&self.imap_passwd_cmd).map_err(Error::GetImapPasswdError)?; let passwd = passwd .lines() .next() - .ok_or_else(|| AccountError::FindPasswordError)?; + .ok_or_else(|| Error::FindPasswordError)?; Ok(passwd.to_string()) } } diff --git a/cli/src/backends/backend.rs b/lib/src/backend/backend.rs similarity index 61% rename from cli/src/backends/backend.rs rename to lib/src/backend/backend.rs index 0162d05..2030347 100644 --- a/cli/src/backends/backend.rs +++ b/lib/src/backend/backend.rs @@ -3,10 +3,48 @@ //! This module exposes the backend trait, which can be used to create //! custom backend implementations. -use anyhow::Result; -use himalaya_lib::{mbox::Mboxes, msg::Envelopes}; +use std::result; -use crate::msg::Msg; +use thiserror::Error; + +use crate::{ + account, + mbox::Mboxes, + msg::{self, Envelopes, Msg}, +}; + +use super::id_mapper; + +#[cfg(feature = "maildir-backend")] +use super::MaildirError; + +#[cfg(feature = "notmuch-backend")] +use super::NotmuchError; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + ImapError(#[from] super::imap::Error), + + #[error(transparent)] + AccountError(#[from] account::Error), + + #[error(transparent)] + MsgError(#[from] msg::Error), + + #[error(transparent)] + IdMapperError(#[from] id_mapper::Error), + + #[cfg(feature = "maildir-backend")] + #[error(transparent)] + MaildirError(#[from] MaildirError), + + #[cfg(feature = "notmuch-backend")] + #[error(transparent)] + NotmuchError(#[from] NotmuchError), +} + +pub type Result = result::Result; pub trait Backend<'a> { fn connect(&mut self) -> Result<()> { diff --git a/cli/src/backends/id_mapper.rs b/lib/src/backend/id_mapper.rs similarity index 56% rename from cli/src/backends/id_mapper.rs rename to lib/src/backend/id_mapper.rs index 09a5422..d5ab5df 100644 --- a/cli/src/backends/id_mapper.rs +++ b/lib/src/backend/id_mapper.rs @@ -1,43 +1,56 @@ -use anyhow::{anyhow, Context, Result}; use std::{ - collections::HashMap, - fs::OpenOptions, - io::{BufRead, BufReader, Write}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, + collections, fs, + io::{self, prelude::*}, + ops, path, result, }; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("cannot parse id mapper cache line {0}")] + ParseLineError(String), + #[error("cannot find message id from short hash {0}")] + FindFromShortHashError(String), + #[error("the short hash {0} matches more than one hash: {1}")] + MatchShortHashError(String, String), + + #[error("cannot open id mapper file: {1}")] + OpenHashMapFileError(#[source] io::Error, path::PathBuf), + #[error("cannot write id mapper file: {1}")] + WriteHashMapFileError(#[source] io::Error, path::PathBuf), + #[error("cannot read line from id mapper file")] + ReadHashMapFileLineError(#[source] io::Error), +} + +type Result = result::Result; #[derive(Debug, Default)] pub struct IdMapper { - path: PathBuf, - map: HashMap, + path: path::PathBuf, + map: collections::HashMap, short_hash_len: usize, } impl IdMapper { - pub fn new(dir: &Path) -> Result { + pub fn new(dir: &path::Path) -> Result { let mut mapper = Self::default(); mapper.path = dir.join(".himalaya-id-map"); - let file = OpenOptions::new() + let file = fs::OpenOptions::new() .read(true) .write(true) .create(true) .open(&mapper.path) - .context("cannot open id hash map file")?; - let reader = BufReader::new(file); + .map_err(|err| Error::OpenHashMapFileError(err, mapper.path.to_owned()))?; + let reader = io::BufReader::new(file); for line in reader.lines() { - let line = - line.context("cannot read line from maildir envelopes id mapper cache file")?; + let line = line.map_err(Error::ReadHashMapFileLineError)?; if mapper.short_hash_len == 0 { mapper.short_hash_len = 2.max(line.parse().unwrap_or(2)); } else { - let (hash, id) = line.split_once(' ').ok_or_else(|| { - anyhow!( - "cannot parse line {:?} from maildir envelopes id mapper cache file", - line - ) - })?; + let (hash, id) = line + .split_once(' ') + .ok_or_else(|| Error::ParseLineError(line.to_owned()))?; mapper.insert(hash.to_owned(), id.to_owned()); } } @@ -51,24 +64,16 @@ impl IdMapper { .filter(|hash| hash.starts_with(short_hash)) .collect(); if matching_hashes.len() == 0 { - Err(anyhow!( - "cannot find maildir message id from short hash {:?}", - short_hash, - )) + Err(Error::FindFromShortHashError(short_hash.to_owned())) } else if matching_hashes.len() > 1 { - Err(anyhow!( - "the short hash {:?} matches more than one hash: {}", - short_hash, + Err(Error::MatchShortHashError( + short_hash.to_owned(), matching_hashes .iter() .map(|s| s.to_string()) .collect::>() - .join(", ") - ) - .context(format!( - "cannot find maildir message id from short hash {:?}", - short_hash - ))) + .join(", "), + )) } else { Ok(self.get(matching_hashes[0]).unwrap().to_owned()) } @@ -98,28 +103,28 @@ impl IdMapper { self.short_hash_len = short_hash_len; - OpenOptions::new() + fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&self.path) - .context("cannot open maildir id hash map cache")? + .map_err(|err| Error::OpenHashMapFileError(err, self.path.to_owned()))? .write(format!("{}\n{}", short_hash_len, entries).as_bytes()) - .context("cannot write maildir id hash map cache")?; + .map_err(|err| Error::WriteHashMapFileError(err, self.path.to_owned()))?; Ok(short_hash_len) } } -impl Deref for IdMapper { - type Target = HashMap; +impl ops::Deref for IdMapper { + type Target = collections::HashMap; fn deref(&self) -> &Self::Target { &self.map } } -impl DerefMut for IdMapper { +impl ops::DerefMut for IdMapper { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.map } diff --git a/lib/src/backend/imap/error.rs b/lib/src/backend/imap/error.rs new file mode 100644 index 0000000..76454a3 --- /dev/null +++ b/lib/src/backend/imap/error.rs @@ -0,0 +1,86 @@ +use std::result; +use thiserror::Error; + +use crate::{ + account, + msg::{self, Flags}, +}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot get envelope of message {0}")] + GetEnvelopeError(u32), + #[error("cannot get sender of message {0}")] + GetSenderError(u32), + #[error("cannot get imap session")] + GetSessionError, + #[error("cannot retrieve message {0}'s uid")] + GetMsgUidError(u32), + #[error("cannot find message {0}")] + FindMsgError(String), + #[error("cannot parse sort criterion {0}")] + ParseSortCriterionError(String), + + #[error("cannot decode subject of message {1}")] + DecodeSubjectError(#[source] rfc2047_decoder::Error, u32), + #[error("cannot decode sender name of message {1}")] + DecodeSenderNameError(#[source] rfc2047_decoder::Error, u32), + #[error("cannot decode sender mailbox of message {1}")] + DecodeSenderMboxError(#[source] rfc2047_decoder::Error, u32), + #[error("cannot decode sender host of message {1}")] + DecodeSenderHostError(#[source] rfc2047_decoder::Error, u32), + + #[error("cannot create tls connector")] + CreateTlsConnectorError(#[source] native_tls::Error), + #[error("cannot connect to imap server")] + ConnectImapServerError(#[source] imap::Error), + #[error("cannot login to imap server")] + LoginImapServerError(#[source] imap::Error), + #[error("cannot search new messages")] + SearchNewMsgsError(#[source] imap::Error), + #[error("cannot examine mailbox {1}")] + ExamineMboxError(#[source] imap::Error, String), + #[error("cannot start the idle mode")] + StartIdleModeError(#[source] imap::Error), + #[error("cannot parse message {1}")] + ParseMsgError(#[source] mailparse::MailParseError, String), + #[error("cannot fetch new messages envelope")] + FetchNewMsgsEnvelopeError(#[source] imap::Error), + #[error("cannot get uid of message {0}")] + GetUidError(u32), + #[error("cannot create mailbox {1}")] + CreateMboxError(#[source] imap::Error, String), + #[error("cannot list mailboxes")] + ListMboxesError(#[source] imap::Error), + #[error("cannot delete mailbox {1}")] + DeleteMboxError(#[source] imap::Error, String), + #[error("cannot select mailbox {1}")] + SelectMboxError(#[source] imap::Error, String), + #[error("cannot fetch messages within range {1}")] + FetchMsgsByRangeError(#[source] imap::Error, String), + #[error("cannot fetch messages by sequence {1}")] + FetchMsgsBySeqError(#[source] imap::Error, String), + #[error("cannot append message to mailbox {1}")] + AppendMsgError(#[source] imap::Error, String), + #[error("cannot sort messages in mailbox {1} with query: {2}")] + SortMsgsError(#[source] imap::Error, String, String), + #[error("cannot search messages in mailbox {1} with query: {2}")] + SearchMsgsError(#[source] imap::Error, String, String), + #[error("cannot expunge mailbox {1}")] + ExpungeError(#[source] imap::Error, String), + #[error("cannot add flags {1} to message(s) {2}")] + AddFlagsError(#[source] imap::Error, Flags, String), + #[error("cannot set flags {1} to message(s) {2}")] + SetFlagsError(#[source] imap::Error, Flags, String), + #[error("cannot delete flags {1} to message(s) {2}")] + DelFlagsError(#[source] imap::Error, Flags, String), + #[error("cannot logout from imap server")] + LogoutError(#[source] imap::Error), + + #[error(transparent)] + AccountError(#[from] account::Error), + #[error(transparent)] + MsgError(#[from] msg::Error), +} + +pub type Result = result::Result; diff --git a/cli/src/backends/imap/imap_backend.rs b/lib/src/backend/imap/imap_backend.rs similarity index 78% rename from cli/src/backends/imap/imap_backend.rs rename to lib/src/backend/imap/imap_backend.rs index 71e9a32..d5bcee5 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/lib/src/backend/imap/imap_backend.rs @@ -2,24 +2,20 @@ //! //! This module contains the definition of the IMAP backend. -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::{ - account::{AccountConfig, ImapBackendConfig}, - mbox::{Mbox, Mboxes}, - msg::{Envelopes, Flags}, -}; use imap::types::NameAttribute; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; use crate::{ - backends::{ - from_imap_fetch, from_imap_fetches, imap::msg_sort_criterion::SortCriteria, - into_imap_flags, Backend, + account::{AccountConfig, ImapBackendConfig}, + backend::{ + backend::Result, from_imap_fetch, from_imap_fetches, + imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend, }, - msg::Msg, - output::run_cmd, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags, Msg}, + process::run_cmd, }; type ImapSess = imap::Session>; @@ -47,7 +43,7 @@ impl<'a> ImapBackend<'a> { .danger_accept_invalid_certs(self.imap_config.imap_insecure) .danger_accept_invalid_hostnames(self.imap_config.imap_insecure) .build() - .context("cannot create TLS connector")?; + .map_err(Error::CreateTlsConnectorError)?; debug!("create client"); debug!("host: {}", self.imap_config.imap_host); @@ -60,7 +56,7 @@ impl<'a> ImapBackend<'a> { } let client = client_builder .connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?)) - .context("cannot connect to IMAP server")?; + .map_err(Error::ConnectImapServerError)?; debug!("create session"); debug!("login: {}", self.imap_config.imap_login); @@ -70,23 +66,24 @@ impl<'a> ImapBackend<'a> { &self.imap_config.imap_login, &self.imap_config.imap_passwd()?, ) - .map_err(|res| res.0) - .context("cannot login to IMAP server")?; + .map_err(|res| Error::LoginImapServerError(res.0))?; sess.debug = log_enabled!(Level::Trace); self.sess = Some(sess); } - match self.sess { + let sess = match self.sess { Some(ref mut sess) => Ok(sess), - None => Err(anyhow!("cannot get IMAP session")), - } + None => Err(Error::GetSessionError), + }?; + + Ok(sess) } fn search_new_msgs(&mut self, query: &str) -> Result> { let uids: Vec = self .sess()? .uid_search(query) - .context("cannot search new messages")? + .map_err(Error::SearchNewMsgsError)? .into_iter() .collect(); debug!("found {} new messages", uids.len()); @@ -101,7 +98,7 @@ impl<'a> ImapBackend<'a> { debug!("examine mailbox {:?}", mbox); self.sess()? .examine(mbox) - .context(format!("cannot examine mailbox {}", mbox))?; + .map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?; debug!("init messages hashset"); let mut msgs_set: HashSet = self @@ -123,7 +120,7 @@ impl<'a> ImapBackend<'a> { false }) }) - .context("cannot start the idle mode")?; + .map_err(Error::StartIdleModeError)?; let uids: Vec = self .search_new_msgs(&self.account_config.notify_query)? @@ -142,13 +139,11 @@ impl<'a> ImapBackend<'a> { let fetches = self .sess()? .uid_fetch(uids, "(UID ENVELOPE)") - .context("cannot fetch new messages enveloppe")?; + .map_err(Error::FetchNewMsgsEnvelopeError)?; for fetch in fetches.iter() { let msg = from_imap_fetch(fetch)?; - let uid = fetch.uid.ok_or_else(|| { - anyhow!("cannot retrieve message {}'s UID", fetch.message) - })?; + let uid = fetch.uid.ok_or_else(|| Error::GetUidError(fetch.message))?; let from = msg.sender.to_owned().into(); self.account_config.run_notify_cmd(&msg.subject, &from)?; @@ -171,7 +166,7 @@ impl<'a> ImapBackend<'a> { self.sess()? .examine(mbox) - .context(format!("cannot examine mailbox `{}`", mbox))?; + .map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?; loop { debug!("begin loop"); @@ -185,7 +180,7 @@ impl<'a> ImapBackend<'a> { false }) }) - .context("cannot start the idle mode")?; + .map_err(Error::StartIdleModeError)?; let cmds = self.account_config.watch_cmds.clone(); thread::spawn(move || { @@ -204,9 +199,14 @@ impl<'a> ImapBackend<'a> { impl<'a> Backend<'a> for ImapBackend<'a> { fn add_mbox(&mut self, mbox: &str) -> Result<()> { + trace!(">> add mailbox"); + self.sess()? .create(mbox) - .context(format!("cannot create imap mailbox {:?}", mbox)) + .map_err(|err| Error::CreateMboxError(err, mbox.to_owned()))?; + + trace!("<< add mailbox"); + Ok(()) } fn get_mboxes(&mut self) -> Result { @@ -215,7 +215,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let imap_mboxes = self .sess()? .list(Some(""), Some("*")) - .context("cannot list mailboxes")?; + .map_err(Error::ListMboxesError)?; let mboxes = Mboxes { mboxes: imap_mboxes .iter() @@ -244,16 +244,21 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn del_mbox(&mut self, mbox: &str) -> Result<()> { + trace!(">> delete imap mailbox"); + self.sess()? .delete(mbox) - .context(format!("cannot delete imap mailbox {:?}", mbox)) + .map_err(|err| Error::DeleteMboxError(err, mbox.to_owned()))?; + + trace!("<< delete imap mailbox"); + Ok(()) } fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result { let last_seq = self .sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))? + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? .exists as usize; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { @@ -273,9 +278,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let fetches = self .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .context(format!("cannot fetch messages within range {:?}", range))?; + .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; - from_imap_fetches(fetches) + let envelopes = from_imap_fetches(fetches)?; + Ok(envelopes) } fn search_envelopes( @@ -289,7 +295,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let last_seq = self .sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))? + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? .exists; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { @@ -301,10 +307,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let seqs: Vec = if sort.is_empty() { self.sess()? .search(query) - .context(format!( - "cannot find envelopes in {:?} with query {:?}", - mbox, query - ))? + .map_err(|err| Error::SearchMsgsError(err, mbox.to_owned(), query.to_owned()))? .iter() .map(|seq| seq.to_string()) .collect() @@ -313,10 +316,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let charset = imap::extensions::sort::SortCharset::Utf8; self.sess()? .sort(&sort, charset, query) - .context(format!( - "cannot find envelopes in {:?} with query {:?}", - mbox, query - ))? + .map_err(|err| Error::SortMsgsError(err, mbox.to_owned(), query.to_owned()))? .iter() .map(|seq| seq.to_string()) .collect() @@ -329,9 +329,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let fetches = self .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .context(format!("cannot fetch messages within range {:?}", range))?; + .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; - from_imap_fetches(fetches) + let envelopes = from_imap_fetches(fetches)?; + Ok(envelopes) } fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { @@ -340,11 +341,11 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .append(mbox, msg) .flags(into_imap_flags(&flags)) .finish() - .context(format!("cannot append message to {:?}", mbox))?; + .map_err(|err| Error::AppendMsgError(err, mbox.to_owned()))?; let last_seq = self .sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))? + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? .exists; Ok(last_seq.to_string()) } @@ -352,17 +353,18 @@ impl<'a> Backend<'a> for ImapBackend<'a> { fn get_msg(&mut self, mbox: &str, seq: &str) -> Result { self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; let fetches = self .sess()? .fetch(seq, "(FLAGS INTERNALDATE BODY[])") - .context(format!("cannot fetch messages {:?}", seq))?; + .map_err(|err| Error::FetchMsgsBySeqError(err, seq.to_owned()))?; let fetch = fetches .first() - .ok_or_else(|| anyhow!("cannot find message {:?}", seq))?; + .ok_or_else(|| Error::FindMsgError(seq.to_owned()))?; let msg_raw = fetch.body().unwrap_or_default().to_owned(); let mut msg = Msg::from_parsed_mail( - mailparse::parse_mail(&msg_raw).context("cannot parse message")?, + mailparse::parse_mail(&msg_raw) + .map_err(|err| Error::ParseMsgError(err, seq.to_owned()))?, self.account_config, )?; msg.raw = msg_raw; @@ -391,13 +393,13 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let flags: Flags = flags.into(); self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; self.sess()? .store(seq_range, format!("+FLAGS ({})", flags)) - .context(format!("cannot add flags {:?}", &flags))?; + .map_err(|err| Error::AddFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; self.sess()? .expunge() - .context(format!("cannot expunge mailbox {:?}", mbox))?; + .map_err(|err| Error::ExpungeError(err, mbox.to_owned()))?; Ok(()) } @@ -405,10 +407,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let flags: Flags = flags.into(); self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; self.sess()? .store(seq_range, format!("FLAGS ({})", flags)) - .context(format!("cannot set flags {:?}", &flags))?; + .map_err(|err| Error::SetFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; Ok(()) } @@ -416,18 +418,24 @@ impl<'a> Backend<'a> for ImapBackend<'a> { let flags: Flags = flags.into(); self.sess()? .select(mbox) - .context(format!("cannot select mailbox {:?}", mbox))?; + .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; self.sess()? .store(seq_range, format!("-FLAGS ({})", flags)) - .context(format!("cannot remove flags {:?}", &flags))?; + .map_err(|err| Error::DelFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; Ok(()) } fn disconnect(&mut self) -> Result<()> { + trace!(">> imap logout"); + if let Some(ref mut sess) = self.sess { - debug!("logout from IMAP server"); - sess.logout().context("cannot logout from IMAP server")?; + debug!("logout from imap server"); + sess.logout().map_err(Error::LogoutError)?; + } else { + debug!("no session found"); } + + trace!("<< imap logout"); Ok(()) } } diff --git a/cli/src/backends/imap/imap_envelope.rs b/lib/src/backend/imap/imap_envelope.rs similarity index 53% rename from cli/src/backends/imap/imap_envelope.rs rename to lib/src/backend/imap/imap_envelope.rs index 8a90e3e..639d009 100644 --- a/cli/src/backends/imap/imap_envelope.rs +++ b/lib/src/backend/imap/imap_envelope.rs @@ -3,10 +3,15 @@ //! This module provides IMAP types and conversion utilities related //! to the envelope. -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::msg::Envelope; +use rfc2047_decoder; -use super::from_imap_flags; +use crate::{ + backend::{ + from_imap_flags, + imap::{Error, Result}, + }, + msg::Envelope, +}; /// Represents the raw envelope returned by the `imap` crate. pub type ImapFetch = imap::types::Fetch; @@ -14,7 +19,7 @@ pub type ImapFetch = imap::types::Fetch; pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { let envelope = fetch .envelope() - .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; + .ok_or_else(|| Error::GetEnvelopeError(fetch.message))?; let id = fetch.message.to_string(); @@ -24,10 +29,8 @@ pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { .subject .as_ref() .map(|subj| { - rfc2047_decoder::decode(subj).context(format!( - "cannot decode subject of message {}", - fetch.message - )) + rfc2047_decoder::decode(subj) + .map_err(|err| Error::DecodeSubjectError(err, fetch.message)) }) .unwrap_or_else(|| Ok(String::default()))?; @@ -36,32 +39,26 @@ pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { .as_ref() .and_then(|addrs| addrs.get(0)) .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) - .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?; + .ok_or_else(|| Error::GetSenderError(fetch.message))?; let sender = if let Some(ref name) = sender.name { - rfc2047_decoder::decode(&name.to_vec()).context(format!( - "cannot decode sender's name of message {}", - fetch.message, - ))? + rfc2047_decoder::decode(&name.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message))? } else { let mbox = sender .mailbox .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) + .ok_or_else(|| Error::GetSenderError(fetch.message)) .and_then(|mbox| { - rfc2047_decoder::decode(&mbox.to_vec()).context(format!( - "cannot decode sender's mailbox of message {}", - fetch.message, - )) + rfc2047_decoder::decode(&mbox.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) })?; let host = sender .host .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) + .ok_or_else(|| Error::GetSenderError(fetch.message)) .and_then(|host| { - rfc2047_decoder::decode(&host.to_vec()).context(format!( - "cannot decode sender's host of message {}", - fetch.message, - )) + rfc2047_decoder::decode(&host.to_vec()) + .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) })?; format!("{}@{}", mbox, host) }; diff --git a/lib/src/backend/imap/imap_envelopes.rs b/lib/src/backend/imap/imap_envelopes.rs new file mode 100644 index 0000000..3cbb010 --- /dev/null +++ b/lib/src/backend/imap/imap_envelopes.rs @@ -0,0 +1,18 @@ +use crate::{ + backend::{ + imap::{from_imap_fetch, Result}, + ImapFetch, + }, + msg::Envelopes, +}; + +/// Represents the list of raw envelopes returned by the `imap` crate. +pub type ImapFetches = imap::types::ZeroCopy>; + +pub fn from_imap_fetches(fetches: ImapFetches) -> Result { + let mut envelopes = Envelopes::default(); + for fetch in fetches.iter().rev() { + envelopes.push(from_imap_fetch(fetch)?); + } + Ok(envelopes) +} diff --git a/cli/src/backends/imap/imap_flag.rs b/lib/src/backend/imap/imap_flag.rs similarity index 95% rename from cli/src/backends/imap/imap_flag.rs rename to lib/src/backend/imap/imap_flag.rs index 1a24ab0..58ec612 100644 --- a/cli/src/backends/imap/imap_flag.rs +++ b/lib/src/backend/imap/imap_flag.rs @@ -1,4 +1,4 @@ -use himalaya_lib::msg::Flag; +use crate::msg::Flag; pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag { match imap_flag { diff --git a/cli/src/backends/imap/imap_flags.rs b/lib/src/backend/imap/imap_flags.rs similarity index 91% rename from cli/src/backends/imap/imap_flags.rs rename to lib/src/backend/imap/imap_flags.rs index 186328a..3aa42d5 100644 --- a/cli/src/backends/imap/imap_flags.rs +++ b/lib/src/backend/imap/imap_flags.rs @@ -1,6 +1,7 @@ -use himalaya_lib::msg::{Flag, Flags}; - -use super::from_imap_flag; +use crate::{ + backend::from_imap_flag, + msg::{Flag, Flags}, +}; pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec> { flags diff --git a/cli/src/backends/imap/msg_sort_criterion.rs b/lib/src/backend/imap/msg_sort_criterion.rs similarity index 95% rename from cli/src/backends/imap/msg_sort_criterion.rs rename to lib/src/backend/imap/msg_sort_criterion.rs index d20e9bd..222677b 100644 --- a/cli/src/backends/imap/msg_sort_criterion.rs +++ b/lib/src/backend/imap/msg_sort_criterion.rs @@ -3,9 +3,10 @@ //! This module regroups everything related to deserialization of //! message sort criteria. -use anyhow::{anyhow, Error, Result}; use std::{convert::TryFrom, ops::Deref}; +use crate::backend::imap::Error; + /// Represents the message sort criteria. It is just a wrapper around /// the `imap::extensions::sort::SortCriterion`. pub struct SortCriteria<'a>(Vec>); @@ -53,7 +54,7 @@ impl<'a> TryFrom<&'a str> for SortCriteria<'a> { "to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( &imap::extensions::sort::SortCriterion::To, )), - _ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)), + _ => Err(Error::ParseSortCriterionError(criterion_str.to_owned())), }?); } Ok(Self(criteria)) diff --git a/lib/src/backend/maildir/error.rs b/lib/src/backend/maildir/error.rs new file mode 100644 index 0000000..898d249 --- /dev/null +++ b/lib/src/backend/maildir/error.rs @@ -0,0 +1,49 @@ +use std::{io, path}; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MaildirError { + #[error("cannot find maildir sender")] + FindSenderError, + #[error("cannot read maildir directory {0}")] + ReadDirError(path::PathBuf), + #[error("cannot parse maildir subdirectory {0}")] + ParseSubdirError(path::PathBuf), + #[error("cannot get maildir envelopes at page {0}")] + GetEnvelopesOutOfBoundsError(usize), + #[error("cannot search maildir envelopes: feature not implemented")] + SearchEnvelopesUnimplementedError, + #[error("cannot get maildir message {0}")] + GetMsgError(String), + #[error("cannot decode maildir entry")] + DecodeEntryError(#[source] io::Error), + #[error("cannot parse maildir message")] + ParseMsgError(#[source] maildir::MailEntryError), + #[error("cannot decode header {0}")] + DecodeHeaderError(#[source] rfc2047_decoder::Error, String), + #[error("cannot parse maildir message header {0}")] + ParseHeaderError(#[source] mailparse::MailParseError, String), + #[error("cannot create maildir subdirectory {1}")] + CreateSubdirError(#[source] io::Error, String), + #[error("cannot decode maildir subdirectory")] + DecodeSubdirError(#[source] io::Error), + #[error("cannot delete subdirectories at {1}")] + DeleteAllDirError(#[source] io::Error, path::PathBuf), + #[error("cannot get current directory")] + GetCurrentDirError(#[source] io::Error), + #[error("cannot store maildir message with flags")] + StoreWithFlagsError(#[source] maildir::MaildirError), + #[error("cannot copy maildir message")] + CopyMsgError(#[source] io::Error), + #[error("cannot move maildir message")] + MoveMsgError(#[source] io::Error), + #[error("cannot delete maildir message")] + DelMsgError(#[source] io::Error), + #[error("cannot add maildir flags")] + AddFlagsError(#[source] io::Error), + #[error("cannot set maildir flags")] + SetFlagsError(#[source] io::Error), + #[error("cannot remove maildir flags")] + DelFlagsError(#[source] io::Error), +} diff --git a/cli/src/backends/maildir/maildir_backend.rs b/lib/src/backend/maildir/maildir_backend.rs similarity index 50% rename from cli/src/backends/maildir/maildir_backend.rs rename to lib/src/backend/maildir/maildir_backend.rs index 2c4e2d0..c031246 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/lib/src/backend/maildir/maildir_backend.rs @@ -3,20 +3,18 @@ //! This module contains the definition of the maildir backend and its //! traits implementation. -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::{ - account::{AccountConfig, MaildirBackendConfig}, - mbox::{Mbox, Mboxes}, - msg::Envelopes, -}; use log::{debug, info, trace}; use std::{env, ffi::OsStr, fs, path::PathBuf}; use crate::{ - backends::{maildir_envelopes, Backend, IdMapper}, - msg::Msg, + account::{AccountConfig, MaildirBackendConfig}, + backend::{backend::Result, maildir_envelopes, maildir_flags, Backend, IdMapper}, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags, Msg}, }; +use super::MaildirError; + /// Represents the maildir backend. pub struct MaildirBackend<'a> { account_config: &'a AccountConfig, @@ -35,11 +33,12 @@ impl<'a> MaildirBackend<'a> { } fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result { - if mdir_path.is_dir() { + let path = if mdir_path.is_dir() { Ok(mdir_path) } else { - Err(anyhow!("cannot read maildir directory {:?}", mdir_path)) - } + Err(MaildirError::ReadDirError(mdir_path.to_owned())) + }?; + Ok(path) } /// Creates a maildir instance from a string slice. @@ -60,7 +59,13 @@ impl<'a> MaildirBackend<'a> { // 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(|_| { + self.validate_mdir_path( + env::current_dir() + .map_err(MaildirError::GetCurrentDirError)? + .join(&dir), + ) + }) .or_else(|_| { // Otherwise creates a maildir instance from a maildir // subdirectory by adding a "." in front of the name @@ -82,7 +87,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { trace!("subdir path: {:?}", path); fs::create_dir(&path) - .with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?; + .map_err(|err| MaildirError::CreateSubdirError(err, subdir.to_owned()))?; info!("<< add maildir subdir"); Ok(()) @@ -100,19 +105,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { }) } for entry in self.mdir.list_subdirs() { - let dir = entry?; + let dir = entry.map_err(MaildirError::DecodeSubdirError)?; let dirname = dir.path().file_name(); mboxes.push(Mbox { delim: String::from("/"), name: dirname .and_then(OsStr::to_str) .and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) }) - .ok_or_else(|| { - anyhow!( - "cannot parse maildir subdirectory name from path {:?}", - dirname - ) - })? + .ok_or_else(|| MaildirError::ParseSubdirError(dir.path().to_owned()))? .into(), ..Mbox::default() }); @@ -131,7 +131,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { trace!("dir path: {:?}", path); fs::remove_dir_all(&path) - .with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?; + .map_err(|err| MaildirError::DeleteAllDirError(err, path.to_owned()))?; info!("<< delete maildir dir"); Ok(()) @@ -143,16 +143,11 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("page size: {:?}", page_size); debug!("page: {:?}", page); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; + let mdir = self.get_mdir_from_dir(dir)?; // Reads envelopes from the "cur" folder of the selected // maildir. - let mut envelopes = - maildir_envelopes::from_maildir_entries(mdir.list_cur()).with_context(|| { - format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) - })?; + let mut envelopes = maildir_envelopes::from_maildir_entries(mdir.list_cur())?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -160,10 +155,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let page_begin = page * page_size; debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { - return Err(anyhow!( - "cannot get maildir envelopes at page {:?} (out of bounds)", - page_begin + 1, - )); + return Err(MaildirError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; } let page_end = envelopes.len().min(page_begin + page_size); debug!("page end: {:?}", page_end); @@ -207,9 +199,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { ) -> Result { info!(">> search maildir envelopes"); info!("<< search maildir envelopes"); - Err(anyhow!( - "cannot find maildir envelopes: feature not implemented" - )) + Err(MaildirError::SearchEnvelopesUnimplementedError)? } fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result { @@ -217,27 +207,20 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("dir: {:?}", dir); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; + let flags = Flags::from(flags); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(dir)?; let id = mdir - .store_cur_with_flags(msg, &flags.to_string()) - .with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?; + .store_cur_with_flags(msg, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::StoreWithFlagsError)?; debug!("id: {:?}", id); let hash = format!("{:x}", md5::compute(&id)); debug!("hash: {:?}", hash); // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?; - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + let mut mapper = IdMapper::new(mdir.path())?; + mapper.append(vec![(hash.clone(), id.clone())])?; info!("<< add maildir message"); Ok(hash) @@ -248,32 +231,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let id = IdMapper::new(mdir.path())? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - let mut mail_entry = mdir.find(&id).ok_or_else(|| { - anyhow!( - "cannot find maildir message by id {:?} at {:?}", - id, - mdir.path() - ) - })?; - let parsed_mail = mail_entry.parsed().with_context(|| { - format!("cannot parse maildir message {:?} at {:?}", id, mdir.path()) - })?; - let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).with_context(|| { - format!("cannot parse maildir message {:?} at {:?}", id, mdir.path()) - })?; + let mut mail_entry = mdir + .find(&id) + .ok_or_else(|| MaildirError::GetMsgError(id.to_owned()))?; + let parsed_mail = mail_entry.parsed().map_err(MaildirError::ParseMsgError)?; + let msg = Msg::from_parsed_mail(parsed_mail, self.account_config)?; trace!("message: {:?}", msg); info!("<< get maildir message"); @@ -285,46 +250,19 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("source dir: {:?}", dir_src); debug!("destination dir: {:?}", dir_dst); - let mdir_src = self - .get_mdir_from_dir(dir_src) - .with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?; - let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| { - format!("cannot get destination maildir instance from {:?}", dir_dst) - })?; - let id = IdMapper::new(mdir_src.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir_src.path() - ) - })?; + let mdir_src = self.get_mdir_from_dir(dir_src)?; + let mdir_dst = self.get_mdir_from_dir(dir_dst)?; + let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir_src.copy_to(&id, &mdir_dst).with_context(|| { - format!( - "cannot copy message {:?} from maildir {:?} to maildir {:?}", - id, - mdir_src.path(), - mdir_dst.path() - ) - })?; + mdir_src + .copy_to(&id, &mdir_dst) + .map_err(MaildirError::CopyMsgError)?; // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| { - format!("cannot create id mapper instance for {:?}", mdir_dst.path()) - })?; + let mut mapper = IdMapper::new(mdir_dst.path())?; let hash = format!("{:x}", md5::compute(&id)); - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + mapper.append(vec![(hash.clone(), id.clone())])?; info!("<< copy maildir message"); Ok(()) @@ -335,46 +273,19 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("source dir: {:?}", dir_src); debug!("destination dir: {:?}", dir_dst); - let mdir_src = self - .get_mdir_from_dir(dir_src) - .with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?; - let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| { - format!("cannot get destination maildir instance from {:?}", dir_dst) - })?; - let id = IdMapper::new(mdir_src.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir_src.path() - ) - })?; + let mdir_src = self.get_mdir_from_dir(dir_src)?; + let mdir_dst = self.get_mdir_from_dir(dir_dst)?; + let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir_src.move_to(&id, &mdir_dst).with_context(|| { - format!( - "cannot move message {:?} from maildir {:?} to maildir {:?}", - id, - mdir_src.path(), - mdir_dst.path() - ) - })?; + mdir_src + .move_to(&id, &mdir_dst) + .map_err(MaildirError::MoveMsgError)?; // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| { - format!("cannot create id mapper instance for {:?}", mdir_dst.path()) - })?; + let mut mapper = IdMapper::new(mdir_dst.path())?; let hash = format!("{:x}", md5::compute(&id)); - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + mapper.append(vec![(hash.clone(), id.clone())])?; info!("<< move maildir message"); Ok(()) @@ -385,27 +296,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.delete(&id).with_context(|| { - format!( - "cannot delete message {:?} from maildir {:?}", - id, - mdir.path() - ) - })?; + mdir.delete(&id).map_err(MaildirError::DelMsgError)?; info!("<< delete maildir message"); Ok(()) @@ -415,25 +309,15 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { info!(">> add maildir message flags"); debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); + let flags = Flags::from(flags); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - debug!("flags: {:?}", flags); - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.add_flags(&id, &flags.to_string()) - .with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?; + + mdir.add_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::AddFlagsError)?; info!("<< add maildir message flags"); Ok(()) @@ -443,25 +327,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { info!(">> set maildir message flags"); debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); + let flags = Flags::from(flags); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - debug!("flags: {:?}", flags); - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.set_flags(&id, &flags.to_string()) - .with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?; + mdir.set_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::SetFlagsError)?; info!("<< set maildir message flags"); Ok(()) @@ -471,30 +344,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { info!(">> delete maildir message flags"); debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); + let flags = Flags::from(flags); debug!("flags: {:?}", flags); - let mdir = self - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - debug!("flags: {:?}", flags); - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ) - })?; + let mdir = self.get_mdir_from_dir(dir)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; debug!("id: {:?}", id); - mdir.remove_flags(&id, &flags.to_string()) - .with_context(|| { - format!( - "cannot delete flags {:?} to maildir message {:?}", - flags, id - ) - })?; + mdir.remove_flags(&id, &maildir_flags::to_normalized_string(&flags)) + .map_err(MaildirError::DelFlagsError)?; info!("<< delete maildir message flags"); Ok(()) diff --git a/cli/src/backends/maildir/maildir_envelope.rs b/lib/src/backend/maildir/maildir_envelope.rs similarity index 83% rename from cli/src/backends/maildir/maildir_envelope.rs rename to lib/src/backend/maildir/maildir_envelope.rs index 8cc9884..58966fd 100644 --- a/cli/src/backends/maildir/maildir_envelope.rs +++ b/lib/src/backend/maildir/maildir_envelope.rs @@ -1,13 +1,13 @@ -use anyhow::{anyhow, Context, Result}; use chrono::DateTime; -use himalaya_lib::msg::Envelope; use log::trace; use crate::{ - backends::maildir_flags, - msg::{from_slice_to_addrs, Addr}, + backend::{backend::Result, maildir_flags}, + msg::{from_slice_to_addrs, Addr, Envelope}, }; +use super::MaildirError; + /// Represents the raw envelope returned by the `maildir` crate. pub type MaildirEnvelope = maildir::MailEntry; @@ -20,7 +20,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { envelope.id = format!("{:x}", md5::compute(&envelope.internal_id)); envelope.flags = maildir_flags::from_maildir_entry(&entry); - let parsed_mail = entry.parsed().context("cannot parse maildir mail entry")?; + let parsed_mail = entry.parsed().map_err(MaildirError::ParseMsgError)?; trace!(">> parse headers"); for h in parsed_mail.get_headers() { @@ -28,7 +28,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { trace!("header key: {:?}", k); let v = rfc2047_decoder::decode(h.get_value_raw()) - .context(format!("cannot decode value from header {:?}", k))?; + .map_err(|err| MaildirError::DecodeHeaderError(err, k.to_owned()))?; trace!("header value: {:?}", v); match k.to_lowercase().as_str() { @@ -43,7 +43,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { } "from" => { envelope.sender = from_slice_to_addrs(v) - .context(format!("cannot parse header {:?}", k))? + .map_err(|err| MaildirError::ParseHeaderError(err, k.to_owned()))? .and_then(|senders| { if senders.is_empty() { None @@ -59,7 +59,7 @@ pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { group_name.to_owned() } }) - .ok_or_else(|| anyhow!("cannot find sender"))?; + .ok_or_else(|| MaildirError::FindSenderError)?; } _ => (), } diff --git a/cli/src/backends/maildir/maildir_envelopes.rs b/lib/src/backend/maildir/maildir_envelopes.rs similarity index 52% rename from cli/src/backends/maildir/maildir_envelopes.rs rename to lib/src/backend/maildir/maildir_envelopes.rs index 4921227..ff83a58 100644 --- a/cli/src/backends/maildir/maildir_envelopes.rs +++ b/lib/src/backend/maildir/maildir_envelopes.rs @@ -1,25 +1,21 @@ //! Maildir mailbox module. //! //! This module provides Maildir types and conversion utilities -//! related to the envelope +//! related to the envelope. -use himalaya_lib::msg::Envelopes; -use anyhow::{Result, Context}; +use crate::{backend::backend::Result, msg::Envelopes}; -use super::maildir_envelope; +use super::{maildir_envelope, MaildirError}; -/// Represents a list of raw envelopees returned by the `maildir` crate. +/// Represents a list of raw envelopees returned by the `maildir` +/// crate. pub type MaildirEnvelopes = maildir::MailEntries; pub fn from_maildir_entries(mail_entries: MaildirEnvelopes) -> Result { let mut envelopes = Envelopes::default(); for entry in mail_entries { - envelopes.push( - maildir_envelope::from_maildir_entry( - entry.context("cannot decode maildir mail entry")?, - ) - .context("cannot parse maildir mail entry")?, - ); + let entry = entry.map_err(MaildirError::DecodeEntryError)?; + envelopes.push(maildir_envelope::from_maildir_entry(entry)?); } Ok(envelopes) } diff --git a/lib/src/backend/maildir/maildir_flag.rs b/lib/src/backend/maildir/maildir_flag.rs new file mode 100644 index 0000000..f506e4a --- /dev/null +++ b/lib/src/backend/maildir/maildir_flag.rs @@ -0,0 +1,24 @@ +use crate::msg::Flag; + +pub fn from_char(c: char) -> Flag { + match c { + 'r' | 'R' => Flag::Answered, + 's' | 'S' => Flag::Seen, + 't' | 'T' => Flag::Deleted, + 'd' | 'D' => Flag::Draft, + 'f' | 'F' => Flag::Flagged, + 'p' | 'P' => Flag::Custom(String::from("Passed")), + flag => Flag::Custom(flag.to_string()), + } +} + +pub fn to_normalized_char(flag: &Flag) -> Option { + match flag { + Flag::Answered => Some('R'), + Flag::Seen => Some('S'), + Flag::Deleted => Some('T'), + Flag::Draft => Some('D'), + Flag::Flagged => Some('F'), + _ => None, + } +} diff --git a/lib/src/backend/maildir/maildir_flags.rs b/lib/src/backend/maildir/maildir_flags.rs new file mode 100644 index 0000000..db537d7 --- /dev/null +++ b/lib/src/backend/maildir/maildir_flags.rs @@ -0,0 +1,11 @@ +use crate::msg::Flags; + +use super::maildir_flag; + +pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { + entry.flags().chars().map(maildir_flag::from_char).collect() +} + +pub fn to_normalized_string(flags: &Flags) -> String { + String::from_iter(flags.iter().filter_map(maildir_flag::to_normalized_char)) +} diff --git a/lib/src/backend/mod.rs b/lib/src/backend/mod.rs new file mode 100644 index 0000000..665c543 --- /dev/null +++ b/lib/src/backend/mod.rs @@ -0,0 +1,73 @@ +pub mod backend; +pub use backend::*; + +pub mod id_mapper; +pub use id_mapper::*; + +#[cfg(feature = "imap-backend")] +pub mod imap { + pub mod imap_backend; + pub use imap_backend::*; + + pub mod imap_envelopes; + pub use imap_envelopes::*; + + pub mod imap_envelope; + pub use imap_envelope::*; + + pub mod imap_flags; + pub use imap_flags::*; + + pub mod imap_flag; + pub use imap_flag::*; + + pub mod msg_sort_criterion; + + pub mod error; + pub use error::*; +} + +#[cfg(feature = "imap-backend")] +pub use self::imap::*; + +#[cfg(feature = "maildir-backend")] +pub mod maildir { + pub mod maildir_backend; + pub use maildir_backend::*; + + pub mod maildir_envelopes; + pub use maildir_envelopes::*; + + pub mod maildir_envelope; + pub use maildir_envelope::*; + + pub mod maildir_flags; + pub use maildir_flags::*; + + pub mod maildir_flag; + pub use maildir_flag::*; + + pub mod error; + pub use error::*; +} + +#[cfg(feature = "maildir-backend")] +pub use self::maildir::*; + +#[cfg(feature = "notmuch-backend")] +pub mod notmuch { + pub mod notmuch_backend; + pub use notmuch_backend::*; + + pub mod notmuch_envelopes; + pub use notmuch_envelopes::*; + + pub mod notmuch_envelope; + pub use notmuch_envelope::*; + + pub mod error; + pub use error::*; +} + +#[cfg(feature = "notmuch-backend")] +pub use self::notmuch::*; diff --git a/lib/src/backend/notmuch/error.rs b/lib/src/backend/notmuch/error.rs new file mode 100644 index 0000000..5ff1485 --- /dev/null +++ b/lib/src/backend/notmuch/error.rs @@ -0,0 +1,49 @@ +use std::io; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NotmuchError { + #[error("cannot parse notmuch message header {1}")] + ParseMsgHeaderError(#[source] notmuch::Error, String), + #[error("cannot parse notmuch message date {1}")] + ParseMsgDateError(#[source] chrono::ParseError, String), + #[error("cannot find notmuch message header {0}")] + FindMsgHeaderError(String), + #[error("cannot find notmuch message sender")] + FindSenderError, + #[error("cannot parse notmuch message senders {1}")] + ParseSendersError(#[source] mailparse::MailParseError, String), + #[error("cannot open notmuch database")] + OpenDbError(#[source] notmuch::Error), + #[error("cannot build notmuch query")] + BuildQueryError(#[source] notmuch::Error), + #[error("cannot search notmuch envelopes")] + SearchEnvelopesError(#[source] notmuch::Error), + #[error("cannot get notmuch envelopes at page {0}")] + GetEnvelopesOutOfBoundsError(usize), + #[error("cannot add notmuch mailbox: feature not implemented")] + AddMboxUnimplementedError, + #[error("cannot delete notmuch mailbox: feature not implemented")] + DelMboxUnimplementedError, + #[error("cannot copy notmuch message: feature not implemented")] + CopyMsgUnimplementedError, + #[error("cannot move notmuch message: feature not implemented")] + MoveMsgUnimplementedError, + #[error("cannot index notmuch message")] + IndexFileError(#[source] notmuch::Error), + #[error("cannot find notmuch message")] + FindMsgError(#[source] notmuch::Error), + #[error("cannot find notmuch message")] + FindMsgEmptyError, + #[error("cannot read notmuch raw message from file")] + ReadMsgError(#[source] io::Error), + #[error("cannot parse notmuch raw message")] + ParseMsgError(#[source] mailparse::MailParseError), + #[error("cannot delete notmuch message")] + DelMsgError(#[source] notmuch::Error), + #[error("cannot add notmuch tag")] + AddTagError(#[source] notmuch::Error), + #[error("cannot delete notmuch tag")] + DelTagError(#[source] notmuch::Error), +} diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/lib/src/backend/notmuch/notmuch_backend.rs similarity index 61% rename from cli/src/backends/notmuch/notmuch_backend.rs rename to lib/src/backend/notmuch/notmuch_backend.rs index b9bcc12..8918c9b 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/lib/src/backend/notmuch/notmuch_backend.rs @@ -1,16 +1,13 @@ +use log::{debug, info, trace}; use std::fs; -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::{ - account::{AccountConfig, NotmuchBackendConfig}, - mbox::{Mbox, Mboxes}, - msg::Envelopes, -}; -use log::{debug, info, trace}; - use crate::{ - backends::{notmuch_envelopes, Backend, IdMapper, MaildirBackend}, - msg::Msg, + account::{AccountConfig, NotmuchBackendConfig}, + backend::{ + backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError, + }, + mbox::{Mbox, Mboxes}, + msg::{Envelopes, Msg}, }; /// Represents the Notmuch backend. @@ -37,12 +34,7 @@ impl<'a> NotmuchBackend<'a> { notmuch_config.notmuch_database_dir.clone(), notmuch::DatabaseMode::ReadWrite, ) - .with_context(|| { - format!( - "cannot open notmuch database at {:?}", - notmuch_config.notmuch_database_dir - ) - })?, + .map_err(NotmuchError::OpenDbError)?, }; info!("<< create new notmuch backend"); @@ -59,12 +51,12 @@ impl<'a> NotmuchBackend<'a> { let query_builder = self .db .create_query(query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let mut envelopes = - notmuch_envelopes::from_notmuch_msgs(query_builder.search_messages().with_context( - || format!("cannot find notmuch envelopes from query {:?}", query), - )?) - .with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; + let mut envelopes = notmuch_envelopes::from_notmuch_msgs( + query_builder + .search_messages() + .map_err(NotmuchError::SearchEnvelopesError)?, + )?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -72,10 +64,7 @@ impl<'a> NotmuchBackend<'a> { let page_begin = page * page_size; debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { - return Err(anyhow!( - "cannot get notmuch envelopes at page {:?} (out of bounds)", - page_begin + 1, - )); + return Err(NotmuchError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; } let page_end = envelopes.len().min(page_begin + page_size); debug!("page end: {:?}", page_end); @@ -113,9 +102,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn add_mbox(&mut self, _mbox: &str) -> Result<()> { info!(">> add notmuch mailbox"); info!("<< add notmuch mailbox"); - Err(anyhow!( - "cannot add notmuch mailbox: feature not implemented" - )) + Err(NotmuchError::AddMboxUnimplementedError)? } fn get_mboxes(&mut self) -> Result { @@ -139,9 +126,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn del_mbox(&mut self, _mbox: &str) -> Result<()> { info!(">> delete notmuch mailbox"); info!("<< delete notmuch mailbox"); - Err(anyhow!( - "cannot delete notmuch mailbox: feature not implemented" - )) + Err(NotmuchError::DelMboxUnimplementedError)? } fn get_envelopes( @@ -205,51 +190,32 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let dir = &self.notmuch_config.notmuch_database_dir; // Adds the message to the maildir folder and gets its hash. - let hash = self - .mdir - .add_msg("", msg, "seen") - .with_context(|| { - format!( - "cannot add notmuch message to maildir {:?}", - self.notmuch_config.notmuch_database_dir - ) - })? - .to_string(); + let hash = self.mdir.add_msg("", msg, "seen")?; debug!("hash: {:?}", hash); // Retrieves the file path of the added message by its maildir // identifier. - let mut mapper = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))?; - let id = mapper - .find(&hash) - .with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?; + let mut mapper = IdMapper::new(dir)?; + let id = mapper.find(&hash)?; debug!("id: {:?}", id); let file_path = dir.join("cur").join(format!("{}:2,S", id)); debug!("file path: {:?}", file_path); + println!("file_path: {:?}", file_path); // Adds the message to the notmuch database by indexing it. let id = self .db .index_file(&file_path, None) - .with_context(|| format!("cannot index notmuch message from file {:?}", file_path))? + .map_err(NotmuchError::IndexFileError)? .id() .to_string(); let hash = format!("{:x}", md5::compute(&id)); // Appends hash entry to the id mapper cache file. - mapper - .append(vec![(hash.clone(), id.clone())]) - .with_context(|| { - format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ) - })?; + mapper.append(vec![(hash.clone(), id.clone())])?; // Attaches tags to the notmuch message. - self.add_flags("", &hash, tags) - .with_context(|| format!("cannot add flags to notmuch message {:?}", id))?; + self.add_flags("", &hash, tags)?; info!("<< add notmuch envelopes"); Ok(hash) @@ -260,31 +226,19 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("short hash: {:?}", short_hash); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let msg_file_path = self .db .find_message(&id) - .with_context(|| format!("cannot find notmuch message {:?}", id))? - .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? + .map_err(NotmuchError::FindMsgError)? + .ok_or_else(|| NotmuchError::FindMsgEmptyError)? .filename() .to_owned(); debug!("message file path: {:?}", msg_file_path); - let raw_msg = fs::read(&msg_file_path).with_context(|| { - format!("cannot read notmuch message from file {:?}", msg_file_path) - })?; - let msg = mailparse::parse_mail(&raw_msg) - .with_context(|| format!("cannot parse raw notmuch message {:?}", id))?; - let msg = Msg::from_parsed_mail(msg, &self.account_config) - .with_context(|| format!("cannot parse notmuch message {:?}", id))?; + let raw_msg = fs::read(&msg_file_path).map_err(NotmuchError::ReadMsgError)?; + let msg = mailparse::parse_mail(&raw_msg).map_err(NotmuchError::ParseMsgError)?; + let msg = Msg::from_parsed_mail(msg, &self.account_config)?; trace!("message: {:?}", msg); info!("<< get notmuch message"); @@ -294,17 +248,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { info!(">> copy notmuch message"); info!("<< copy notmuch message"); - Err(anyhow!( - "cannot copy notmuch message: feature not implemented" - )) + Err(NotmuchError::CopyMsgUnimplementedError)? } fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { info!(">> move notmuch message"); info!("<< move notmuch message"); - Err(anyhow!( - "cannot move notmuch message: feature not implemented" - )) + Err(NotmuchError::MoveMsgUnimplementedError)? } fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> { @@ -312,27 +262,19 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("short hash: {:?}", short_hash); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let msg_file_path = self .db .find_message(&id) - .with_context(|| format!("cannot find notmuch message {:?}", id))? - .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? + .map_err(NotmuchError::FindMsgError)? + .ok_or_else(|| NotmuchError::FindMsgEmptyError)? .filename() .to_owned(); debug!("message file path: {:?}", msg_file_path); self.db .remove_message(msg_file_path) - .with_context(|| format!("cannot delete notmuch message {:?}", id))?; + .map_err(NotmuchError::DelMsgError)?; info!("<< delete notmuch message"); Ok(()) @@ -343,15 +285,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("tags: {:?}", tags); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let query = format!("id:{}", id); debug!("query: {:?}", query); @@ -359,15 +293,14 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let query_builder = self .db .create_query(&query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; let msgs = query_builder .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::SearchEnvelopesError)?; + for msg in msgs { for tag in tags.iter() { - msg.add_tag(*tag).with_context(|| { - format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id()) - })? + msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?; } } @@ -380,15 +313,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("tags: {:?}", tags); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let query = format!("id:{}", id); debug!("query: {:?}", query); @@ -396,18 +321,15 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let query_builder = self .db .create_query(&query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; let msgs = query_builder .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::SearchEnvelopesError)?; for msg in msgs { - msg.remove_all_tags().with_context(|| { - format!("cannot remove all tags from notmuch message {:?}", msg.id()) - })?; + msg.remove_all_tags().map_err(NotmuchError::DelTagError)?; + for tag in tags.iter() { - msg.add_tag(*tag).with_context(|| { - format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id()) - })? + msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?; } } @@ -420,15 +342,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { debug!("tags: {:?}", tags); let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? - .find(short_hash) - .with_context(|| { - format!( - "cannot find notmuch message from short hash {:?}", - short_hash - ) - })?; + let id = IdMapper::new(dir)?.find(short_hash)?; debug!("id: {:?}", id); let query = format!("id:{}", id); debug!("query: {:?}", query); @@ -436,19 +350,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let query_builder = self .db .create_query(&query) - .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + .map_err(NotmuchError::BuildQueryError)?; let msgs = query_builder .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + .map_err(NotmuchError::SearchEnvelopesError)?; for msg in msgs { for tag in tags.iter() { - msg.remove_tag(*tag).with_context(|| { - format!( - "cannot delete tag {:?} from notmuch message {:?}", - tag, - msg.id() - ) - })? + msg.remove_tag(*tag).map_err(NotmuchError::DelTagError)?; } } diff --git a/cli/src/backends/notmuch/notmuch_envelope.rs b/lib/src/backend/notmuch/notmuch_envelope.rs similarity index 69% rename from cli/src/backends/notmuch/notmuch_envelope.rs rename to lib/src/backend/notmuch/notmuch_envelope.rs index 4d53ba2..6361a9a 100644 --- a/cli/src/backends/notmuch/notmuch_envelope.rs +++ b/lib/src/backend/notmuch/notmuch_envelope.rs @@ -3,12 +3,13 @@ //! This module provides Notmuch types and conversion utilities //! related to the envelope -use anyhow::{anyhow, Context, Result}; use chrono::DateTime; -use himalaya_lib::msg::{Envelope, Flag}; use log::{info, trace}; -use crate::msg::{from_slice_to_addrs, Addr}; +use crate::{ + backend::{backend::Result, NotmuchError}, + msg::{from_slice_to_addrs, Addr, Envelope, Flag}, +}; /// Represents the raw envelope returned by the `notmuch` crate. pub type RawNotmuchEnvelope = notmuch::Message; @@ -20,15 +21,16 @@ pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { let id = format!("{:x}", md5::compute(&internal_id)); let subject = raw_envelope .header("subject") - .context("cannot get header \"Subject\" from notmuch message")? + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("subject")))? .unwrap_or_default() .to_string(); let sender = raw_envelope .header("from") - .context("cannot get header \"From\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", internal_id))? + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("from")))? + .ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("from")))? .to_string(); - let sender = from_slice_to_addrs(sender)? + let sender = from_slice_to_addrs(&sender) + .map_err(|err| NotmuchError::ParseSendersError(err, sender.to_owned()))? .and_then(|senders| { if senders.is_empty() { None @@ -42,17 +44,14 @@ pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { } Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), }) - .ok_or_else(|| anyhow!("cannot find sender"))?; + .ok_or_else(|| NotmuchError::FindSenderError)?; let date = raw_envelope .header("date") - .context("cannot get header \"Date\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", internal_id))? + .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("date")))? + .ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("date")))? .to_string(); let date = DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) - .context(format!( - "cannot parse message date {:?} of notmuch message {:?}", - date, internal_id - )) + .map_err(|err| NotmuchError::ParseMsgDateError(err, date.to_owned())) .map(|date| date.naive_local().to_string()) .ok(); diff --git a/cli/src/backends/notmuch/notmuch_envelopes.rs b/lib/src/backend/notmuch/notmuch_envelopes.rs similarity index 66% rename from cli/src/backends/notmuch/notmuch_envelopes.rs rename to lib/src/backend/notmuch/notmuch_envelopes.rs index 7235a46..7bf1240 100644 --- a/cli/src/backends/notmuch/notmuch_envelopes.rs +++ b/lib/src/backend/notmuch/notmuch_envelopes.rs @@ -1,5 +1,4 @@ -use anyhow::{Context, Result}; -use himalaya_lib::msg::Envelopes; +use crate::{backend::backend::Result, msg::Envelopes}; use super::notmuch_envelope; @@ -10,8 +9,7 @@ pub type RawNotmuchEnvelopes = notmuch::Messages; pub fn from_notmuch_msgs(msgs: RawNotmuchEnvelopes) -> Result { let mut envelopes = Envelopes::default(); for msg in msgs { - let envelope = - notmuch_envelope::from_notmuch_msg(msg).context("cannot parse notmuch message")?; + let envelope = notmuch_envelope::from_notmuch_msg(msg)?; envelopes.push(envelope); } Ok(envelopes) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d8398a8..ab692bc 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,6 @@ mod process; pub mod account; +pub mod backend; pub mod mbox; pub mod msg; diff --git a/cli/src/msg/addr_entity.rs b/lib/src/msg/addr.rs similarity index 91% rename from cli/src/msg/addr_entity.rs rename to lib/src/msg/addr.rs index f55278d..0a8b6d5 100644 --- a/cli/src/msg/addr_entity.rs +++ b/lib/src/msg/addr.rs @@ -2,9 +2,10 @@ //! //! This module regroups email address entities and converters. -use anyhow::Result; use mailparse; -use std::fmt::Debug; +use std::{fmt, result}; + +use crate::msg::Result; /// Defines a single email address. pub type Addr = mailparse::MailAddr; @@ -13,7 +14,9 @@ pub type Addr = mailparse::MailAddr; pub type Addrs = mailparse::MailAddrList; /// Converts a slice into an optional list of addresses. -pub fn from_slice_to_addrs + Debug>(addrs: S) -> Result> { +pub fn from_slice_to_addrs + fmt::Debug>( + addrs: S, +) -> result::Result, mailparse::MailParseError> { let addrs = mailparse::addrparse(addrs.as_ref())?; Ok(if addrs.is_empty() { None } else { Some(addrs) }) } diff --git a/lib/src/msg/error.rs b/lib/src/msg/error.rs new file mode 100644 index 0000000..0cf689f --- /dev/null +++ b/lib/src/msg/error.rs @@ -0,0 +1,56 @@ +use std::{ + env, io, + path::{self, PathBuf}, + result, +}; +use thiserror::Error; + +use crate::account; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot expand attachment path {1}")] + ExpandAttachmentPathError(#[source] shellexpand::LookupError, String), + #[error("cannot read attachment at {1}")] + ReadAttachmentError(#[source] io::Error, PathBuf), + #[error("cannot parse template")] + ParseTplError(#[source] mailparse::MailParseError), + #[error("cannot parse content type of attachment {1}")] + ParseAttachmentContentTypeError(#[source] lettre::message::header::ContentTypeErr, String), + #[error("cannot write temporary multipart on the disk")] + WriteTmpMultipartError(#[source] io::Error), + #[error("cannot write temporary multipart on the disk")] + BuildSendableMsgError(#[source] lettre::error::Error), + #[error("cannot parse {1} value: {2}")] + ParseHeaderError(#[source] mailparse::MailParseError, String, String), + #[error("cannot build envelope")] + BuildEnvelopeError(#[source] lettre::error::Error), + #[error("cannot get file name of attachment {0}")] + GetAttachmentFilenameError(PathBuf), + #[error("cannot parse recipient")] + ParseRecipientError, + + #[error("cannot parse message or address")] + ParseAddressError(#[from] lettre::address::AddressError), + + #[error(transparent)] + AccountError(#[from] account::Error), + + #[error("cannot get content type of multipart")] + GetMultipartContentTypeError, + #[error("cannot find encrypted part of multipart")] + GetEncryptedPartMultipartError, + #[error("cannot parse encrypted part of multipart")] + ParseEncryptedPartError(#[source] mailparse::MailParseError), + #[error("cannot get body from encrypted part")] + GetEncryptedPartBodyError(#[source] mailparse::MailParseError), + #[error("cannot write encrypted part to temporary file")] + WriteEncryptedPartBodyError(#[source] io::Error), + #[error("cannot write encrypted part to temporary file")] + DecryptPartError(#[source] account::Error), + + #[error("cannot delete local draft: {1}")] + DeleteLocalDraftError(#[source] io::Error, path::PathBuf), +} + +pub type Result = result::Result; diff --git a/lib/src/msg/flags.rs b/lib/src/msg/flags.rs index 50db6ce..28faad1 100644 --- a/lib/src/msg/flags.rs +++ b/lib/src/msg/flags.rs @@ -1,6 +1,5 @@ -use std::{fmt, ops}; - use serde::Serialize; +use std::{fmt, ops}; use super::Flag; diff --git a/lib/src/msg/mod.rs b/lib/src/msg/mod.rs index d3bec40..9470357 100644 --- a/lib/src/msg/mod.rs +++ b/lib/src/msg/mod.rs @@ -1,3 +1,6 @@ +mod error; +pub use error::*; + mod flag; pub use flag::*; @@ -9,3 +12,18 @@ pub use envelope::*; mod envelopes; pub use envelopes::*; + +mod parts; +pub use parts::*; + +mod addr; +pub use addr::*; + +mod tpl; +pub use tpl::*; + +mod msg; +pub use msg::*; + +mod msg_utils; +pub use msg_utils::*; diff --git a/cli/src/msg/msg_entity.rs b/lib/src/msg/msg.rs similarity index 84% rename from cli/src/msg/msg_entity.rs rename to lib/src/msg/msg.rs index 0ee49ce..64f04f8 100644 --- a/cli/src/msg/msg_entity.rs +++ b/lib/src/msg/msg.rs @@ -1,10 +1,6 @@ use ammonia; -use anyhow::{anyhow, Context, Error, Result}; use chrono::{DateTime, Local, TimeZone, Utc}; use convert_case::{Case, Casing}; -use himalaya_lib::account::{ - AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM, -}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use log::{info, trace, warn}; @@ -17,19 +13,14 @@ use std::{ fs, path::PathBuf, }; +use tree_magic; use uuid::Uuid; use crate::{ - backends::Backend, + account::{AccountConfig, DEFAULT_SIG_DELIM}, msg::{ - from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils, - Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, - }, - output::PrinterService, - smtp::SmtpService, - ui::{ - choice::{self, PostEditChoice, PreEditChoice}, - editor, + from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr, + Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride, }, }; @@ -329,100 +320,6 @@ impl Msg { Ok(self) } - fn _edit_with_editor(&self, tpl: TplOverride, account: &AccountConfig) -> Result { - let tpl = self.to_tpl(tpl, account)?; - let tpl = editor::open_with_tpl(tpl)?; - Self::from_tpl(&tpl) - } - - pub fn edit_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( - mut self, - tpl: TplOverride, - account: &AccountConfig, - printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, - ) -> Result> { - info!("start editing with editor"); - - let draft = msg_utils::local_draft_path(); - if draft.exists() { - loop { - match choice::pre_edit() { - Ok(choice) => match choice { - PreEditChoice::Edit => { - let tpl = editor::open_with_draft()?; - self.merge_with(Msg::from_tpl(&tpl)?); - break; - } - PreEditChoice::Discard => { - self.merge_with(self._edit_with_editor(tpl.clone(), account)?); - break; - } - PreEditChoice::Quit => return Ok(backend), - }, - Err(err) => { - println!("{}", err); - continue; - } - } - } - } else { - self.merge_with(self._edit_with_editor(tpl.clone(), account)?); - } - - loop { - match choice::post_edit() { - Ok(PostEditChoice::Send) => { - 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); - 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("Done!")?; - break; - } - Ok(PostEditChoice::Edit) => { - self.merge_with(self._edit_with_editor(tpl.clone(), account)?); - continue; - } - Ok(PostEditChoice::LocalDraft) => { - printer.print_struct("Message successfully saved locally")?; - break; - } - Ok(PostEditChoice::RemoteDraft) => { - let tpl = self.to_tpl(TplOverride::default(), account)?; - let draft_folder = account - .mailboxes - .get("draft") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_DRAFT_FOLDER); - backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; - msg_utils::remove_local_draft()?; - printer - .print_struct(format!("Message successfully saved to {}", draft_folder))?; - break; - } - Ok(PostEditChoice::Discard) => { - msg_utils::remove_local_draft()?; - break; - } - Err(err) => { - println!("{}", err); - continue; - } - } - } - - Ok(backend) - } - pub fn encrypt(mut self, encrypt: bool) -> Self { self.encrypt = encrypt; self @@ -431,14 +328,15 @@ impl Msg { pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result { for path in attachments_paths { let path = shellexpand::full(path) - .context(format!(r#"cannot expand attachment path "{}""#, path))?; + .map_err(|err| Error::ExpandAttachmentPathError(err, path.to_owned()))?; let path = PathBuf::from(path.to_string()); let filename: String = path .file_name() - .ok_or_else(|| anyhow!("cannot get file name of attachment {:?}", path))? + .ok_or_else(|| Error::GetAttachmentFilenameError(path.to_owned()))? .to_string_lossy() .into(); - let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?; + let content = + fs::read(&path).map_err(|err| Error::ReadAttachmentError(err, path.to_owned()))?; let mime = tree_magic::from_u8(&content); self.parts.push(Part::Binary(BinaryPart { @@ -562,7 +460,7 @@ impl Msg { info!("begin: building message from template"); trace!("template: {:?}", tpl); - let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?; + let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).map_err(Error::ParseTplError)?; info!("end: building message from template"); Self::from_parsed_mail(parsed_mail, &AccountConfig::default()) @@ -613,10 +511,9 @@ impl Msg { for part in self.attachments() { multipart = multipart.singlepart(Attachment::new(part.filename.clone()).body( part.content, - part.mime.parse().context(format!( - "cannot parse content type of attachment {}", - part.filename - ))?, + part.mime.parse().map_err(|err| { + Error::ParseAttachmentContentTypeError(err, part.filename) + })?, )) } multipart @@ -624,16 +521,15 @@ impl Msg { if self.encrypt { let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string()); - fs::write(multipart_buffer.clone(), multipart.formatted())?; + fs::write(multipart_buffer.clone(), multipart.formatted()) + .map_err(Error::WriteTmpMultipartError)?; let addr = self .to .as_ref() .and_then(|addrs| addrs.clone().extract_single_info()) .map(|addr| addr.addr) - .ok_or_else(|| anyhow!("cannot find recipient"))?; - let encrypted_multipart = account - .pgp_encrypt_file(&addr, multipart_buffer.clone())? - .ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?; + .ok_or_else(|| Error::ParseRecipientError)?; + let encrypted_multipart = account.pgp_encrypt_file(&addr, multipart_buffer.clone())?; trace!("encrypted multipart: {:#?}", encrypted_multipart); multipart = MultiPart::encrypted(String::from("application/pgp-encrypted")) .singlepart( @@ -650,7 +546,7 @@ impl Msg { msg_builder .multipart(multipart) - .context("cannot build sendable message") + .map_err(Error::BuildSendableMsgError) } pub fn from_parsed_mail( @@ -686,24 +582,24 @@ impl Msg { } }, "from" => { - msg.from = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.from = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "to" => { - msg.to = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.to = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "reply-to" => { - msg.reply_to = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.reply_to = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "cc" => { - msg.cc = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.cc = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } "bcc" => { - msg.bcc = from_slice_to_addrs(val) - .context(format!("cannot parse header {:?}", key))? + msg.bcc = from_slice_to_addrs(&val) + .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? } key => { msg.headers.insert(key.to_lowercase(), val); @@ -712,8 +608,7 @@ impl Msg { trace!("<< parse header"); } - msg.parts = Parts::from_parsed_mail(config, &parsed_mail) - .context("cannot parsed message mime parts")?; + msg.parts = Parts::from_parsed_mail(config, &parsed_mail)?; trace!("message: {:?}", msg); info!("<< build message from parsed mail"); @@ -840,7 +735,7 @@ impl TryInto for &Msg { .as_ref() .map(from_addrs_to_sendable_addrs) .unwrap_or(Ok(vec![]))?; - Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?) + Ok(lettre::address::Envelope::new(from, to).map_err(Error::BuildEnvelopeError)?) } } diff --git a/lib/src/msg/msg_utils.rs b/lib/src/msg/msg_utils.rs new file mode 100644 index 0000000..3c61d7d --- /dev/null +++ b/lib/src/msg/msg_utils.rs @@ -0,0 +1,24 @@ +use log::{debug, trace}; +use std::{env, fs, path}; + +use crate::msg::{Error, Result}; + +pub fn local_draft_path() -> path::PathBuf { + trace!(">> get local draft path"); + + let path = env::temp_dir().join("himalaya-draft.eml"); + debug!("local draft path: {:?}", path); + + trace!("<< get local draft path"); + path +} + +pub fn remove_local_draft() -> Result<()> { + trace!(">> remove local draft"); + + let path = local_draft_path(); + fs::remove_file(&path).map_err(|err| Error::DeleteLocalDraftError(err, path))?; + + trace!("<< remove local draft"); + Ok(()) +} diff --git a/cli/src/msg/parts_entity.rs b/lib/src/msg/parts.rs similarity index 82% rename from cli/src/msg/parts_entity.rs rename to lib/src/msg/parts.rs index 36eac50..9de2bdb 100644 --- a/cli/src/msg/parts_entity.rs +++ b/lib/src/msg/parts.rs @@ -1,5 +1,3 @@ -use anyhow::{anyhow, Context, Result}; -use himalaya_lib::account::AccountConfig; use mailparse::MailHeaderMap; use serde::Serialize; use std::{ @@ -8,6 +6,8 @@ use std::{ }; use uuid::Uuid; +use crate::{account::AccountConfig, msg}; + #[derive(Debug, Clone, Default, Serialize)] pub struct TextPlainPart { pub content: String, @@ -52,7 +52,7 @@ impl Parts { pub fn from_parsed_mail<'a>( account: &'a AccountConfig, part: &'a mailparse::ParsedMail<'a>, - ) -> Result { + ) -> msg::Result { let mut parts = vec![]; if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() { @@ -83,7 +83,7 @@ fn build_parts_map_rec( account: &AccountConfig, parsed_mail: &mailparse::ParsedMail, parts: &mut Vec, -) -> Result<()> { +) -> msg::Result<()> { if parsed_mail.subparts.is_empty() { let cdisp = parsed_mail.get_content_disposition(); match cdisp.disposition { @@ -117,16 +117,15 @@ fn build_parts_map_rec( let ctype = parsed_mail .get_headers() .get_first_value("content-type") - .ok_or_else(|| anyhow!("cannot get content type of multipart"))?; + .ok_or_else(|| msg::Error::GetMultipartContentTypeError)?; if ctype.starts_with("multipart/encrypted") { let decrypted_part = parsed_mail .subparts .get(1) - .ok_or_else(|| anyhow!("cannot find encrypted part of multipart")) - .and_then(|part| decrypt_part(account, part)) - .context("cannot decrypt part of multipart")?; + .ok_or_else(|| msg::Error::GetEncryptedPartMultipartError) + .and_then(|part| decrypt_part(account, part))?; let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes()) - .context("cannot parse decrypted part of multipart")?; + .map_err(msg::Error::ParseEncryptedPartError)?; build_parts_map_rec(account, &parsed_mail, parts)?; } else { for part in parsed_mail.subparts.iter() { @@ -138,14 +137,14 @@ fn build_parts_map_rec( Ok(()) } -fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result { +fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> msg::Result { let msg_path = env::temp_dir().join(Uuid::new_v4().to_string()); let msg_body = msg .get_body() - .context("cannot get body from encrypted part")?; - fs::write(msg_path.clone(), &msg_body) - .context(format!("cannot write encrypted part to temporary file"))?; - account - .pgp_decrypt_file(msg_path.clone())? - .ok_or_else(|| anyhow!("cannot find pgp decrypt command in config")) + .map_err(msg::Error::GetEncryptedPartBodyError)?; + fs::write(msg_path.clone(), &msg_body).map_err(msg::Error::WriteEncryptedPartBodyError)?; + let content = account + .pgp_decrypt_file(msg_path.clone()) + .map_err(msg::Error::DecryptPartError)?; + Ok(content) } diff --git a/lib/src/msg/tpl.rs b/lib/src/msg/tpl.rs new file mode 100644 index 0000000..b7ba08a --- /dev/null +++ b/lib/src/msg/tpl.rs @@ -0,0 +1,15 @@ +//! Module related to message template CLI. +//! +//! This module provides subcommands, arguments and a command matcher related to message template. + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct TplOverride<'a> { + pub subject: Option<&'a str>, + pub from: Option>, + pub to: Option>, + pub cc: Option>, + pub bcc: Option>, + pub headers: Option>, + pub body: Option<&'a str>, + pub sig: Option<&'a str>, +} diff --git a/lib/src/process.rs b/lib/src/process.rs index 3f19104..36bc526 100644 --- a/lib/src/process.rs +++ b/lib/src/process.rs @@ -1,25 +1,30 @@ -use log::debug; -use std::{io, process::Command, result, string}; +use log::{debug, trace}; +use std::{io, process, result, string}; use thiserror::Error; #[derive(Error, Debug)] -pub enum ProcessError { - #[error("cannot run command")] - RunCmdError(#[from] io::Error), +pub enum Error { + #[error("cannot run command: {1}")] + RunCmdError(#[source] io::Error, String), #[error("cannot parse command output")] - ParseCmdOutputError(#[from] string::FromUtf8Error), + ParseCmdOutputError(#[source] string::FromUtf8Error), } -type Result = result::Result; +pub type Result = result::Result; pub fn run_cmd(cmd: &str) -> Result { - debug!("running command: {}", cmd); + trace!(">> run command"); + debug!("command: {}", cmd); let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output() + process::Command::new("cmd").args(&["/C", cmd]).output() } else { - Command::new("sh").arg("-c").arg(cmd).output() - }?; + process::Command::new("sh").arg("-c").arg(cmd).output() + }; + let output = output.map_err(|err| Error::RunCmdError(err, cmd.to_string()))?; + let output = String::from_utf8(output.stdout).map_err(Error::ParseCmdOutputError)?; - Ok(String::from_utf8(output.stdout)?) + debug!("command output: {}", output); + trace!("<< run command"); + Ok(output) } diff --git a/tests/emails/alice-to-patrick-encrypted.eml b/lib/tests/emails/alice-to-patrick-encrypted.eml similarity index 100% rename from tests/emails/alice-to-patrick-encrypted.eml rename to lib/tests/emails/alice-to-patrick-encrypted.eml diff --git a/tests/emails/alice-to-patrick.eml b/lib/tests/emails/alice-to-patrick.eml similarity index 100% rename from tests/emails/alice-to-patrick.eml rename to lib/tests/emails/alice-to-patrick.eml diff --git a/tests/keys/alice.asc b/lib/tests/keys/alice.asc similarity index 100% rename from tests/keys/alice.asc rename to lib/tests/keys/alice.asc diff --git a/tests/keys/alice.pub.asc b/lib/tests/keys/alice.pub.asc similarity index 100% rename from tests/keys/alice.pub.asc rename to lib/tests/keys/alice.pub.asc diff --git a/tests/keys/patrick.asc b/lib/tests/keys/patrick.asc similarity index 100% rename from tests/keys/patrick.asc rename to lib/tests/keys/patrick.asc diff --git a/tests/keys/patrick.pub.asc b/lib/tests/keys/patrick.pub.asc similarity index 100% rename from tests/keys/patrick.pub.asc rename to lib/tests/keys/patrick.pub.asc diff --git a/tests/test_imap_backend.rs b/lib/tests/test_imap_backend.rs similarity index 84% rename from tests/test_imap_backend.rs rename to lib/tests/test_imap_backend.rs index d0d5035..3235822 100644 --- a/tests/test_imap_backend.rs +++ b/lib/tests/test_imap_backend.rs @@ -1,7 +1,7 @@ #[cfg(feature = "imap-backend")] -use himalaya::{ - backends::{Backend, ImapBackend, ImapEnvelopes}, - config::{AccountConfig, ImapBackendConfig}, +use himalaya_lib::{ + account::{AccountConfig, ImapBackendConfig}, + backend::{Backend, ImapBackend}, }; #[cfg(feature = "imap-backend")] @@ -46,7 +46,6 @@ fn test_imap_backend() { // check that the envelope of the added message exists let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); let envelope = envelopes.first().unwrap(); assert_eq!("alice@localhost", envelope.sender); @@ -56,20 +55,16 @@ fn test_imap_backend() { imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) .unwrap(); let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); // check that the message can be moved imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) .unwrap(); let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(0, envelopes.len()); let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); - let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(2, envelopes.len()); let id = envelopes.first().unwrap().id.to_string(); diff --git a/tests/test_maildir_backend.rs b/lib/tests/test_maildir_backend.rs similarity index 66% rename from tests/test_maildir_backend.rs rename to lib/tests/test_maildir_backend.rs index 11eaa52..9a9e22a 100644 --- a/tests/test_maildir_backend.rs +++ b/lib/tests/test_maildir_backend.rs @@ -1,9 +1,10 @@ use maildir::Maildir; use std::{collections::HashMap, env, fs, iter::FromIterator}; -use himalaya::{ - backends::{Backend, MaildirBackend, MaildirEnvelopes, MaildirFlag}, - config::{AccountConfig, MaildirBackendConfig}, +use himalaya_lib::{ + account::{AccountConfig, MaildirBackendConfig}, + backend::{Backend, MaildirBackend}, + msg::Flag, }; #[test] @@ -33,7 +34,7 @@ fn test_maildir_backend() { // check that a message can be added let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let hash = mdir.add_msg("inbox", msg, "seen").unwrap().to_string(); + let hash = mdir.add_msg("inbox", msg, "seen").unwrap(); // check that the added message exists let msg = mdir.get_msg("inbox", &hash).unwrap(); @@ -43,48 +44,42 @@ fn test_maildir_backend() { // check that the envelope of the added message exists let envelopes = mdir.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert_eq!(1, envelopes.len()); assert_eq!("alice@localhost", envelope.sender); assert_eq!("Plain message", envelope.subject); // check that a flag can be added to the message - mdir.add_flags("inbox", &envelope.hash, "flagged passed") - .unwrap(); + mdir.add_flags("inbox", &envelope.id, "flagged").unwrap(); let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&MaildirFlag::Seen)); - assert!(envelope.flags.contains(&MaildirFlag::Flagged)); - assert!(envelope.flags.contains(&MaildirFlag::Passed)); + assert!(envelope.flags.contains(&Flag::Seen)); + assert!(envelope.flags.contains(&Flag::Flagged)); // check that the message flags can be changed - mdir.set_flags("inbox", &envelope.hash, "passed").unwrap(); + mdir.set_flags("inbox", &envelope.id, "answered").unwrap(); let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(!envelope.flags.contains(&MaildirFlag::Seen)); - assert!(!envelope.flags.contains(&MaildirFlag::Flagged)); - assert!(envelope.flags.contains(&MaildirFlag::Passed)); + assert!(!envelope.flags.contains(&Flag::Seen)); + assert!(!envelope.flags.contains(&Flag::Flagged)); + assert!(envelope.flags.contains(&Flag::Answered)); // check that a flag can be removed from the message - mdir.del_flags("inbox", &envelope.hash, "passed").unwrap(); + mdir.del_flags("inbox", &envelope.id, "answered").unwrap(); let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(!envelope.flags.contains(&MaildirFlag::Seen)); - assert!(!envelope.flags.contains(&MaildirFlag::Flagged)); - assert!(!envelope.flags.contains(&MaildirFlag::Passed)); + assert!(!envelope.flags.contains(&Flag::Seen)); + assert!(!envelope.flags.contains(&Flag::Flagged)); + assert!(!envelope.flags.contains(&Flag::Answered)); // check that the message can be copied - mdir.copy_msg("inbox", "subdir", &envelope.hash).unwrap(); + mdir.copy_msg("inbox", "subdir", &envelope.id).unwrap(); assert!(mdir.get_msg("inbox", &hash).is_ok()); assert!(mdir.get_msg("subdir", &hash).is_ok()); assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); // check that the message can be moved - mdir.move_msg("inbox", "subdir", &envelope.hash).unwrap(); + mdir.move_msg("inbox", "subdir", &envelope.id).unwrap(); assert!(mdir.get_msg("inbox", &hash).is_err()); assert!(mdir.get_msg("subdir", &hash).is_ok()); assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); diff --git a/tests/test_notmuch_backend.rs b/lib/tests/test_notmuch_backend.rs similarity index 64% rename from tests/test_notmuch_backend.rs rename to lib/tests/test_notmuch_backend.rs index dbd44c5..1a886e7 100644 --- a/tests/test_notmuch_backend.rs +++ b/lib/tests/test_notmuch_backend.rs @@ -1,17 +1,17 @@ #[cfg(feature = "notmuch-backend")] use std::{collections::HashMap, env, fs, iter::FromIterator}; - #[cfg(feature = "notmuch-backend")] -use himalaya::{ - backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, +use himalaya_lib::{ + account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, + backend::{Backend, MaildirBackend, NotmuchBackend}, }; -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig} #[cfg(feature = "notmuch-backend")] #[test] fn test_notmuch_backend() { + use himalaya_lib::msg::Flag; + // set up maildir folders and notmuch database let mdir: maildir::Maildir = env::temp_dir().join("himalaya-test-notmuch").into(); if let Err(_) = fs::remove_dir_all(mdir.path()) {} @@ -44,7 +44,6 @@ fn test_notmuch_backend() { // check that the envelope of the added message exists let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert_eq!(1, envelopes.len()); assert_eq!("alice@localhost", envelope.sender); @@ -52,37 +51,34 @@ fn test_notmuch_backend() { // check that a flag can be added to the message notmuch - .add_flags("", &envelope.hash, "flagged passed") + .add_flags("", &envelope.id, "flagged answered") .unwrap(); let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&"inbox".into())); - assert!(envelope.flags.contains(&"seen".into())); - assert!(envelope.flags.contains(&"flagged".into())); - assert!(envelope.flags.contains(&"passed".into())); + assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); + assert!(envelope.flags.contains(&Flag::Custom("seen".into()))); + assert!(envelope.flags.contains(&Flag::Custom("flagged".into()))); + assert!(envelope.flags.contains(&Flag::Custom("answered".into()))); // check that the message flags can be changed notmuch - .set_flags("", &envelope.hash, "inbox passed") + .set_flags("", &envelope.id, "inbox answered") .unwrap(); let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&"inbox".into())); - assert!(!envelope.flags.contains(&"seen".into())); - assert!(!envelope.flags.contains(&"flagged".into())); - assert!(envelope.flags.contains(&"passed".into())); + assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("seen".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("flagged".into()))); + assert!(envelope.flags.contains(&Flag::Custom("answered".into()))); // check that a flag can be removed from the message - notmuch.del_flags("", &envelope.hash, "passed").unwrap(); + notmuch.del_flags("", &envelope.id, "answered").unwrap(); let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&"inbox".into())); - assert!(!envelope.flags.contains(&"seen".into())); - assert!(!envelope.flags.contains(&"flagged".into())); - assert!(!envelope.flags.contains(&"passed".into())); + assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("seen".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("flagged".into()))); + assert!(!envelope.flags.contains(&Flag::Custom("answered".into()))); // check that the message can be deleted notmuch.del_msg("", &hash).unwrap(); From c0e002ea1b0b45356333d5c9c402b97e160e8f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 27 Jun 2022 01:13:55 +0200 Subject: [PATCH 21/24] clean process and account modules (#340) --- cli/src/config/account_handlers.rs | 8 +- cli/src/main.rs | 6 +- cli/src/mbox/mbox_handlers.rs | 6 +- cli/src/msg/msg_handlers.rs | 22 +- cli/src/msg/tpl_handlers.rs | 12 +- cli/src/output/print_table.rs | 4 +- cli/src/smtp/smtp_service.rs | 12 +- cli/src/ui/editor.rs | 6 +- cli/src/ui/table.rs | 10 +- lib/src/account/account_config.rs | 192 ++++++++++++------ .../account/deserialized_account_config.rs | 12 +- lib/src/account/deserialized_config.rs | 14 +- lib/src/account/format.rs | 23 --- lib/src/account/hooks.rs | 7 - lib/src/account/mod.rs | 20 +- lib/src/backend/backend.rs | 2 +- lib/src/backend/imap/error.rs | 2 +- lib/src/backend/imap/imap_backend.rs | 10 +- lib/src/backend/maildir/maildir_backend.rs | 9 +- lib/src/backend/notmuch/notmuch_backend.rs | 6 +- lib/src/msg/error.rs | 4 +- lib/src/msg/msg.rs | 26 +-- lib/src/msg/parts.rs | 8 +- lib/src/process.rs | 32 +-- lib/tests/test_imap_backend.rs | 6 +- lib/tests/test_maildir_backend.rs | 6 +- lib/tests/test_notmuch_backend.rs | 2 +- 27 files changed, 251 insertions(+), 216 deletions(-) delete mode 100644 lib/src/account/format.rs delete mode 100644 lib/src/account/hooks.rs diff --git a/cli/src/config/account_handlers.rs b/cli/src/config/account_handlers.rs index 80059d9..4e0e082 100644 --- a/cli/src/config/account_handlers.rs +++ b/cli/src/config/account_handlers.rs @@ -3,7 +3,7 @@ //! This module gathers all account actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::account::{AccountConfig, DeserializedConfig}; +use himalaya_lib::account::{Account, DeserializedConfig}; use log::{info, trace}; use crate::{ @@ -15,7 +15,7 @@ use crate::{ pub fn list<'a, P: PrinterService>( max_width: Option, config: &DeserializedConfig, - account_config: &AccountConfig, + account_config: &Account, printer: &mut P, ) -> Result<()> { info!(">> account list handler"); @@ -38,7 +38,7 @@ pub fn list<'a, P: PrinterService>( #[cfg(test)] mod tests { use himalaya_lib::account::{ - AccountConfig, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig, + Account, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig, }; use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator}; use termcolor::ColorSpec; @@ -122,7 +122,7 @@ mod tests { ..DeserializedConfig::default() }; - let account_config = AccountConfig::default(); + let account_config = Account::default(); let mut printer = PrinterServiceTest::default(); assert!(list(None, &config, &account_config, &mut printer).is_ok()); diff --git a/cli/src/main.rs b/cli/src/main.rs index b3f834c..e81924f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use himalaya_lib::{ - account::{AccountConfig, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER}, + account::{Account, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER}, backend::Backend, }; use std::{convert::TryFrom, env}; @@ -58,7 +58,7 @@ fn main() -> Result<()> { if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { let config = DeserializedConfig::from_opt_path(None)?; let (account_config, backend_config) = - AccountConfig::from_config_and_opt_account_name(&config, None)?; + Account::from_config_and_opt_account_name(&config, None)?; let mut printer = StdoutPrinter::from(OutputFmt::Plain); let url = Url::parse(&raw_args[1])?; let mut smtp = LettreService::from(&account_config); @@ -114,7 +114,7 @@ fn main() -> Result<()> { // Init entities and services. let config = DeserializedConfig::from_opt_path(m.value_of("config"))?; let (account_config, backend_config) = - AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?; + Account::from_config_and_opt_account_name(&config, m.value_of("account"))?; let mbox = m .value_of("mbox-source") .or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str())) diff --git a/cli/src/mbox/mbox_handlers.rs b/cli/src/mbox/mbox_handlers.rs index c4d50ac..b5a9aa9 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/cli/src/mbox/mbox_handlers.rs @@ -3,7 +3,7 @@ //! This module gathers all mailbox actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::{account::AccountConfig, backend::Backend}; +use himalaya_lib::{account::Account, backend::Backend}; use log::{info, trace}; use crate::output::{PrintTableOpts, PrinterService}; @@ -11,7 +11,7 @@ use crate::output::{PrintTableOpts, PrinterService}; /// Lists all mailboxes. pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( max_width: Option, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -170,7 +170,7 @@ mod tests { } } - let config = AccountConfig::default(); + let config = Account::default(); let mut printer = PrinterServiceTest::default(); let mut backend = TestBackend {}; let backend = Box::new(&mut backend); diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index b18c2c4..4041fdf 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result}; use atty::Stream; use himalaya_lib::{ - account::{AccountConfig, DEFAULT_SENT_FOLDER}, + account::{Account, DEFAULT_SENT_FOLDER}, backend::Backend, msg::{Msg, Part, Parts, TextPlainPart, TplOverride}, }; @@ -28,7 +28,7 @@ use crate::{ pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -92,7 +92,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( attachments_paths: Vec<&str>, encrypt: bool, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, @@ -112,7 +112,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( page_size: Option, page: usize, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, imap: Box<&'a mut B>, ) -> Result<()> { @@ -134,7 +134,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// [mailto]: https://en.wikipedia.org/wiki/Mailto pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( url: &Url, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, @@ -212,7 +212,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( raw: bool, headers: Vec<&str>, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -233,7 +233,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( attachments_paths: Vec<&str>, encrypt: bool, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, @@ -285,7 +285,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( page_size: Option, page: usize, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -310,7 +310,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( page_size: Option, page: usize, mbox: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -330,7 +330,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Send a raw message. pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( raw_msg: &str, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&mut B>, smtp: &mut S, @@ -371,7 +371,7 @@ pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( tpl: TplOverride, attachments_paths: Vec<&str>, encrypt: bool, - config: &AccountConfig, + config: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, diff --git a/cli/src/msg/tpl_handlers.rs b/cli/src/msg/tpl_handlers.rs index 8d8ce14..558e76a 100644 --- a/cli/src/msg/tpl_handlers.rs +++ b/cli/src/msg/tpl_handlers.rs @@ -5,7 +5,7 @@ use anyhow::Result; use atty::Stream; use himalaya_lib::{ - account::AccountConfig, + account::Account, backend::Backend, msg::{Msg, TplOverride}, }; @@ -16,7 +16,7 @@ use crate::{output::PrinterService, smtp::SmtpService}; /// Generate a new message template. pub fn new<'a, P: PrinterService>( opts: TplOverride<'a>, - account: &'a AccountConfig, + account: &'a Account, printer: &'a mut P, ) -> Result<()> { let tpl = Msg::default().to_tpl(opts, account)?; @@ -29,7 +29,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>( all: bool, opts: TplOverride<'a>, mbox: &str, - config: &'a AccountConfig, + config: &'a Account, printer: &'a mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -45,7 +45,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, opts: TplOverride<'a>, mbox: &str, - config: &'a AccountConfig, + config: &'a Account, printer: &'a mut P, backend: Box<&'a mut B>, ) -> Result<()> { @@ -59,7 +59,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Saves a message based on a template. pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( mbox: &str, - config: &AccountConfig, + config: &Account, attachments_paths: Vec<&str>, tpl: &str, printer: &mut P, @@ -84,7 +84,7 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Sends a message based on a template. pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( mbox: &str, - account: &AccountConfig, + account: &Account, attachments_paths: Vec<&str>, tpl: &str, printer: &mut P, diff --git a/cli/src/output/print_table.rs b/cli/src/output/print_table.rs index 007c2da..a99e316 100644 --- a/cli/src/output/print_table.rs +++ b/cli/src/output/print_table.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use himalaya_lib::account::Format; +use himalaya_lib::account::TextPlainFormat; use std::io; use termcolor::{self, StandardStream}; @@ -12,6 +12,6 @@ pub trait PrintTable { } pub struct PrintTableOpts<'a> { - pub format: &'a Format, + pub format: &'a TextPlainFormat, pub max_width: Option, } diff --git a/cli/src/smtp/smtp_service.rs b/cli/src/smtp/smtp_service.rs index c5c788d..7a9db3b 100644 --- a/cli/src/smtp/smtp_service.rs +++ b/cli/src/smtp/smtp_service.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use himalaya_lib::{account::AccountConfig, msg::Msg}; +use himalaya_lib::{account::Account, msg::Msg}; use lettre::{ self, transport::smtp::{ @@ -13,11 +13,11 @@ use std::convert::TryInto; use crate::output::pipe_cmd; pub trait SmtpService { - fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; + fn send(&mut self, account: &Account, msg: &Msg) -> Result>; } pub struct LettreService<'a> { - account: &'a AccountConfig, + account: &'a Account, transport: Option, } @@ -56,7 +56,7 @@ impl LettreService<'_> { } impl SmtpService for LettreService<'_> { - fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result> { + fn send(&mut self, account: &Account, msg: &Msg) -> Result> { let mut raw_msg = msg.into_sendable_msg(account)?.formatted(); let envelope: lettre::address::Envelope = @@ -76,8 +76,8 @@ impl SmtpService for LettreService<'_> { } } -impl<'a> From<&'a AccountConfig> for LettreService<'a> { - fn from(account: &'a AccountConfig) -> Self { +impl<'a> From<&'a Account> for LettreService<'a> { + fn from(account: &'a Account) -> Self { Self { account, transport: None, diff --git a/cli/src/ui/editor.rs b/cli/src/ui/editor.rs index f940cef..613f6c5 100644 --- a/cli/src/ui/editor.rs +++ b/cli/src/ui/editor.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use himalaya_lib::{ - account::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER}, + account::{Account, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER}, backend::Backend, msg::{local_draft_path, remove_local_draft, Msg, TplOverride}, }; @@ -39,7 +39,7 @@ pub fn open_with_draft() -> Result { open_with_tpl(tpl) } -fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &AccountConfig) -> Result { +fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &Account) -> Result { let tpl = msg.to_tpl(tpl, account)?; let tpl = open_with_tpl(tpl)?; Msg::from_tpl(&tpl).context("cannot parse message from template") @@ -48,7 +48,7 @@ fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &AccountConfig) - pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( mut msg: Msg, tpl: TplOverride, - account: &AccountConfig, + account: &Account, printer: &mut P, backend: Box<&'a mut B>, smtp: &mut S, diff --git a/cli/src/ui/table.rs b/cli/src/ui/table.rs index 2e84209..82fef6f 100644 --- a/cli/src/ui/table.rs +++ b/cli/src/ui/table.rs @@ -5,7 +5,7 @@ //! [builder design pattern]: https://refactoring.guru/design-patterns/builder use anyhow::{Context, Result}; -use himalaya_lib::account::Format; +use himalaya_lib::account::TextPlainFormat; use log::trace; use termcolor::{Color, ColorSpec}; use terminal_size; @@ -169,11 +169,11 @@ where /// 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 is_format_flowed = matches!(opts.format, TextPlainFormat::Flowed); let max_width = match opts.format { - Format::Fixed(width) => opts.max_width.unwrap_or(*width), - Format::Flowed => 0, - Format::Auto => opts + TextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width), + TextPlainFormat::Flowed => 0, + TextPlainFormat::Auto => opts .max_width .or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize)) .unwrap_or(DEFAULT_TERM_WIDTH), diff --git a/lib/src/account/account_config.rs b/lib/src/account/account_config.rs index 7229bb9..15e3dc9 100644 --- a/lib/src/account/account_config.rs +++ b/lib/src/account/account_config.rs @@ -1,52 +1,78 @@ +//! Account config module. +//! +//! This module contains the representation of the user account. + use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; use log::{debug, info, trace}; use mailparse::MailAddr; +use serde::Deserialize; use shellexpand; -use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf, result}; +use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; use thiserror::Error; -use crate::process; +use crate::process::{self, ProcessError}; use super::*; -#[derive(Error, Debug)] -pub enum Error { - #[error("cannot run encrypt file command")] - RunEncryptFileCmdError(#[source] process::Error), - #[error("cannot find pgp encrypt file command from config")] - FindPgpEncryptFileCmdError, - #[error("cannot find pgp decrypt file command from config")] - FindPgpDecryptFileCmdError, +pub const DEFAULT_PAGE_SIZE: usize = 10; +pub const DEFAULT_SIG_DELIM: &str = "-- \n"; + +pub const DEFAULT_INBOX_FOLDER: &str = "INBOX"; +pub const DEFAULT_SENT_FOLDER: &str = "Sent"; +pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; + +#[derive(Debug, Error)] +pub enum AccountError { + #[error("cannot encrypt file using pgp")] + EncryptFileError(#[source] ProcessError), + #[error("cannot find encrypt file command from config file")] + EncryptFileMissingCmdError, + + #[error("cannot decrypt file using pgp")] + DecryptFileError(#[source] ProcessError), + #[error("cannot find decrypt file command from config file")] + DecryptFileMissingCmdError, + + #[error("cannot get smtp password")] + GetSmtpPasswdError(#[source] ProcessError), + #[error("cannot get smtp password: password is empty")] + GetSmtpPasswdEmptyError, + + #[cfg(feature = "imap-backend")] + #[error("cannot get imap password")] + GetImapPasswdError(#[source] ProcessError), + #[cfg(feature = "imap-backend")] + #[error("cannot get imap password: password is empty")] + GetImapPasswdEmptyError, #[error("cannot find default account")] FindDefaultAccountError, - #[error("cannot find account \"{0}\"")] + #[error("cannot find account {0}")] FindAccountError(String), - #[error("cannot shell expand")] - ShellExpandError(#[from] shellexpand::LookupError), - #[error("cannot parse account address")] - ParseAccountAddressError(#[from] mailparse::MailParseError), - #[error("cannot find account address from \"{0}\"")] - FindAccountAddressError(String), - #[error("cannot parse download file name from \"{0}\"")] - ParseDownloadFileNameError(PathBuf), - #[error("cannot find password")] - FindPasswordError, - #[error("cannot get smtp password")] - GetSmtpPasswdError(#[source] process::Error), - #[error("cannot get imap password")] - GetImapPasswdError(#[source] process::Error), - #[error("cannot decrypt pgp file")] - DecryptPgpFileError(#[source] process::Error), - #[error("cannot run notify command")] - RunNotifyCmdError(#[source] process::Error), -} + #[error("cannot parse account address {0}")] + ParseAccountAddrError(#[source] mailparse::MailParseError, String), + #[error("cannot find account address in {0}")] + ParseAccountAddrNotFoundError(String), -pub type Result = result::Result; + #[cfg(feature = "maildir-backend")] + #[error("cannot expand maildir path")] + ExpandMaildirPathError(#[source] shellexpand::LookupError), + #[cfg(feature = "notmuch-backend")] + #[error("cannot expand notmuch path")] + ExpandNotmuchDatabasePathError(#[source] shellexpand::LookupError), + #[error("cannot expand mailbox alias {1}")] + ExpandMboxAliasError(#[source] shellexpand::LookupError, String), + + #[error("cannot parse download file name from {0}")] + ParseDownloadFileNameError(PathBuf), + + #[error("cannot start the notify mode")] + StartNotifyModeError(#[source] ProcessError), +} /// Represents the user account. #[derive(Debug, Default, Clone)] -pub struct AccountConfig { +pub struct Account { /// Represents the name of the user account. pub name: String, /// Makes this account the default one. @@ -69,7 +95,7 @@ pub struct AccountConfig { pub watch_cmds: Vec, /// Represents the text/plain format as defined in the /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) - pub format: Format, + pub format: TextPlainFormat, /// Overrides the default headers displayed at the top of /// the read message. pub read_headers: Vec, @@ -99,13 +125,13 @@ pub struct AccountConfig { pub pgp_decrypt_cmd: Option, } -impl<'a> AccountConfig { +impl<'a> Account { /// Tries to create an account from a config and an optional /// account name. pub fn from_config_and_opt_account_name( config: &'a DeserializedConfig, account_name: Option<&str>, - ) -> Result<(AccountConfig, BackendConfig)> { + ) -> Result<(Account, BackendConfig), AccountError> { info!("begin: parsing account and backend configs from config and account name"); debug!("account name: {:?}", account_name.unwrap_or("default")); @@ -126,12 +152,12 @@ impl<'a> AccountConfig { } }) .map(|(name, account)| (name.to_owned(), account)) - .ok_or_else(|| Error::FindDefaultAccountError), + .ok_or_else(|| AccountError::FindDefaultAccountError), Some(name) => config .accounts .get(name) .map(|account| (name.to_owned(), account)) - .ok_or_else(|| Error::FindAccountError(name.to_owned())), + .ok_or_else(|| AccountError::FindAccountError(name.to_owned())), }?; let base_account = account.to_base(); @@ -175,7 +201,7 @@ impl<'a> AccountConfig { .or_else(|| sig.map(|sig| sig.to_owned())) .map(|sig| format!("{}{}", sig_delim, sig.trim_end())); - let account_config = AccountConfig { + let account_config = Account { name, display_name: base_account .name @@ -234,13 +260,17 @@ impl<'a> AccountConfig { #[cfg(feature = "maildir-backend")] DeserializedAccountConfig::Maildir(config) => { BackendConfig::Maildir(MaildirBackendConfig { - maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(), + maildir_dir: shellexpand::full(&config.maildir_dir) + .map_err(AccountError::ExpandMaildirPathError)? + .to_string() + .into(), }) } #[cfg(feature = "notmuch-backend")] DeserializedAccountConfig::Notmuch(config) => { BackendConfig::Notmuch(NotmuchBackendConfig { - notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)? + notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir) + .map_err(AccountError::ExpandNotmuchDatabasePathError)? .to_string() .into(), }) @@ -253,7 +283,7 @@ impl<'a> AccountConfig { } /// Builds the full RFC822 compliant address of the user account. - pub fn address(&self) -> Result { + pub fn address(&self) -> Result { let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c)); let addr = if self.display_name.is_empty() { self.email.clone() @@ -264,19 +294,21 @@ impl<'a> AccountConfig { format!("{} <{}>", self.display_name, self.email) }; - Ok(mailparse::addrparse(&addr)? + Ok(mailparse::addrparse(&addr) + .map_err(|err| AccountError::ParseAccountAddrError(err, addr.to_owned()))? .first() - .ok_or_else(|| Error::FindAccountAddressError(addr.into()))? + .ok_or_else(|| AccountError::ParseAccountAddrNotFoundError(addr.to_owned()))? .clone()) } /// Builds the user account SMTP credentials. - pub fn smtp_creds(&self) -> Result { - let passwd = process::run_cmd(&self.smtp_passwd_cmd).map_err(Error::GetSmtpPasswdError)?; + pub fn smtp_creds(&self) -> Result { + let passwd = + process::run(&self.smtp_passwd_cmd).map_err(AccountError::GetSmtpPasswdError)?; let passwd = passwd .lines() .next() - .ok_or_else(|| Error::FindPasswordError)?; + .ok_or_else(|| AccountError::GetSmtpPasswdEmptyError)?; Ok(SmtpCredentials::new( self.smtp_login.to_owned(), @@ -285,27 +317,30 @@ impl<'a> AccountConfig { } /// Encrypts a file. - pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result { + pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result { if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() { let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path); - Ok(process::run_cmd(&encrypt_file_cmd).map_err(Error::RunEncryptFileCmdError)?) + Ok(process::run(&encrypt_file_cmd).map_err(AccountError::EncryptFileError)?) } else { - Err(Error::FindPgpEncryptFileCmdError) + Err(AccountError::EncryptFileMissingCmdError) } } /// Decrypts a file. - pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result { + pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result { if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() { let decrypt_file_cmd = format!("{} {:?}", cmd, path); - Ok(process::run_cmd(&decrypt_file_cmd).map_err(Error::DecryptPgpFileError)?) + Ok(process::run(&decrypt_file_cmd).map_err(AccountError::DecryptFileError)?) } else { - Err(Error::FindPgpDecryptFileCmdError) + Err(AccountError::DecryptFileMissingCmdError) } } /// Gets the download path from a file name. - pub fn get_download_file_path>(&self, file_name: S) -> Result { + pub fn get_download_file_path>( + &self, + file_name: S, + ) -> Result { let file_path = self.downloads_dir.join(file_name.as_ref()); self.get_unique_download_file_path(&file_path, |path, _count| path.is_file()) } @@ -316,7 +351,7 @@ impl<'a> AccountConfig { &self, original_file_path: &PathBuf, is_file: impl Fn(&PathBuf, u8) -> bool, - ) -> Result { + ) -> Result { let mut count = 0; let file_ext = original_file_path .extension() @@ -332,7 +367,9 @@ impl<'a> AccountConfig { .file_stem() .and_then(OsStr::to_str) .map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) - .ok_or_else(|| Error::ParseDownloadFileNameError(file_path.to_owned()))?, + .ok_or_else(|| { + AccountError::ParseDownloadFileNameError(file_path.to_owned()) + })?, )); } @@ -340,7 +377,7 @@ impl<'a> AccountConfig { } /// Runs the notify command. - pub fn run_notify_cmd>(&self, subject: S, sender: S) -> Result<()> { + pub fn run_notify_cmd>(&self, subject: S, sender: S) -> Result<(), AccountError> { let subject = subject.as_ref(); let sender = sender.as_ref(); @@ -351,19 +388,21 @@ impl<'a> AccountConfig { .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) .unwrap_or(default_cmd); - process::run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?; + process::run(&cmd).map_err(AccountError::StartNotifyModeError)?; 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 { + 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); - let mbox = shellexpand::full(mbox).map(String::from)?; + let mbox = shellexpand::full(mbox) + .map(String::from) + .map_err(|err| AccountError::ExpandMboxAliasError(err, mbox.to_owned()))?; Ok(mbox) } } @@ -400,12 +439,13 @@ pub struct ImapBackendConfig { #[cfg(feature = "imap-backend")] impl ImapBackendConfig { /// Gets the IMAP password of the user account. - pub fn imap_passwd(&self) -> Result { - let passwd = process::run_cmd(&self.imap_passwd_cmd).map_err(Error::GetImapPasswdError)?; + pub fn imap_passwd(&self) -> Result { + let passwd = + process::run(&self.imap_passwd_cmd).map_err(AccountError::GetImapPasswdError)?; let passwd = passwd .lines() .next() - .ok_or_else(|| Error::FindPasswordError)?; + .ok_or_else(|| AccountError::GetImapPasswdEmptyError)?; Ok(passwd.to_string()) } } @@ -426,13 +466,39 @@ pub struct NotmuchBackendConfig { pub notmuch_database_dir: PathBuf, } +/// Represents the text/plain format as defined in the [RFC2646]. +/// +/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt +#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[serde(tag = "type", content = "width", rename_all = "lowercase")] +pub enum TextPlainFormat { + // Forces the content width with a fixed amount of pixels. + Fixed(usize), + // Makes the content fit the terminal. + Auto, + // Does not restrict the content. + Flowed, +} + +impl Default for TextPlainFormat { + fn default() -> Self { + Self::Auto + } +} + +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Hooks { + pub pre_send: Option, +} + #[cfg(test)] mod tests { use super::*; #[test] fn it_should_get_unique_download_file_path() { - let account = AccountConfig::default(); + let account = Account::default(); let path = PathBuf::from("downloads/file.ext"); // When file path is unique diff --git a/lib/src/account/deserialized_account_config.rs b/lib/src/account/deserialized_account_config.rs index 7539a41..8ba6278 100644 --- a/lib/src/account/deserialized_account_config.rs +++ b/lib/src/account/deserialized_account_config.rs @@ -1,7 +1,12 @@ +//! Deserialized account config module. +//! +//! This module contains the raw deserialized representation of an +//! account in the accounts section of the user configuration file. + use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; -use crate::account::{Format, Hooks}; +use super::*; pub trait ToDeserializedBaseAccountConfig { fn to_base(&self) -> DeserializedBaseAccountConfig; @@ -53,9 +58,8 @@ macro_rules! make_account_config { pub notify_query: Option, /// Overrides the watch commands for this account. pub watch_cmds: Option>, - /// Represents the text/plain format as defined in the - /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) - pub format: Option, + /// Represents the text/plain format. + pub format: Option, /// Represents the default headers displayed at the top of /// the read message. #[serde(default)] diff --git a/lib/src/account/deserialized_config.rs b/lib/src/account/deserialized_config.rs index 500d00a..cc4280e 100644 --- a/lib/src/account/deserialized_config.rs +++ b/lib/src/account/deserialized_config.rs @@ -1,17 +1,15 @@ +//! Deserialized config module. +//! +//! This module contains the raw deserialized representation of the +//! user configuration file. + use log::{debug, trace}; use serde::Deserialize; use std::{collections::HashMap, env, fs, io, path::PathBuf, result}; use thiserror::Error; use toml; -use crate::account::DeserializedAccountConfig; - -pub const DEFAULT_PAGE_SIZE: usize = 10; -pub const DEFAULT_SIG_DELIM: &str = "-- \n"; - -pub const DEFAULT_INBOX_FOLDER: &str = "INBOX"; -pub const DEFAULT_SENT_FOLDER: &str = "Sent"; -pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; +use super::*; #[derive(Error, Debug)] pub enum DeserializeConfigError { diff --git a/lib/src/account/format.rs b/lib/src/account/format.rs deleted file mode 100644 index 304b7f6..0000000 --- a/lib/src/account/format.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::Deserialize; - -/// Represents the text/plain format as defined in the [RFC2646]. The -/// format is then used by the table system to adjust the way it is -/// rendered. -/// -/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] -#[serde(tag = "type", content = "width", rename_all = "lowercase")] -pub enum Format { - // Forces the content width with a fixed amount of pixels. - Fixed(usize), - // Makes the content fit the terminal. - Auto, - // Does not restrict the content. - Flowed, -} - -impl Default for Format { - fn default() -> Self { - Self::Auto - } -} diff --git a/lib/src/account/hooks.rs b/lib/src/account/hooks.rs deleted file mode 100644 index 4bd44f0..0000000 --- a/lib/src/account/hooks.rs +++ /dev/null @@ -1,7 +0,0 @@ -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/lib/src/account/mod.rs b/lib/src/account/mod.rs index 5409db8..01a8c4d 100644 --- a/lib/src/account/mod.rs +++ b/lib/src/account/mod.rs @@ -1,16 +1,12 @@ -pub mod deserialized_config; -pub use deserialized_config::*; +//! Account module. +//! +//! This module contains everything related to the user configuration. -pub mod deserialized_account_config; -pub use deserialized_account_config::*; - -// pub mod account_handlers; - -pub mod account_config; +mod account_config; pub use account_config::*; -pub mod format; -pub use format::*; +mod deserialized_config; +pub use deserialized_config::*; -pub mod hooks; -pub use hooks::*; +mod deserialized_account_config; +pub use deserialized_account_config::*; diff --git a/lib/src/backend/backend.rs b/lib/src/backend/backend.rs index 2030347..b1d1d99 100644 --- a/lib/src/backend/backend.rs +++ b/lib/src/backend/backend.rs @@ -27,7 +27,7 @@ pub enum Error { ImapError(#[from] super::imap::Error), #[error(transparent)] - AccountError(#[from] account::Error), + AccountError(#[from] account::AccountError), #[error(transparent)] MsgError(#[from] msg::Error), diff --git a/lib/src/backend/imap/error.rs b/lib/src/backend/imap/error.rs index 76454a3..ff3b233 100644 --- a/lib/src/backend/imap/error.rs +++ b/lib/src/backend/imap/error.rs @@ -78,7 +78,7 @@ pub enum Error { LogoutError(#[source] imap::Error), #[error(transparent)] - AccountError(#[from] account::Error), + AccountError(#[from] account::AccountError), #[error(transparent)] MsgError(#[from] msg::Error), } diff --git a/lib/src/backend/imap/imap_backend.rs b/lib/src/backend/imap/imap_backend.rs index d5bcee5..eb8f2a0 100644 --- a/lib/src/backend/imap/imap_backend.rs +++ b/lib/src/backend/imap/imap_backend.rs @@ -8,26 +8,26 @@ use native_tls::{TlsConnector, TlsStream}; use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; use crate::{ - account::{AccountConfig, ImapBackendConfig}, + account::{Account, ImapBackendConfig}, backend::{ backend::Result, from_imap_fetch, from_imap_fetches, imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend, }, mbox::{Mbox, Mboxes}, msg::{Envelopes, Flags, Msg}, - process::run_cmd, + process, }; type ImapSess = imap::Session>; pub struct ImapBackend<'a> { - account_config: &'a AccountConfig, + account_config: &'a Account, imap_config: &'a ImapBackendConfig, sess: Option, } impl<'a> ImapBackend<'a> { - pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self { + pub fn new(account_config: &'a Account, imap_config: &'a ImapBackendConfig) -> Self { Self { account_config, imap_config, @@ -187,7 +187,7 @@ impl<'a> ImapBackend<'a> { debug!("batch execution of {} cmd(s)", cmds.len()); cmds.iter().for_each(|cmd| { debug!("running command {:?}…", cmd); - let res = run_cmd(cmd); + let res = process::run(cmd); debug!("{:?}", res); }) }); diff --git a/lib/src/backend/maildir/maildir_backend.rs b/lib/src/backend/maildir/maildir_backend.rs index c031246..2c50328 100644 --- a/lib/src/backend/maildir/maildir_backend.rs +++ b/lib/src/backend/maildir/maildir_backend.rs @@ -7,7 +7,7 @@ use log::{debug, info, trace}; use std::{env, ffi::OsStr, fs, path::PathBuf}; use crate::{ - account::{AccountConfig, MaildirBackendConfig}, + account::{Account, MaildirBackendConfig}, backend::{backend::Result, maildir_envelopes, maildir_flags, Backend, IdMapper}, mbox::{Mbox, Mboxes}, msg::{Envelopes, Flags, Msg}, @@ -17,15 +17,12 @@ use super::MaildirError; /// Represents the maildir backend. pub struct MaildirBackend<'a> { - account_config: &'a AccountConfig, + account_config: &'a Account, mdir: maildir::Maildir, } impl<'a> MaildirBackend<'a> { - pub fn new( - account_config: &'a AccountConfig, - maildir_config: &'a MaildirBackendConfig, - ) -> Self { + pub fn new(account_config: &'a Account, maildir_config: &'a MaildirBackendConfig) -> Self { Self { account_config, mdir: maildir_config.maildir_dir.clone().into(), diff --git a/lib/src/backend/notmuch/notmuch_backend.rs b/lib/src/backend/notmuch/notmuch_backend.rs index 8918c9b..e249d46 100644 --- a/lib/src/backend/notmuch/notmuch_backend.rs +++ b/lib/src/backend/notmuch/notmuch_backend.rs @@ -2,7 +2,7 @@ use log::{debug, info, trace}; use std::fs; use crate::{ - account::{AccountConfig, NotmuchBackendConfig}, + account::{Account, NotmuchBackendConfig}, backend::{ backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError, }, @@ -12,7 +12,7 @@ use crate::{ /// Represents the Notmuch backend. pub struct NotmuchBackend<'a> { - account_config: &'a AccountConfig, + account_config: &'a Account, notmuch_config: &'a NotmuchBackendConfig, pub mdir: &'a mut MaildirBackend<'a>, db: notmuch::Database, @@ -20,7 +20,7 @@ pub struct NotmuchBackend<'a> { impl<'a> NotmuchBackend<'a> { pub fn new( - account_config: &'a AccountConfig, + account_config: &'a Account, notmuch_config: &'a NotmuchBackendConfig, mdir: &'a mut MaildirBackend<'a>, ) -> Result> { diff --git a/lib/src/msg/error.rs b/lib/src/msg/error.rs index 0cf689f..33d4473 100644 --- a/lib/src/msg/error.rs +++ b/lib/src/msg/error.rs @@ -34,7 +34,7 @@ pub enum Error { ParseAddressError(#[from] lettre::address::AddressError), #[error(transparent)] - AccountError(#[from] account::Error), + AccountError(#[from] account::AccountError), #[error("cannot get content type of multipart")] GetMultipartContentTypeError, @@ -47,7 +47,7 @@ pub enum Error { #[error("cannot write encrypted part to temporary file")] WriteEncryptedPartBodyError(#[source] io::Error), #[error("cannot write encrypted part to temporary file")] - DecryptPartError(#[source] account::Error), + DecryptPartError(#[source] account::AccountError), #[error("cannot delete local draft: {1}")] DeleteLocalDraftError(#[source] io::Error, path::PathBuf), diff --git a/lib/src/msg/msg.rs b/lib/src/msg/msg.rs index 64f04f8..264624c 100644 --- a/lib/src/msg/msg.rs +++ b/lib/src/msg/msg.rs @@ -17,7 +17,7 @@ use tree_magic; use uuid::Uuid; use crate::{ - account::{AccountConfig, DEFAULT_SIG_DELIM}, + account::{Account, DEFAULT_SIG_DELIM}, msg::{ from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr, Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride, @@ -166,7 +166,7 @@ impl Msg { } } - pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result { + pub fn into_reply(mut self, all: bool, account: &Account) -> Result { let account_addr = account.address()?; // In-Reply-To @@ -264,7 +264,7 @@ impl Msg { Ok(self) } - pub fn into_forward(mut self, account: &AccountConfig) -> Result { + pub fn into_forward(mut self, account: &Account) -> Result { let account_addr = account.address()?; let prev_subject = self.subject.to_owned(); @@ -380,7 +380,7 @@ impl Msg { } } - pub fn to_tpl(&self, opts: TplOverride, account: &AccountConfig) -> Result { + pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> Result { let account_addr: Addrs = vec![account.address()?].into(); let mut tpl = String::default(); @@ -463,10 +463,10 @@ impl Msg { let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).map_err(Error::ParseTplError)?; info!("end: building message from template"); - Self::from_parsed_mail(parsed_mail, &AccountConfig::default()) + Self::from_parsed_mail(parsed_mail, &Account::default()) } - pub fn into_sendable_msg(&self, account: &AccountConfig) -> Result { + pub fn into_sendable_msg(&self, account: &Account) -> Result { let mut msg_builder = lettre::Message::builder() .message_id(self.message_id.to_owned()) .subject(self.subject.to_owned()); @@ -551,7 +551,7 @@ impl Msg { pub fn from_parsed_mail( parsed_mail: mailparse::ParsedMail<'_>, - config: &AccountConfig, + config: &Account, ) -> Result { trace!(">> build message from parsed mail"); trace!("parsed mail: {:?}", parsed_mail); @@ -623,7 +623,7 @@ impl Msg { &self, text_mime: &str, headers: Vec<&str>, - config: &AccountConfig, + config: &Account, ) -> Result { let mut all_headers = vec![]; for h in config.read_headers.iter() { @@ -750,10 +750,10 @@ mod tests { #[test] fn test_into_reply() { - let config = AccountConfig { + let config = Account { display_name: "Test".into(), email: "test-account@local".into(), - ..AccountConfig::default() + ..Account::default() }; // Checks that: @@ -889,7 +889,7 @@ mod tests { #[test] fn test_to_readable() { - let config = AccountConfig::default(); + let config = Account::default(); let msg = Msg { parts: Parts(vec![Part::TextPlain(TextPlainPart { content: String::from("hello, world!"), @@ -952,14 +952,14 @@ mod tests { .unwrap() ); - let config = AccountConfig { + let config = Account { read_headers: vec![ "CusTOM-heaDER".into(), "Subject".into(), "from".into(), "cc".into(), ], - ..AccountConfig::default() + ..Account::default() }; // header present but empty in msg headers, empty config assert_eq!( diff --git a/lib/src/msg/parts.rs b/lib/src/msg/parts.rs index 9de2bdb..b64c01a 100644 --- a/lib/src/msg/parts.rs +++ b/lib/src/msg/parts.rs @@ -6,7 +6,7 @@ use std::{ }; use uuid::Uuid; -use crate::{account::AccountConfig, msg}; +use crate::{account::Account, msg}; #[derive(Debug, Clone, Default, Serialize)] pub struct TextPlainPart { @@ -50,7 +50,7 @@ impl Parts { } pub fn from_parsed_mail<'a>( - account: &'a AccountConfig, + account: &'a Account, part: &'a mailparse::ParsedMail<'a>, ) -> msg::Result { let mut parts = vec![]; @@ -80,7 +80,7 @@ impl DerefMut for Parts { } fn build_parts_map_rec( - account: &AccountConfig, + account: &Account, parsed_mail: &mailparse::ParsedMail, parts: &mut Vec, ) -> msg::Result<()> { @@ -137,7 +137,7 @@ fn build_parts_map_rec( Ok(()) } -fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> msg::Result { +fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> msg::Result { let msg_path = env::temp_dir().join(Uuid::new_v4().to_string()); let msg_body = msg .get_body() diff --git a/lib/src/process.rs b/lib/src/process.rs index 36bc526..c9e282c 100644 --- a/lib/src/process.rs +++ b/lib/src/process.rs @@ -1,30 +1,34 @@ +//! Process module. +//! +//! This module contains cross platform helpers around the +//! `std::process` crate. + use log::{debug, trace}; -use std::{io, process, result, string}; +use std::{io, process::Command, string}; use thiserror::Error; -#[derive(Error, Debug)] -pub enum Error { - #[error("cannot run command: {1}")] +#[derive(Debug, Error)] +pub enum ProcessError { + #[error("cannot run command {1:?}")] RunCmdError(#[source] io::Error, String), + #[error("cannot parse command output")] ParseCmdOutputError(#[source] string::FromUtf8Error), } -pub type Result = result::Result; - -pub fn run_cmd(cmd: &str) -> Result { - trace!(">> run command"); +pub fn run(cmd: &str) -> Result { + debug!(">> run command"); debug!("command: {}", cmd); let output = if cfg!(target_os = "windows") { - process::Command::new("cmd").args(&["/C", cmd]).output() + Command::new("cmd").args(&["/C", cmd]).output() } else { - process::Command::new("sh").arg("-c").arg(cmd).output() + Command::new("sh").arg("-c").arg(cmd).output() }; - let output = output.map_err(|err| Error::RunCmdError(err, cmd.to_string()))?; - let output = String::from_utf8(output.stdout).map_err(Error::ParseCmdOutputError)?; + let output = output.map_err(|err| ProcessError::RunCmdError(err, cmd.to_string()))?; + let output = String::from_utf8(output.stdout).map_err(ProcessError::ParseCmdOutputError)?; - debug!("command output: {}", output); - trace!("<< run command"); + trace!("command output: {}", output); + debug!("<< run command"); Ok(output) } diff --git a/lib/tests/test_imap_backend.rs b/lib/tests/test_imap_backend.rs index 3235822..081d045 100644 --- a/lib/tests/test_imap_backend.rs +++ b/lib/tests/test_imap_backend.rs @@ -1,6 +1,6 @@ #[cfg(feature = "imap-backend")] use himalaya_lib::{ - account::{AccountConfig, ImapBackendConfig}, + account::{Account, ImapBackendConfig}, backend::{Backend, ImapBackend}, }; @@ -8,14 +8,14 @@ use himalaya_lib::{ #[test] fn test_imap_backend() { // configure accounts - let account_config = AccountConfig { + let account_config = Account { smtp_host: "localhost".into(), smtp_port: 3465, smtp_starttls: false, smtp_insecure: true, smtp_login: "inbox@localhost".into(), smtp_passwd_cmd: "echo 'password'".into(), - ..AccountConfig::default() + ..Account::default() }; let imap_config = ImapBackendConfig { imap_host: "localhost".into(), diff --git a/lib/tests/test_maildir_backend.rs b/lib/tests/test_maildir_backend.rs index 9a9e22a..8077aaf 100644 --- a/lib/tests/test_maildir_backend.rs +++ b/lib/tests/test_maildir_backend.rs @@ -2,7 +2,7 @@ use maildir::Maildir; use std::{collections::HashMap, env, fs, iter::FromIterator}; use himalaya_lib::{ - account::{AccountConfig, MaildirBackendConfig}, + account::{Account, MaildirBackendConfig}, backend::{Backend, MaildirBackend}, msg::Flag, }; @@ -19,9 +19,9 @@ fn test_maildir_backend() { mdir_sub.create_dirs().unwrap(); // configure accounts - let account_config = AccountConfig { + let account_config = Account { mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]), - ..AccountConfig::default() + ..Account::default() }; let mdir_config = MaildirBackendConfig { maildir_dir: mdir.path().to_owned(), diff --git a/lib/tests/test_notmuch_backend.rs b/lib/tests/test_notmuch_backend.rs index 1a886e7..dae4e43 100644 --- a/lib/tests/test_notmuch_backend.rs +++ b/lib/tests/test_notmuch_backend.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, env, fs, iter::FromIterator}; #[cfg(feature = "notmuch-backend")] use himalaya_lib::{ - account::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, + account::{Account, MaildirBackendConfig, NotmuchBackendConfig}, backend::{Backend, MaildirBackend, NotmuchBackend}, }; From 1e4dc0cb5a33ee272552aeea3298edeef0197d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 27 Jun 2022 20:51:12 +0200 Subject: [PATCH 22/24] add missing deserialized config errors --- lib/src/account/deserialized_config.rs | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/src/account/deserialized_config.rs b/lib/src/account/deserialized_config.rs index cc4280e..9da1798 100644 --- a/lib/src/account/deserialized_config.rs +++ b/lib/src/account/deserialized_config.rs @@ -5,7 +5,7 @@ use log::{debug, trace}; use serde::Deserialize; -use std::{collections::HashMap, env, fs, io, path::PathBuf, result}; +use std::{collections::HashMap, env, fs, io, path::PathBuf}; use thiserror::Error; use toml; @@ -14,15 +14,13 @@ use super::*; #[derive(Error, Debug)] pub enum DeserializeConfigError { #[error("cannot read config file")] - ReadConfigFile(#[from] io::Error), + ReadConfigFile(#[source] io::Error), #[error("cannot parse config file")] - ParseConfigFile(#[from] toml::de::Error), - #[error("cannot read environment variable")] - ReadEnvVar(#[from] env::VarError), + ParseConfigFile(#[source] toml::de::Error), + #[error("cannot read environment variable {1}")] + ReadEnvVar(#[source] env::VarError, &'static str), } -type Result = result::Result; - /// Represents the user config file. #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -51,13 +49,13 @@ pub struct DeserializedConfig { impl DeserializedConfig { /// Tries to create a config from an optional path. - pub fn from_opt_path(path: Option<&str>) -> Result { + pub fn from_opt_path(path: Option<&str>) -> Result { trace!(">> parse config from path"); debug!("path: {:?}", path); let path = path.map(|s| s.into()).unwrap_or(Self::path()?); - let content = fs::read_to_string(path)?; - let config = toml::from_str(&content)?; + let content = fs::read_to_string(path).map_err(DeserializeConfigError::ReadConfigFile)?; + let config = toml::from_str(&content).map_err(DeserializeConfigError::ParseConfigFile)?; trace!("config: {:?}", config); trace!("<< parse config from path"); @@ -66,21 +64,23 @@ impl DeserializedConfig { /// Tries to get the XDG config file path from XDG_CONFIG_HOME /// environment variable. - fn path_from_xdg() -> Result { - let path = env::var("XDG_CONFIG_HOME")?; + fn path_from_xdg() -> Result { + let path = env::var("XDG_CONFIG_HOME") + .map_err(|err| DeserializeConfigError::ReadEnvVar(err, "XDG_CONFIG_HOME"))?; let path = PathBuf::from(path).join("himalaya").join("config.toml"); Ok(path) } /// Tries to get the XDG config file path from HOME environment /// variable. - fn path_from_xdg_alt() -> Result { + fn path_from_xdg_alt() -> Result { let home_var = if cfg!(target_family = "windows") { "USERPROFILE" } else { "HOME" }; - let path = env::var(home_var)?; + let path = + env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?; let path = PathBuf::from(path) .join(".config") .join("himalaya") @@ -90,19 +90,20 @@ impl DeserializedConfig { /// Tries to get the .himalayarc config file path from HOME /// environment variable. - fn path_from_home() -> Result { + fn path_from_home() -> Result { let home_var = if cfg!(target_family = "windows") { "USERPROFILE" } else { "HOME" }; - let path = env::var(home_var)?; + let path = + env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?; let path = PathBuf::from(path).join(".himalayarc"); Ok(path) } /// Tries to get the config file path. - pub fn path() -> Result { + pub fn path() -> Result { Self::path_from_xdg() .or_else(|_| Self::path_from_xdg_alt()) .or_else(|_| Self::path_from_home()) From 9bcd659af2e2e6da39b3783c2ca52aced8976e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 27 Jun 2022 20:55:22 +0200 Subject: [PATCH 23/24] clean mbox lib module --- lib/src/mbox/mbox.rs | 4 ++++ lib/src/mbox/mboxes.rs | 10 +++++++--- lib/src/mbox/mod.rs | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/src/mbox/mbox.rs b/lib/src/mbox/mbox.rs index b14606c..ceab669 100644 --- a/lib/src/mbox/mbox.rs +++ b/lib/src/mbox/mbox.rs @@ -1,3 +1,7 @@ +//! Mailbox module. +//! +//! This module contains the representation of the mailbox. + use serde::Serialize; use std::fmt; diff --git a/lib/src/mbox/mboxes.rs b/lib/src/mbox/mboxes.rs index 161479e..0adca85 100644 --- a/lib/src/mbox/mboxes.rs +++ b/lib/src/mbox/mboxes.rs @@ -1,5 +1,9 @@ +//! Mailboxes module. +//! +//! This module contains the representation of the mailboxes. + use serde::Serialize; -use std::ops::{Deref, DerefMut}; +use std::ops; use super::Mbox; @@ -10,7 +14,7 @@ pub struct Mboxes { pub mboxes: Vec, } -impl Deref for Mboxes { +impl ops::Deref for Mboxes { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -18,7 +22,7 @@ impl Deref for Mboxes { } } -impl DerefMut for Mboxes { +impl ops::DerefMut for Mboxes { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.mboxes } diff --git a/lib/src/mbox/mod.rs b/lib/src/mbox/mod.rs index 5efcf73..25e70b5 100644 --- a/lib/src/mbox/mod.rs +++ b/lib/src/mbox/mod.rs @@ -1,3 +1,7 @@ +//! Mailbox module. +//! +//! This module contains everything related to mailboxes. + mod mbox; pub use mbox::*; From 29c731336f141ba7546eabf58c76f333847006c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 14 Jul 2022 12:13:52 +0200 Subject: [PATCH 24/24] doc: add announcement about financial support --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b7b71dc..6cf38bd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +*📢 Announcement: Himalaya receives support help, see the +[discussion](https://github.com/soywod/himalaya/discussions/399) for +more details.* + # 📫 Himalaya Command-line interface for email management