move backend to lib folder (#340)

This commit is contained in:
Clément DOUIN 2022-06-26 21:45:25 +02:00
parent 3b2991ae56
commit a5c4fdaac6
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
59 changed files with 1125 additions and 1014 deletions

9
Cargo.lock generated
View file

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

View file

@ -1,2 +1,2 @@
[workspace]
members = ["lib", "cli"]
members = ["lib", "cli"]

View file

@ -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()),
}
}

View file

@ -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()
}

View file

@ -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<Vec<ImapFetch>>;

View file

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

View file

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

View file

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

View file

@ -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<T>,
opts: PrintTableOpts,
) -> Result<()> {
) -> anyhow::Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
fn print_str<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
unimplemented!()
}
fn print_struct<T: Debug + Print + serde::Serialize>(
&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<Mboxes> {
fn get_mboxes(&mut self) -> backend::Result<Mboxes> {
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<Envelopes> {
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
unimplemented!()
}
fn search_envelopes(
@ -146,31 +141,31 @@ mod tests {
_: &str,
_: usize,
_: usize,
) -> Result<Envelopes> {
) -> backend::Result<Envelopes> {
unimplemented!()
}
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result<String> {
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
unimplemented!()
}
fn get_msg(&mut self, _: &str, _: &str) -> Result<Msg> {
fn get_msg(&mut self, _: &str, _: &str) -> backend::Result<Msg> {
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!()
}
}

View file

@ -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 `\`.

View file

@ -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<PageSize>, Page),
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
Send(RawMsg<'a>),
Write(tpl_args::TplOverride<'a>, AttachmentPaths<'a>, Encrypt),
Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt),
Flag(Option<flag_args::Cmd<'a>>),
Tpl(Option<tpl_args::Cmd<'a>>),
@ -261,7 +265,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
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)));
}

View file

@ -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(())
}

View file

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

View file

@ -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<Vec<&'a str>>,
pub to: Option<Vec<&'a str>>,
pub cc: Option<Vec<&'a str>>,
pub bcc: Option<Vec<&'a str>>,
pub headers: Option<Vec<&'a str>>,
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<Option<Cmd<'a>>> {
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<Option<Cmd<'a>>> {
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<Option<Cmd<'a>>> {
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)));
}

View file

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

View file

@ -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<Vec<u8>>;

View file

@ -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<String> {
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<String> {
}
pub fn open_with_draft() -> Result<String> {
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<Msg> {
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<Box<&'a mut B>> {
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)
}

View file

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

View file

@ -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<T> = result::Result<T, AccountError>;
pub type Result<T> = result::Result<T, Error>;
/// 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<SmtpCredentials> {
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<Option<String>> {
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<String> {
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<Option<String>> {
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<String> {
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<String> {
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())
}
}

View file

@ -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<T> = result::Result<T, Error>;
pub trait Backend<'a> {
fn connect(&mut self) -> Result<()> {

View file

@ -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<T> = result::Result<T, Error>;
#[derive(Debug, Default)]
pub struct IdMapper {
path: PathBuf,
map: HashMap<String, String>,
path: path::PathBuf,
map: collections::HashMap<String, String>,
short_hash_len: usize,
}
impl IdMapper {
pub fn new(dir: &Path) -> Result<Self> {
pub fn new(dir: &path::Path) -> Result<Self> {
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::<Vec<_>>()
.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<String, String>;
impl ops::Deref for IdMapper {
type Target = collections::HashMap<String, String>;
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
}

View file

@ -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<T> = result::Result<T, Error>;

View file

@ -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<TlsStream<TcpStream>>;
@ -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<Vec<u32>> {
let uids: Vec<u32> = 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<u32> = self
@ -123,7 +120,7 @@ impl<'a> ImapBackend<'a> {
false
})
})
.context("cannot start the idle mode")?;
.map_err(Error::StartIdleModeError)?;
let uids: Vec<u32> = 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<Mboxes> {
@ -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<Envelopes> {
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<String> = 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<String> {
@ -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<Msg> {
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(())
}
}

View file

@ -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<Envelope> {
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<Envelope> {
.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<Envelope> {
.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)
};

View file

@ -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<Vec<ImapFetch>>;
pub fn from_imap_fetches(fetches: ImapFetches) -> Result<Envelopes> {
let mut envelopes = Envelopes::default();
for fetch in fetches.iter().rev() {
envelopes.push(from_imap_fetch(fetch)?);
}
Ok(envelopes)
}

View file

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

View file

@ -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<imap::types::Flag<'a>> {
flags

View file

@ -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<imap::extensions::sort::SortCriterion<'a>>);
@ -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))

View file

@ -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),
}

View file

@ -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<PathBuf> {
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<Envelopes> {
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<String> {
@ -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(())

View file

@ -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> {
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<Envelope> {
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<Envelope> {
}
"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<Envelope> {
group_name.to_owned()
}
})
.ok_or_else(|| anyhow!("cannot find sender"))?;
.ok_or_else(|| MaildirError::FindSenderError)?;
}
_ => (),
}

View file

@ -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<Envelopes> {
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)
}

View file

@ -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<char> {
match flag {
Flag::Answered => Some('R'),
Flag::Seen => Some('S'),
Flag::Deleted => Some('T'),
Flag::Draft => Some('D'),
Flag::Flagged => Some('F'),
_ => None,
}
}

View file

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

73
lib/src/backend/mod.rs Normal file
View file

@ -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::*;

View file

@ -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),
}

View file

@ -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<Mboxes> {
@ -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)?;
}
}

View file

@ -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<Envelope> {
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<Envelope> {
}
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();

View file

@ -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<Envelopes> {
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)

View file

@ -1,5 +1,6 @@
mod process;
pub mod account;
pub mod backend;
pub mod mbox;
pub mod msg;

View file

@ -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<S: AsRef<str> + Debug>(addrs: S) -> Result<Option<Addrs>> {
pub fn from_slice_to_addrs<S: AsRef<str> + fmt::Debug>(
addrs: S,
) -> result::Result<Option<Addrs>, mailparse::MailParseError> {
let addrs = mailparse::addrparse(addrs.as_ref())?;
Ok(if addrs.is_empty() { None } else { Some(addrs) })
}

56
lib/src/msg/error.rs Normal file
View file

@ -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<env::VarError>, 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<T> = result::Result<T, Error>;

View file

@ -1,6 +1,5 @@
use std::{fmt, ops};
use serde::Serialize;
use std::{fmt, ops};
use super::Flag;

View file

@ -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::*;

View file

@ -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<Self> {
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<Box<&'a mut B>> {
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<Self> {
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<lettre::address::Envelope> 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)?)
}
}

24
lib/src/msg/msg_utils.rs Normal file
View file

@ -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(())
}

View file

@ -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<Self> {
) -> msg::Result<Self> {
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<Part>,
) -> 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<String> {
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> msg::Result<String> {
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)
}

15
lib/src/msg/tpl.rs Normal file
View file

@ -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<Vec<&'a str>>,
pub to: Option<Vec<&'a str>>,
pub cc: Option<Vec<&'a str>>,
pub bcc: Option<Vec<&'a str>>,
pub headers: Option<Vec<&'a str>>,
pub body: Option<&'a str>,
pub sig: Option<&'a str>,
}

View file

@ -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<T> = result::Result<T, ProcessError>;
pub type Result<T> = result::Result<T, Error>;
pub fn run_cmd(cmd: &str) -> Result<String> {
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)
}

View file

@ -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();

View file

@ -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());

View file

@ -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();