mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-26 04:50:25 +00:00
commit
26c2ed2a43
26 changed files with 1721 additions and 229 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.7] - 2022-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Notmuch support [#57]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Build failure due to `imap` version [#303]
|
||||
- No tilde expansion in `maildir-dir` [#305]
|
||||
- Unknown command SORT [#308]
|
||||
|
||||
### Changed
|
||||
|
||||
- [**BREAKING**] Replace `inbox-folder`, `sent-folder` and `draft-folder` by a generic hashmap `mailboxes`
|
||||
- Display short envelopes id for `maildir` and `notmuch` backends [#309]
|
||||
|
||||
## [0.5.6] - 2022-02-22
|
||||
|
||||
### Added
|
||||
|
@ -304,7 +321,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Password from command [#22]
|
||||
- Set up README [#20]
|
||||
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.6...HEAD
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.7...HEAD
|
||||
[0.5.7]: https://github.com/soywod/himalaya/compare/v0.5.6...v0.5.7
|
||||
[0.5.6]: https://github.com/soywod/himalaya/compare/v0.5.5...v0.5.6
|
||||
[0.5.5]: https://github.com/soywod/himalaya/compare/v0.5.4...v0.5.5
|
||||
[0.5.4]: https://github.com/soywod/himalaya/compare/v0.5.3...v0.5.4
|
||||
|
@ -364,6 +382,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#48]: https://github.com/soywod/himalaya/issues/48
|
||||
[#50]: https://github.com/soywod/himalaya/issues/50
|
||||
[#54]: https://github.com/soywod/himalaya/issues/54
|
||||
[#57]: https://github.com/soywod/himalaya/issues/57
|
||||
[#58]: https://github.com/soywod/himalaya/issues/58
|
||||
[#59]: https://github.com/soywod/himalaya/issues/59
|
||||
[#61]: https://github.com/soywod/himalaya/issues/61
|
||||
|
@ -433,3 +452,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#280]: https://github.com/soywod/himalaya/issues/280
|
||||
[#288]: https://github.com/soywod/himalaya/issues/288
|
||||
[#289]: https://github.com/soywod/himalaya/issues/289
|
||||
[#303]: https://github.com/soywod/himalaya/issues/303
|
||||
[#305]: https://github.com/soywod/himalaya/issues/305
|
||||
[#308]: https://github.com/soywod/himalaya/issues/308
|
||||
[#309]: https://github.com/soywod/himalaya/issues/309
|
||||
|
|
90
Cargo.lock
generated
90
Cargo.lock
generated
|
@ -153,7 +153,7 @@ dependencies = [
|
|||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
]
|
||||
|
@ -183,6 +183,41 @@ version = "0.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.9.3",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
|
@ -281,6 +316,27 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "from_variants"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "221a1eb1a3c98980bc1b740f462b3dcf73f4e371cda294986bac72497995a4e3"
|
||||
dependencies = [
|
||||
"from_variants_impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "from_variants_impl"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e08079fa3c89edec9160ceaa9e7172785468c26c053d12924cce0d5a55c241a"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "1.1.0"
|
||||
|
@ -380,7 +436,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "himalaya"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
|
@ -396,7 +452,9 @@ dependencies = [
|
|||
"log",
|
||||
"maildir",
|
||||
"mailparse",
|
||||
"md5",
|
||||
"native-tls",
|
||||
"notmuch",
|
||||
"regex",
|
||||
"rfc2047-decoder",
|
||||
"serde",
|
||||
|
@ -457,6 +515,12 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
|
@ -649,6 +713,12 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "1.0.2"
|
||||
|
@ -732,6 +802,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notmuch"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca0941fd9af5b8529e3d42494f56efafb909b76190a7a454cde9d6e397390cf9"
|
||||
dependencies = [
|
||||
"from_variants",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
|
@ -1241,6 +1321,12 @@ version = "0.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.81"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||
|
@ -25,13 +25,15 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions"
|
|||
env_logger = "0.8.3"
|
||||
erased-serde = "0.3.18"
|
||||
html-escape = "0.2.9"
|
||||
imap = "3.0.0-alpha.4"
|
||||
imap = "=3.0.0-alpha.4"
|
||||
imap-proto = "0.14.3"
|
||||
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
|
||||
log = "0.4.14"
|
||||
maildir = "0.6.0"
|
||||
mailparse = "0.13.6"
|
||||
md5 = "0.7.0"
|
||||
native-tls = "0.2.8"
|
||||
notmuch = { version = "0.7.1", optional = true }
|
||||
regex = "1.5.4"
|
||||
rfc2047-decoder = "0.1.2"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
|
|
|
@ -30,7 +30,7 @@ curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh |
|
|||
```
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Installation:from-binary)
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary)
|
||||
for other installation methods.*
|
||||
|
||||
## Configuration
|
||||
|
|
|
@ -61,7 +61,6 @@ use_field_init_shorthand = false
|
|||
force_explicit_abi = true
|
||||
condense_wildcard_suffixes = false
|
||||
color = "Auto"
|
||||
required_version = "1.4.37"
|
||||
unstable_features = false
|
||||
disable_all_formatting = false
|
||||
skip_children = false
|
||||
|
|
|
@ -21,8 +21,14 @@ pub trait Backend<'a> {
|
|||
fn get_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>>;
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
filter: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>>;
|
||||
|
|
127
src/backends/id_mapper.rs
Normal file
127
src/backends/id_mapper.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::OpenOptions,
|
||||
io::{BufRead, BufReader, Write},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdMapper {
|
||||
path: PathBuf,
|
||||
map: HashMap<String, String>,
|
||||
short_hash_len: usize,
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn new(dir: &Path) -> Result<Self> {
|
||||
let mut mapper = Self::default();
|
||||
mapper.path = dir.join(".himalaya-id-map");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&mapper.path)
|
||||
.context("cannot open id hash map file")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
let line =
|
||||
line.context("cannot read line from maildir envelopes id mapper cache file")?;
|
||||
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
|
||||
)
|
||||
})?;
|
||||
mapper.insert(hash.to_owned(), id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mapper)
|
||||
}
|
||||
|
||||
pub fn find(&self, short_hash: &str) -> Result<String> {
|
||||
let matching_hashes: Vec<_> = self
|
||||
.keys()
|
||||
.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,
|
||||
))
|
||||
} else if matching_hashes.len() > 1 {
|
||||
Err(anyhow!(
|
||||
"the short hash {:?} matches more than one hash: {}",
|
||||
short_hash,
|
||||
matching_hashes
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
.context(format!(
|
||||
"cannot find maildir message id from short hash {:?}",
|
||||
short_hash
|
||||
)))
|
||||
} else {
|
||||
Ok(self.get(matching_hashes[0]).unwrap().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, lines: Vec<(String, String)>) -> Result<usize> {
|
||||
self.extend(lines);
|
||||
|
||||
let mut entries = String::new();
|
||||
let mut short_hash_len = self.short_hash_len;
|
||||
|
||||
for (hash, id) in self.iter() {
|
||||
loop {
|
||||
let short_hash = &hash[0..self.short_hash_len];
|
||||
let conflict_found = self
|
||||
.map
|
||||
.keys()
|
||||
.find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash)
|
||||
.is_some();
|
||||
if self.short_hash_len > 32 || !conflict_found {
|
||||
break;
|
||||
}
|
||||
short_hash_len += 1;
|
||||
}
|
||||
entries.push_str(&format!("{} {}\n", hash, id));
|
||||
}
|
||||
|
||||
self.short_hash_len = short_hash_len;
|
||||
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&self.path)
|
||||
.context("cannot open maildir id hash map cache")?
|
||||
.write(format!("{}\n{}", short_hash_len, entries).as_bytes())
|
||||
.context("cannot write maildir id hash map cache")?;
|
||||
|
||||
Ok(short_hash_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IdMapper {
|
||||
type Target = HashMap<String, String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for IdMapper {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
|
@ -229,8 +229,42 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
fn get_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.exists as usize;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
if last_seq == 0 {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
}
|
||||
|
||||
let range = if page_size > 0 {
|
||||
let cursor = page * page_size;
|
||||
let begin = 1.max(last_seq - cursor);
|
||||
let end = begin - begin.min(page_size) + 1;
|
||||
format!("{}:{}", end, begin)
|
||||
} else {
|
||||
String::from("1:*")
|
||||
};
|
||||
debug!("range: {:?}", range);
|
||||
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(format!("cannot fetch messages within range {:?}", range))?;
|
||||
let envelopes: ImapEnvelopes = fetches.try_into()?;
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
filter: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
|
@ -239,24 +273,36 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.exists;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
if last_seq == 0 {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
}
|
||||
|
||||
let sort: SortCriteria = sort.try_into()?;
|
||||
let charset = imap::extensions::sort::SortCharset::Utf8;
|
||||
let begin = page * page_size;
|
||||
let end = begin + (page_size - 1);
|
||||
let seqs: Vec<String> = self
|
||||
.sess()?
|
||||
.sort(&sort, charset, filter)
|
||||
.context(format!(
|
||||
"cannot search in {:?} with query {:?}",
|
||||
mbox, filter
|
||||
))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect();
|
||||
let seqs: Vec<String> = if sort.is_empty() {
|
||||
self.sess()?
|
||||
.search(query)
|
||||
.context(format!(
|
||||
"cannot find envelopes in {:?} with query {:?}",
|
||||
mbox, query
|
||||
))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect()
|
||||
} else {
|
||||
let sort: SortCriteria = sort.try_into()?;
|
||||
let charset = imap::extensions::sort::SortCharset::Utf8;
|
||||
self.sess()?
|
||||
.sort(&sort, charset, query)
|
||||
.context(format!(
|
||||
"cannot find envelopes in {:?} with query {:?}",
|
||||
mbox, query
|
||||
))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect()
|
||||
};
|
||||
if seqs.is_empty() {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{convert::TryFrom, fmt, ops::Deref};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the imap flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
//! Maildir backend module.
|
||||
//!
|
||||
//! This module contains the definition of the maildir backend and its
|
||||
//! traits implementation.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
use std::{convert::TryInto, fs, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
|
||||
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
|
||||
config::{AccountConfig, MaildirBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
/// Represents the maildir backend.
|
||||
pub struct MaildirBackend<'a> {
|
||||
mdir: maildir::Maildir,
|
||||
account_config: &'a AccountConfig,
|
||||
mdir: maildir::Maildir,
|
||||
}
|
||||
|
||||
impl<'a> MaildirBackend<'a> {
|
||||
|
@ -28,21 +35,32 @@ impl<'a> MaildirBackend<'a> {
|
|||
if mdir_path.is_dir() {
|
||||
Ok(mdir_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"cannot read maildir from directory {:?}",
|
||||
mdir_path
|
||||
))
|
||||
Err(anyhow!("cannot read maildir directory {:?}", mdir_path))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mdir_from_name(&self, mdir: &str) -> Result<maildir::Maildir> {
|
||||
if mdir == self.account_config.inbox_folder {
|
||||
/// Creates a maildir instance from a string slice.
|
||||
pub fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> {
|
||||
// If the dir points to the inbox folder, creates a maildir
|
||||
// instance from the root folder.
|
||||
if dir == "inbox" {
|
||||
self.validate_mdir_path(self.mdir.path().to_owned())
|
||||
.map(maildir::Maildir::from)
|
||||
} else {
|
||||
self.validate_mdir_path(mdir.into())
|
||||
// If the dir is a valid maildir path, creates a maildir instance from it.
|
||||
self.validate_mdir_path(dir.into())
|
||||
.or_else(|_| {
|
||||
let path = self.mdir.path().join(format!(".{}", mdir));
|
||||
// Otherwise creates a maildir instance from a
|
||||
// maildir subdirectory by adding a "." in front
|
||||
// of the name as described in the spec:
|
||||
// https://cr.yp.to/proto/maildir.html
|
||||
let dir = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.get(dir)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(dir);
|
||||
let path = self.mdir.path().join(format!(".{}", dir));
|
||||
self.validate_mdir_path(path)
|
||||
})
|
||||
.map(maildir::Maildir::from)
|
||||
|
@ -51,135 +69,423 @@ impl<'a> MaildirBackend<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Backend<'a> for MaildirBackend<'a> {
|
||||
fn add_mbox(&mut self, mdir: &str) -> Result<()> {
|
||||
fs::create_dir(self.mdir.path().join(format!(".{}", mdir)))
|
||||
.context(format!("cannot create maildir subfolder {:?}", mdir))
|
||||
fn add_mbox(&mut self, subdir: &str) -> Result<()> {
|
||||
info!(">> add maildir subdir");
|
||||
debug!("subdir: {:?}", subdir);
|
||||
|
||||
let path = self.mdir.path().join(format!(".{}", subdir));
|
||||
trace!("subdir path: {:?}", path);
|
||||
|
||||
fs::create_dir(&path)
|
||||
.with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?;
|
||||
|
||||
info!("<< add maildir subdir");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
let mboxes: MaildirMboxes = self.mdir.list_subdirs().try_into()?;
|
||||
Ok(Box::new(mboxes))
|
||||
info!(">> get maildir dirs");
|
||||
|
||||
let dirs: MaildirMboxes =
|
||||
self.mdir.list_subdirs().try_into().with_context(|| {
|
||||
format!("cannot parse maildir dirs from {:?}", self.mdir.path())
|
||||
})?;
|
||||
trace!("dirs: {:?}", dirs);
|
||||
|
||||
info!("<< get maildir dirs");
|
||||
Ok(Box::new(dirs))
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, mdir: &str) -> Result<()> {
|
||||
fs::remove_dir_all(self.mdir.path().join(format!(".{}", mdir)))
|
||||
.context(format!("cannot delete maildir subfolder {:?}", mdir))
|
||||
fn del_mbox(&mut self, dir: &str) -> Result<()> {
|
||||
info!(">> delete maildir dir");
|
||||
debug!("dir: {:?}", dir);
|
||||
|
||||
let path = self.mdir.path().join(format!(".{}", dir));
|
||||
trace!("dir path: {:?}", path);
|
||||
|
||||
fs::remove_dir_all(&path)
|
||||
.with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?;
|
||||
|
||||
info!("<< delete maildir dir");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
mdir: &str,
|
||||
_sort: &str,
|
||||
filter: &str,
|
||||
dir: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
let mail_entries = match filter {
|
||||
"new" => mdir.list_new(),
|
||||
_ => mdir.list_cur(),
|
||||
};
|
||||
let mut envelopes: MaildirEnvelopes = mail_entries
|
||||
.try_into()
|
||||
.context("cannot parse maildir envelopes from {:?}")?;
|
||||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
info!(">> get maildir envelopes");
|
||||
debug!("dir: {:?}", dir);
|
||||
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))?;
|
||||
|
||||
// Reads envelopes from the "cur" folder of the selected
|
||||
// maildir.
|
||||
let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| {
|
||||
format!("cannot parse maildir envelopes from {:?}", self.mdir.path())
|
||||
})?;
|
||||
debug!("envelopes len: {:?}", envelopes.len());
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
// Calculates pagination boundaries.
|
||||
let page_begin = page * page_size;
|
||||
debug!("page begin: {:?}", page_begin);
|
||||
if page_begin > envelopes.len() {
|
||||
return Err(anyhow!(format!(
|
||||
"cannot list maildir envelopes at page {:?} (out of bounds)",
|
||||
return Err(anyhow!(
|
||||
"cannot get maildir envelopes at page {:?} (out of bounds)",
|
||||
page_begin + 1,
|
||||
)));
|
||||
));
|
||||
}
|
||||
let page_end = envelopes.len().min(page_begin + page_size);
|
||||
debug!("page end: {:?}", page_end);
|
||||
|
||||
// Sorts envelopes by most recent date.
|
||||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
|
||||
// Applies pagination boundaries.
|
||||
envelopes.0 = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
// represents the minimum hash length possible to avoid
|
||||
// conflicts.
|
||||
let short_hash_len = {
|
||||
let mut mapper = IdMapper::new(mdir.path())?;
|
||||
let entries = envelopes
|
||||
.iter()
|
||||
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
|
||||
.collect();
|
||||
mapper.append(entries)?
|
||||
};
|
||||
debug!("short hash length: {:?}", short_hash_len);
|
||||
|
||||
// Shorten envelopes hash.
|
||||
envelopes
|
||||
.iter_mut()
|
||||
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
|
||||
|
||||
info!("<< get maildir envelopes");
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
let flags: MaildirFlags = flags.try_into()?;
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
_dir: &str,
|
||||
_query: &str,
|
||||
_sort: &str,
|
||||
_page_size: usize,
|
||||
_page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> search maildir envelopes");
|
||||
info!("<< search maildir envelopes");
|
||||
Err(anyhow!(
|
||||
"cannot find maildir envelopes: feature not implemented"
|
||||
))
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
|
||||
info!(">> add maildir message");
|
||||
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: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
let id = mdir
|
||||
.store_cur_with_flags(msg, &flags.to_string())
|
||||
.context(format!(
|
||||
"cannot add message to the \"cur\" folder of maildir {:?}",
|
||||
.with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?;
|
||||
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
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< add maildir message");
|
||||
Ok(Box::new(hash))
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result<Msg> {
|
||||
info!(">> get maildir message");
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let mut mail_entry = mdir.find(&id).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"cannot find maildir message by id {:?} at {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
))?;
|
||||
Ok(Box::new(id))
|
||||
)
|
||||
})?;
|
||||
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())
|
||||
})?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< get maildir message");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, mdir: &str, id: &str) -> Result<Msg> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
let mut mail_entry = mdir
|
||||
.find(id)
|
||||
.ok_or_else(|| anyhow!("cannot find maildir message {:?} in {:?}", id, mdir.path()))?;
|
||||
let parsed_mail = mail_entry.parsed().context(format!(
|
||||
"cannot parse maildir message {:?} in {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
))?;
|
||||
Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!(
|
||||
"cannot parse maildir message {:?} from {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
))
|
||||
fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> copy maildir message");
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
|
||||
// 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 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
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< copy maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> {
|
||||
let mdir_src = self.get_mdir_from_name(mdir_src)?;
|
||||
let mdir_dst = self.get_mdir_from_name(mdir_dst)?;
|
||||
mdir_src.copy_to(id, &mdir_dst).context(format!(
|
||||
"cannot copy message {:?} from maildir {:?} to maildir {:?}",
|
||||
id,
|
||||
mdir_src.path(),
|
||||
mdir_dst.path()
|
||||
))
|
||||
fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> move maildir message");
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
|
||||
// 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 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
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< move maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> {
|
||||
let mdir_src = self.get_mdir_from_name(mdir_src)?;
|
||||
let mdir_dst = self.get_mdir_from_name(mdir_dst)?;
|
||||
mdir_src.move_to(id, &mdir_dst).context(format!(
|
||||
"cannot move message {:?} from maildir {:?} to maildir {:?}",
|
||||
id,
|
||||
mdir_src.path(),
|
||||
mdir_dst.path()
|
||||
))
|
||||
fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> delete maildir message");
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.delete(&id).with_context(|| {
|
||||
format!(
|
||||
"cannot delete message {:?} from maildir {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< delete maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
mdir.delete(id).context(format!(
|
||||
"cannot delete message {:?} from maildir {:?}",
|
||||
id,
|
||||
mdir.path()
|
||||
))
|
||||
fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
|
||||
info!(">> add maildir message flags");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
debug!("flags: {:?}", flags);
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.add_flags(&id, &flags.to_string())
|
||||
.with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?;
|
||||
|
||||
info!("<< add maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
let flags: MaildirFlags = flags_str.try_into()?;
|
||||
mdir.add_flags(id, &flags.to_string()).context(format!(
|
||||
"cannot add flags {:?} to maildir message {:?}",
|
||||
flags_str, id
|
||||
))
|
||||
fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
|
||||
info!(">> set maildir message flags");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
debug!("flags: {:?}", flags);
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.set_flags(&id, &flags.to_string())
|
||||
.with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?;
|
||||
|
||||
info!("<< set maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
let flags: MaildirFlags = flags_str.try_into()?;
|
||||
mdir.set_flags(id, &flags.to_string()).context(format!(
|
||||
"cannot set flags {:?} to maildir message {:?}",
|
||||
flags_str, id
|
||||
))
|
||||
}
|
||||
fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
|
||||
info!(">> delete maildir message flags");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("short hash: {:?}", short_hash);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
|
||||
let mdir = self.get_mdir_from_name(mdir)?;
|
||||
let flags: MaildirFlags = flags_str.try_into()?;
|
||||
mdir.remove_flags(id, &flags.to_string()).context(format!(
|
||||
"cannot remove flags {:?} from maildir message {:?}",
|
||||
flags_str, id
|
||||
))
|
||||
let mdir = self
|
||||
.get_mdir_from_dir(dir)
|
||||
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
|
||||
let flags: MaildirFlags = flags
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
|
||||
debug!("flags: {:?}", flags);
|
||||
let id = IdMapper::new(mdir.path())
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find maildir message by short hash {:?} at {:?}",
|
||||
short_hash,
|
||||
mdir.path()
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.remove_flags(&id, &flags.to_string())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot delete flags {:?} to maildir message {:?}",
|
||||
flags, id
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("<< delete maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,9 @@ pub struct MaildirEnvelope {
|
|||
/// Represents the id of the message.
|
||||
pub id: String,
|
||||
|
||||
/// Represents the MD5 hash of the message id.
|
||||
pub hash: String,
|
||||
|
||||
/// Represents the flags of the message.
|
||||
pub flags: MaildirFlags,
|
||||
|
||||
|
@ -72,7 +75,7 @@ pub struct MaildirEnvelope {
|
|||
impl Table for MaildirEnvelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("IDENTIFIER").bold().underline().white())
|
||||
.cell(Cell::new("HASH").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("SENDER").bold().underline().white())
|
||||
|
@ -80,14 +83,14 @@ impl Table for MaildirEnvelope {
|
|||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let hash = self.hash.clone();
|
||||
let unseen = !self.flags.contains(&MaildirFlag::Seen);
|
||||
let flags = self.flags.to_symbols_string();
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = &self.date;
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(hash).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
|
@ -110,6 +113,7 @@ impl<'a> TryFrom<RawMaildirEnvelopes> for MaildirEnvelopes {
|
|||
.context("cannot parse maildir mail entry")?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
|
||||
Ok(MaildirEnvelopes(envelopes))
|
||||
}
|
||||
}
|
||||
|
@ -123,13 +127,13 @@ impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
|
|||
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
|
||||
info!("begin: try building envelope from maildir parsed mail");
|
||||
|
||||
let mut envelope = Self {
|
||||
id: mail_entry.id().into(),
|
||||
flags: (&mail_entry)
|
||||
.try_into()
|
||||
.context("cannot parse maildir flags")?,
|
||||
..Self::default()
|
||||
};
|
||||
let mut envelope = Self::default();
|
||||
|
||||
envelope.id = mail_entry.id().into();
|
||||
envelope.hash = format!("{:x}", md5::compute(&envelope.id));
|
||||
envelope.flags = (&mail_entry)
|
||||
.try_into()
|
||||
.context("cannot parse maildir flags")?;
|
||||
|
||||
let parsed_mail = mail_entry
|
||||
.parsed()
|
||||
|
|
453
src/backends/notmuch/notmuch_backend.rs
Normal file
453
src/backends/notmuch/notmuch_backend.rs
Normal file
|
@ -0,0 +1,453 @@
|
|||
use std::{convert::TryInto, fs};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
|
||||
config::{AccountConfig, NotmuchBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
/// Represents the Notmuch backend.
|
||||
pub struct NotmuchBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
notmuch_config: &'a NotmuchBackendConfig,
|
||||
pub mdir: &'a mut MaildirBackend<'a>,
|
||||
db: notmuch::Database,
|
||||
}
|
||||
|
||||
impl<'a> NotmuchBackend<'a> {
|
||||
pub fn new(
|
||||
account_config: &'a AccountConfig,
|
||||
notmuch_config: &'a NotmuchBackendConfig,
|
||||
mdir: &'a mut MaildirBackend<'a>,
|
||||
) -> Result<NotmuchBackend<'a>> {
|
||||
info!(">> create new notmuch backend");
|
||||
|
||||
let backend = Self {
|
||||
account_config,
|
||||
notmuch_config,
|
||||
mdir,
|
||||
db: notmuch::Database::open(
|
||||
notmuch_config.notmuch_database_dir.clone(),
|
||||
notmuch::DatabaseMode::ReadWrite,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot open notmuch database at {:?}",
|
||||
notmuch_config.notmuch_database_dir
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
info!("<< create new notmuch backend");
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
fn _search_envelopes(
|
||||
&mut self,
|
||||
query: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
// Gets envelopes matching the given Notmuch query.
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let mut envelopes: NotmuchEnvelopes = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?
|
||||
.try_into()
|
||||
.with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?;
|
||||
debug!("envelopes len: {:?}", envelopes.len());
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
// Calculates pagination boundaries.
|
||||
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,
|
||||
));
|
||||
}
|
||||
let page_end = envelopes.len().min(page_begin + page_size);
|
||||
debug!("page end: {:?}", page_end);
|
||||
|
||||
// Sorts envelopes by most recent date.
|
||||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
|
||||
// Applies pagination boundaries.
|
||||
envelopes.0 = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
// represents the minimum hash length possible to avoid
|
||||
// conflicts.
|
||||
let short_hash_len = {
|
||||
let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?;
|
||||
let entries = envelopes
|
||||
.iter()
|
||||
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
|
||||
.collect();
|
||||
mapper.append(entries)?
|
||||
};
|
||||
debug!("short hash length: {:?}", short_hash_len);
|
||||
|
||||
// Shorten envelopes hash.
|
||||
envelopes
|
||||
.iter_mut()
|
||||
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
|
||||
|
||||
Ok(Box::new(envelopes))
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
))
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
info!(">> get notmuch virtual mailboxes");
|
||||
|
||||
let mut virt_mboxes: Vec<_> = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.iter()
|
||||
.map(|(k, v)| NotmuchMbox::new(k, v))
|
||||
.collect();
|
||||
trace!("virtual mailboxes: {:?}", virt_mboxes);
|
||||
virt_mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
|
||||
info!("<< get notmuch virtual mailboxes");
|
||||
Ok(Box::new(NotmuchMboxes(virt_mboxes)))
|
||||
}
|
||||
|
||||
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"
|
||||
))
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
virt_mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> get notmuch envelopes");
|
||||
debug!("virtual mailbox: {:?}", virt_mbox);
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let query = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.get(virt_mbox)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("all");
|
||||
debug!("query: {:?}", query);
|
||||
let envelopes = self._search_envelopes(query, page_size, page)?;
|
||||
|
||||
info!("<< get notmuch envelopes");
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
virt_mbox: &str,
|
||||
query: &str,
|
||||
_sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
info!(">> search notmuch envelopes");
|
||||
debug!("virtual mailbox: {:?}", virt_mbox);
|
||||
debug!("query: {:?}", query);
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let query = if query.is_empty() {
|
||||
self.account_config
|
||||
.mailboxes
|
||||
.get(virt_mbox)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("all")
|
||||
} else {
|
||||
query
|
||||
};
|
||||
debug!("final query: {:?}", query);
|
||||
let envelopes = self._search_envelopes(query, page_size, page)?;
|
||||
|
||||
info!("<< search notmuch envelopes");
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result<Box<dyn ToString>> {
|
||||
info!(">> add notmuch envelopes");
|
||||
debug!("tags: {:?}", tags);
|
||||
|
||||
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("inbox", msg, "seen")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot add notmuch message to maildir {:?}",
|
||||
self.notmuch_config.notmuch_database_dir
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
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))?;
|
||||
debug!("id: {:?}", id);
|
||||
let file_path = dir.join("cur").join(format!("{}:2,S", id));
|
||||
debug!("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))?
|
||||
.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
|
||||
)
|
||||
})?;
|
||||
|
||||
// Attaches tags to the notmuch message.
|
||||
self.add_flags("", &hash, tags)
|
||||
.with_context(|| format!("cannot add flags to notmuch message {:?}", id))?;
|
||||
|
||||
info!("<< add notmuch envelopes");
|
||||
Ok(Box::new(hash))
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, _: &str, short_hash: &str) -> Result<Msg> {
|
||||
info!(">> add notmuch envelopes");
|
||||
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
|
||||
)
|
||||
})?;
|
||||
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))?
|
||||
.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))?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< get notmuch message");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
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"
|
||||
))
|
||||
}
|
||||
|
||||
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"
|
||||
))
|
||||
}
|
||||
|
||||
fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> {
|
||||
info!(">> delete notmuch message");
|
||||
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
|
||||
)
|
||||
})?;
|
||||
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))?
|
||||
.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))?;
|
||||
|
||||
info!("<< delete notmuch message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
|
||||
info!(">> add notmuch message flags");
|
||||
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
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
let tags: Vec<_> = tags.split_whitespace().collect();
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
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())
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
info!("<< add notmuch message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
|
||||
info!(">> set notmuch message flags");
|
||||
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
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
let tags: Vec<_> = tags.split_whitespace().collect();
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
for msg in msgs {
|
||||
msg.remove_all_tags().with_context(|| {
|
||||
format!("cannot remove all tags from notmuch message {:?}", msg.id())
|
||||
})?;
|
||||
for tag in tags.iter() {
|
||||
msg.add_tag(*tag).with_context(|| {
|
||||
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
info!("<< set notmuch message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
|
||||
info!(">> delete notmuch message flags");
|
||||
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
|
||||
)
|
||||
})?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
let tags: Vec<_> = tags.split_whitespace().collect();
|
||||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
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()
|
||||
)
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
info!("<< delete notmuch message flags");
|
||||
Ok(())
|
||||
}
|
||||
}
|
177
src/backends/notmuch/notmuch_envelope.rs
Normal file
177
src/backends/notmuch/notmuch_envelope.rs
Normal file
|
@ -0,0 +1,177 @@
|
|||
//! Notmuch mailbox module.
|
||||
//!
|
||||
//! This module provides Notmuch types and conversion utilities
|
||||
//! related to the envelope
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::DateTime;
|
||||
use log::{info, trace};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
msg::{from_slice_to_addrs, Addr},
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchEnvelopes(pub Vec<NotmuchEnvelope>);
|
||||
|
||||
impl Deref for NotmuchEnvelopes {
|
||||
type Target = Vec<NotmuchEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NotmuchEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchEnvelopes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the envelope. The envelope is just a message subset,
|
||||
/// and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct NotmuchEnvelope {
|
||||
/// Represents the id of the message.
|
||||
pub id: String,
|
||||
|
||||
/// Represents the MD5 hash of the message id.
|
||||
pub hash: String,
|
||||
|
||||
/// Represents the tags of the message.
|
||||
pub flags: Vec<String>,
|
||||
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
|
||||
/// Represents the date of the message.
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for NotmuchEnvelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("HASH").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("SENDER").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let hash = self.hash.to_string();
|
||||
let unseen = !self.flags.contains(&String::from("unread"));
|
||||
let flags = String::new();
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = &self.date;
|
||||
Row::new()
|
||||
.cell(Cell::new(hash).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw envelopees returned by the `notmuch` crate.
|
||||
pub type RawNotmuchEnvelopes = notmuch::Messages;
|
||||
|
||||
impl<'a> TryFrom<RawNotmuchEnvelopes> for NotmuchEnvelopes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result<Self, Self::Error> {
|
||||
let mut envelopes = vec![];
|
||||
for raw_envelope in raw_envelopes {
|
||||
let envelope: NotmuchEnvelope = raw_envelope
|
||||
.try_into()
|
||||
.context("cannot parse notmuch mail entry")?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
Ok(NotmuchEnvelopes(envelopes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw envelope returned by the `notmuch` crate.
|
||||
pub type RawNotmuchEnvelope = notmuch::Message;
|
||||
|
||||
impl<'a> TryFrom<RawNotmuchEnvelope> for NotmuchEnvelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result<Self, Self::Error> {
|
||||
info!("begin: try building envelope from notmuch parsed mail");
|
||||
|
||||
let id = raw_envelope.id().to_string();
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
let subject = raw_envelope
|
||||
.header("subject")
|
||||
.context("cannot get header \"Subject\" from notmuch message")?
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let sender = raw_envelope
|
||||
.header("from")
|
||||
.context("cannot get header \"From\" from notmuch message")?
|
||||
.ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", id))?
|
||||
.to_string();
|
||||
let sender = from_slice_to_addrs(sender)?
|
||||
.and_then(|senders| {
|
||||
if senders.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(senders)
|
||||
}
|
||||
})
|
||||
.map(|senders| match &senders[0] {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
|
||||
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
|
||||
}
|
||||
Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(),
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find sender"))?;
|
||||
let date = raw_envelope
|
||||
.header("date")
|
||||
.context("cannot get header \"Date\" from notmuch message")?
|
||||
.ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))?
|
||||
.to_string();
|
||||
let date =
|
||||
DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0)
|
||||
.context(format!(
|
||||
"cannot parse message date {:?} of notmuch message {:?}",
|
||||
date, id
|
||||
))?
|
||||
.naive_local()
|
||||
.to_string();
|
||||
|
||||
let envelope = Self {
|
||||
id,
|
||||
hash,
|
||||
flags: raw_envelope.tags().collect(),
|
||||
subject,
|
||||
sender,
|
||||
date,
|
||||
};
|
||||
trace!("envelope: {:?}", envelope);
|
||||
|
||||
info!("end: try building envelope from notmuch parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
80
src/backends/notmuch/notmuch_mbox.rs
Normal file
80
src/backends/notmuch/notmuch_mbox.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
//! Notmuch mailbox module.
|
||||
//!
|
||||
//! This module provides Notmuch types and conversion utilities
|
||||
//! related to the mailbox
|
||||
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mbox::Mboxes,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of Notmuch mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchMboxes(pub Vec<NotmuchMbox>);
|
||||
|
||||
impl Deref for NotmuchMboxes {
|
||||
type Target = Vec<NotmuchMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchMboxes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for NotmuchMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the notmuch virtual mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct NotmuchMbox {
|
||||
/// Represents the virtual mailbox name.
|
||||
pub name: String,
|
||||
|
||||
/// Represents the query associated to the virtual mailbox name.
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
impl NotmuchMbox {
|
||||
pub fn new(name: &str, query: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
query: query.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NotmuchMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for NotmuchMbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("QUERY").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).white())
|
||||
.cell(Cell::new(&self.query).green())
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ use anyhow::{anyhow, Context, Result};
|
|||
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::MailAddr;
|
||||
use std::{env, ffi::OsStr, fs, path::PathBuf};
|
||||
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf};
|
||||
|
||||
use crate::{config::*, output::run_cmd};
|
||||
|
||||
|
@ -23,12 +23,6 @@ pub struct AccountConfig {
|
|||
pub sig: Option<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: usize,
|
||||
/// Represents the inbox folder name for this account.
|
||||
pub inbox_folder: String,
|
||||
/// Represents the sent folder name for this account.
|
||||
pub sent_folder: String,
|
||||
/// Represents the draft folder name for this account.
|
||||
pub draft_folder: String,
|
||||
/// Represents the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the default IMAP query "NEW" used to fetch new messages
|
||||
|
@ -36,6 +30,9 @@ pub struct AccountConfig {
|
|||
/// Represents the watch commands.
|
||||
pub watch_cmds: Vec<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// Represents the SMTP host.
|
||||
pub smtp_host: String,
|
||||
/// Represents the SMTP port.
|
||||
|
@ -73,6 +70,10 @@ impl<'a> AccountConfig {
|
|||
DeserializedAccountConfig::Maildir(account) => {
|
||||
account.default.unwrap_or_default()
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
DeserializedAccountConfig::Notmuch(account) => {
|
||||
account.default.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.map(|(name, account)| (name.to_owned(), account))
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
|
@ -134,24 +135,6 @@ impl<'a> AccountConfig {
|
|||
downloads_dir,
|
||||
sig,
|
||||
default_page_size,
|
||||
inbox_folder: base_account
|
||||
.inbox_folder
|
||||
.as_deref()
|
||||
.or_else(|| config.inbox_folder.as_deref())
|
||||
.unwrap_or(DEFAULT_INBOX_FOLDER)
|
||||
.to_string(),
|
||||
sent_folder: base_account
|
||||
.sent_folder
|
||||
.as_deref()
|
||||
.or_else(|| config.sent_folder.as_deref())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER)
|
||||
.to_string(),
|
||||
draft_folder: base_account
|
||||
.draft_folder
|
||||
.as_deref()
|
||||
.or_else(|| config.draft_folder.as_deref())
|
||||
.unwrap_or(DEFAULT_DRAFT_FOLDER)
|
||||
.to_string(),
|
||||
notify_cmd: base_account.notify_cmd.clone(),
|
||||
notify_query: base_account
|
||||
.notify_query
|
||||
|
@ -165,6 +148,7 @@ impl<'a> AccountConfig {
|
|||
.or_else(|| config.watch_cmds.as_ref())
|
||||
.unwrap_or(&vec![])
|
||||
.to_owned(),
|
||||
mailboxes: base_account.mailboxes.clone(),
|
||||
default: base_account.default.unwrap_or_default(),
|
||||
email: base_account.email.to_owned(),
|
||||
|
||||
|
@ -191,7 +175,15 @@ impl<'a> AccountConfig {
|
|||
}),
|
||||
DeserializedAccountConfig::Maildir(config) => {
|
||||
BackendConfig::Maildir(MaildirBackendConfig {
|
||||
maildir_dir: config.maildir_dir.clone(),
|
||||
maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(),
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
DeserializedAccountConfig::Notmuch(config) => {
|
||||
BackendConfig::Notmuch(NotmuchBackendConfig {
|
||||
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)?
|
||||
.to_string()
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
@ -321,6 +313,8 @@ impl<'a> AccountConfig {
|
|||
pub enum BackendConfig {
|
||||
Imap(ImapBackendConfig),
|
||||
Maildir(MaildirBackendConfig),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch(NotmuchBackendConfig),
|
||||
}
|
||||
|
||||
/// Represents the IMAP backend.
|
||||
|
@ -358,6 +352,14 @@ pub struct MaildirBackendConfig {
|
|||
pub maildir_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Represents the Notmuch backend.
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NotmuchBackendConfig {
|
||||
/// Represents the Notmuch database path.
|
||||
pub notmuch_database_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
pub trait ToDeserializedBaseAccountConfig {
|
||||
fn to_base(&self) -> DeserializedBaseAccountConfig;
|
||||
|
@ -11,6 +11,8 @@ pub trait ToDeserializedBaseAccountConfig {
|
|||
pub enum DeserializedAccountConfig {
|
||||
Imap(DeserializedImapAccountConfig),
|
||||
Maildir(DeserializedMaildirAccountConfig),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Notmuch(DeserializedNotmuchAccountConfig),
|
||||
}
|
||||
|
||||
impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig {
|
||||
|
@ -18,6 +20,8 @@ impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig {
|
|||
match self {
|
||||
Self::Imap(config) => config.to_base(),
|
||||
Self::Maildir(config) => config.to_base(),
|
||||
#[cfg(feature = "notmuch")]
|
||||
Self::Notmuch(config) => config.to_base(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,12 +41,6 @@ macro_rules! make_account_config {
|
|||
pub signature_delimiter: Option<String>,
|
||||
/// Overrides the default page size for this account.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Overrides the inbox folder name for this account.
|
||||
pub inbox_folder: Option<String>,
|
||||
/// Overrides the sent folder name for this account.
|
||||
pub sent_folder: Option<String>,
|
||||
/// Overrides the draft folder name for this account.
|
||||
pub draft_folder: Option<String>,
|
||||
/// Overrides the notify command for this account.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the IMAP query used to fetch new messages for this account.
|
||||
|
@ -73,6 +71,10 @@ macro_rules! make_account_config {
|
|||
/// Represents the command used to decrypt a message.
|
||||
pub pgp_decrypt_cmd: Option<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
#[serde(default)]
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
$(pub $element: $ty),*
|
||||
}
|
||||
|
||||
|
@ -84,9 +86,6 @@ macro_rules! make_account_config {
|
|||
signature: self.signature.clone(),
|
||||
signature_delimiter: self.signature_delimiter.clone(),
|
||||
default_page_size: self.default_page_size.clone(),
|
||||
inbox_folder: self.inbox_folder.clone(),
|
||||
sent_folder: self.sent_folder.clone(),
|
||||
draft_folder: self.draft_folder.clone(),
|
||||
notify_cmd: self.notify_cmd.clone(),
|
||||
notify_query: self.notify_query.clone(),
|
||||
watch_cmds: self.watch_cmds.clone(),
|
||||
|
@ -103,6 +102,8 @@ macro_rules! make_account_config {
|
|||
|
||||
pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(),
|
||||
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
|
||||
|
||||
mailboxes: self.mailboxes.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,4 +122,10 @@ make_account_config!(
|
|||
imap_passwd_cmd: String
|
||||
);
|
||||
|
||||
make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: PathBuf);
|
||||
make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String);
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
make_account_config!(
|
||||
DeserializedNotmuchAccountConfig,
|
||||
notmuch_database_dir: String
|
||||
);
|
||||
|
|
|
@ -27,12 +27,6 @@ pub struct DeserializedConfig {
|
|||
pub signature_delimiter: Option<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Overrides the default inbox folder name "INBOX".
|
||||
pub inbox_folder: Option<String>,
|
||||
/// Overrides the default sent folder name "Sent".
|
||||
pub sent_folder: Option<String>,
|
||||
/// Overrides the default draft folder name "Drafts".
|
||||
pub draft_folder: Option<String>,
|
||||
/// Represents the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the default IMAP query "NEW" used to fetch new messages
|
||||
|
@ -48,12 +42,12 @@ pub struct DeserializedConfig {
|
|||
impl DeserializedConfig {
|
||||
/// Tries to create a config from an optional path.
|
||||
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
|
||||
info!("begin: trying to parse config from path");
|
||||
info!("begin: try to parse config from path");
|
||||
debug!("path: {:?}", path);
|
||||
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
let config = toml::from_str(&content).context("cannot parse config file")?;
|
||||
info!("end: trying to parse config from path");
|
||||
info!("end: try to parse config from path");
|
||||
trace!("config: {:?}", config);
|
||||
Ok(config)
|
||||
}
|
||||
|
|
17
src/lib.rs
17
src/lib.rs
|
@ -37,6 +37,9 @@ pub mod backends {
|
|||
pub use backend::*;
|
||||
pub mod backend;
|
||||
|
||||
pub use id_mapper::*;
|
||||
pub mod id_mapper;
|
||||
|
||||
pub use self::imap::*;
|
||||
pub mod imap {
|
||||
pub mod imap_arg;
|
||||
|
@ -75,6 +78,20 @@ pub mod backends {
|
|||
pub mod maildir_flag;
|
||||
pub use maildir_flag::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub use self::notmuch::*;
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub mod notmuch {
|
||||
pub mod notmuch_backend;
|
||||
pub use notmuch_backend::*;
|
||||
|
||||
pub mod notmuch_mbox;
|
||||
pub use notmuch_mbox::*;
|
||||
|
||||
pub mod notmuch_envelope;
|
||||
pub use notmuch_envelope::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod smtp {
|
||||
|
|
37
src/main.rs
37
src/main.rs
|
@ -5,13 +5,19 @@ use url::Url;
|
|||
use himalaya::{
|
||||
backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend},
|
||||
compl::{compl_arg, compl_handler},
|
||||
config::{account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig},
|
||||
config::{
|
||||
account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig,
|
||||
DEFAULT_INBOX_FOLDER,
|
||||
},
|
||||
mbox::{mbox_arg, mbox_handler},
|
||||
msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler},
|
||||
output::{output_arg, OutputFmt, StdoutPrinter},
|
||||
smtp::LettreService,
|
||||
};
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig};
|
||||
|
||||
fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||
clap::App::new(env!("CARGO_PKG_NAME"))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
|
@ -45,6 +51,10 @@ fn main() -> Result<()> {
|
|||
|
||||
let mut imap;
|
||||
let mut maildir;
|
||||
#[cfg(feature = "notmuch")]
|
||||
let maildir_config: MaildirBackendConfig;
|
||||
#[cfg(feature = "notmuch")]
|
||||
let mut notmuch;
|
||||
let backend: Box<&mut dyn Backend> = match backend_config {
|
||||
BackendConfig::Imap(ref imap_config) => {
|
||||
imap = ImapBackend::new(&account_config, imap_config);
|
||||
|
@ -54,6 +64,15 @@ fn main() -> Result<()> {
|
|||
maildir = MaildirBackend::new(&account_config, maildir_config);
|
||||
Box::new(&mut maildir)
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
BackendConfig::Notmuch(ref notmuch_config) => {
|
||||
maildir_config = MaildirBackendConfig {
|
||||
maildir_dir: notmuch_config.notmuch_database_dir.clone(),
|
||||
};
|
||||
maildir = MaildirBackend::new(&account_config, &maildir_config);
|
||||
notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?;
|
||||
Box::new(&mut notmuch)
|
||||
}
|
||||
};
|
||||
|
||||
return msg_handler::mailto(&url, &account_config, &mut printer, backend, &mut smtp);
|
||||
|
@ -77,10 +96,15 @@ fn main() -> Result<()> {
|
|||
AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?;
|
||||
let mbox = m
|
||||
.value_of("mbox-source")
|
||||
.unwrap_or(&account_config.inbox_folder);
|
||||
.or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str()))
|
||||
.unwrap_or(DEFAULT_INBOX_FOLDER);
|
||||
let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
|
||||
let mut imap;
|
||||
let mut maildir;
|
||||
#[cfg(feature = "notmuch")]
|
||||
let maildir_config: MaildirBackendConfig;
|
||||
#[cfg(feature = "notmuch")]
|
||||
let mut notmuch;
|
||||
let backend: Box<&mut dyn Backend> = match backend_config {
|
||||
BackendConfig::Imap(ref imap_config) => {
|
||||
imap = ImapBackend::new(&account_config, imap_config);
|
||||
|
@ -90,6 +114,15 @@ fn main() -> Result<()> {
|
|||
maildir = MaildirBackend::new(&account_config, maildir_config);
|
||||
Box::new(&mut maildir)
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
BackendConfig::Notmuch(ref notmuch_config) => {
|
||||
maildir_config = MaildirBackendConfig {
|
||||
maildir_dir: notmuch_config.notmuch_database_dir.clone(),
|
||||
};
|
||||
maildir = MaildirBackend::new(&account_config, &maildir_config);
|
||||
notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?;
|
||||
Box::new(&mut notmuch)
|
||||
}
|
||||
};
|
||||
|
||||
let mut smtp = LettreService::from(&account_config);
|
||||
|
|
|
@ -120,7 +120,10 @@ mod tests {
|
|||
fn del_mbox(&mut self, _: &str) -> Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_envelopes(
|
||||
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result<Box<dyn Envelopes>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
|
|
|
@ -5,12 +5,12 @@ use html_escape;
|
|||
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
|
||||
use log::{debug, info, trace};
|
||||
use regex::Regex;
|
||||
use std::{collections::HashSet, env::temp_dir, fmt::Debug, fs, path::PathBuf};
|
||||
use std::{collections::HashSet, convert::TryInto, env::temp_dir, fmt::Debug, fs, path::PathBuf};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::{AccountConfig, DEFAULT_SIG_DELIM},
|
||||
config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM},
|
||||
msg::{
|
||||
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils,
|
||||
Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,
|
||||
|
@ -340,7 +340,12 @@ impl Msg {
|
|||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
let sent_msg = smtp.send_msg(account, &self)?;
|
||||
backend.add_msg(&account.sent_folder, &sent_msg.formatted(), "seen")?;
|
||||
let sent_folder = account
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
backend.add_msg(&sent_folder, &sent_msg.formatted(), "seen")?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
printer.print("Message successfully sent")?;
|
||||
break;
|
||||
|
@ -355,12 +360,14 @@ impl Msg {
|
|||
}
|
||||
Ok(PostEditChoice::RemoteDraft) => {
|
||||
let tpl = self.to_tpl(TplOverride::default(), account)?;
|
||||
backend.add_msg(&account.draft_folder, tpl.as_bytes(), "seen draft")?;
|
||||
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(format!(
|
||||
"Message successfully saved to {}",
|
||||
account.draft_folder
|
||||
))?;
|
||||
printer.print(format!("Message successfully saved to {}", draft_folder))?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Discard) => {
|
||||
|
|
|
@ -16,7 +16,7 @@ use url::Url;
|
|||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::AccountConfig,
|
||||
config::{AccountConfig, DEFAULT_SENT_FOLDER},
|
||||
msg::{Msg, Part, Parts, TextPlainPart},
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
smtp::SmtpService,
|
||||
|
@ -108,7 +108,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = imap.get_envelopes(mbox, "arrival:desc", "all", page_size, page)?;
|
||||
let msgs = imap.get_envelopes(mbox, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(msgs, PrintTableOpts { max_width })
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.get_envelopes(mbox, "arrival:desc", &query, page_size, page)?;
|
||||
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(msgs, PrintTableOpts { max_width })
|
||||
}
|
||||
|
@ -292,7 +292,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.get_envelopes(mbox, &sort, &query, page_size, page)?;
|
||||
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(msgs, PrintTableOpts { max_width })
|
||||
}
|
||||
|
@ -312,6 +312,13 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let sent_folder = config
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
debug!("sent folder: {:?}", sent_folder);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
|
@ -325,9 +332,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
trace!("raw message: {:?}", raw_msg);
|
||||
let envelope: lettre::address::Envelope = Msg::from_tpl(&raw_msg)?.try_into()?;
|
||||
trace!("envelope: {:?}", envelope);
|
||||
|
||||
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
|
||||
backend.add_msg(&config.sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -2,5 +2,6 @@ From: alice@localhost
|
|||
To: patrick@localhost
|
||||
Subject: Plain message
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Date: Tue, 1 Mar 2022 12:00:00 +0000
|
||||
|
||||
Ceci est un message.
|
|
@ -43,9 +43,7 @@ fn test_imap_backend() {
|
|||
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
|
||||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = imap
|
||||
.get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0)
|
||||
.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 envelope = envelopes.first().unwrap();
|
||||
|
@ -55,28 +53,20 @@ fn test_imap_backend() {
|
|||
// check that the message can be copied
|
||||
imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
|
||||
.unwrap();
|
||||
let envelopes = imap
|
||||
.get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0)
|
||||
.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", "arrival:desc", "ALL", 10, 0)
|
||||
.unwrap();
|
||||
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", "arrival:desc", "ALL", 10, 0)
|
||||
.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", "arrival:desc", "ALL", 10, 0)
|
||||
.unwrap();
|
||||
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();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use maildir::Maildir;
|
||||
use std::{env, fs};
|
||||
use std::{collections::HashMap, env, fs, iter::FromIterator};
|
||||
|
||||
use himalaya::{
|
||||
backends::{Backend, MaildirBackend, MaildirEnvelopes},
|
||||
backends::{Backend, MaildirBackend, MaildirEnvelopes, MaildirFlag},
|
||||
config::{AccountConfig, MaildirBackendConfig},
|
||||
};
|
||||
|
||||
|
@ -19,7 +19,10 @@ fn test_maildir_backend() {
|
|||
|
||||
// configure accounts
|
||||
let account_config = AccountConfig {
|
||||
inbox_folder: "INBOX".into(),
|
||||
mailboxes: HashMap::from_iter([
|
||||
("inbox".into(), "INBOX".into()),
|
||||
("subdir".into(), "Subdir".into()),
|
||||
]),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
let mdir_config = MaildirBackendConfig {
|
||||
|
@ -33,36 +36,64 @@ fn test_maildir_backend() {
|
|||
|
||||
// check that a message can be added
|
||||
let msg = include_bytes!("./emails/alice-to-patrick.eml");
|
||||
let id = mdir.add_msg("INBOX", msg, "seen").unwrap().to_string();
|
||||
let hash = mdir.add_msg("inbox", msg, "seen").unwrap().to_string();
|
||||
|
||||
// check that the added message exists
|
||||
let msg = mdir.get_msg("INBOX", &id).unwrap();
|
||||
let msg = mdir.get_msg("inbox", &hash).unwrap();
|
||||
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
|
||||
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
|
||||
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
|
||||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = mdir.get_envelopes("INBOX", "", "cur", 10, 0).unwrap();
|
||||
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();
|
||||
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));
|
||||
|
||||
// check that the message flags can be changed
|
||||
mdir.set_flags("inbox", &envelope.hash, "passed").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));
|
||||
|
||||
// check that a flag can be removed from the message
|
||||
mdir.del_flags("inbox", &envelope.hash, "passed").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));
|
||||
|
||||
// check that the message can be copied
|
||||
mdir.copy_msg("INBOX", "Subdir", &envelope.id).unwrap();
|
||||
assert!(mdir.get_msg("INBOX", &id).is_ok());
|
||||
assert!(mdir.get_msg("Subdir", &id).is_ok());
|
||||
assert!(mdir_subdir.get_msg("INBOX", &id).is_ok());
|
||||
mdir.copy_msg("inbox", "subdir", &envelope.hash).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.id).unwrap();
|
||||
assert!(mdir.get_msg("INBOX", &id).is_err());
|
||||
assert!(mdir.get_msg("Subdir", &id).is_ok());
|
||||
assert!(mdir_subdir.get_msg("INBOX", &id).is_ok());
|
||||
mdir.move_msg("inbox", "subdir", &envelope.hash).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());
|
||||
|
||||
// check that the message can be deleted
|
||||
mdir.del_msg("Subdir", &id).unwrap();
|
||||
assert!(mdir.get_msg("Subdir", &id).is_err());
|
||||
assert!(mdir_subdir.get_msg("INBOX", &id).is_err());
|
||||
mdir.del_msg("subdir", &hash).unwrap();
|
||||
assert!(mdir.get_msg("subdir", &hash).is_err());
|
||||
assert!(mdir_subdir.get_msg("inbox", &hash).is_err());
|
||||
}
|
||||
|
|
88
tests/test_notmuch_backend.rs
Normal file
88
tests/test_notmuch_backend.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
#[cfg(feature = "notmuch")]
|
||||
use std::{collections::HashMap, env, fs, iter::FromIterator};
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
use himalaya::{
|
||||
backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes},
|
||||
config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig},
|
||||
};
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[test]
|
||||
fn test_notmuch_backend() {
|
||||
// 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()) {}
|
||||
mdir.create_dirs().unwrap();
|
||||
notmuch::Database::create(mdir.path()).unwrap();
|
||||
|
||||
// configure accounts
|
||||
let account_config = AccountConfig {
|
||||
mailboxes: HashMap::from_iter([("inbox".into(), "*".into())]),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
let mdir_config = MaildirBackendConfig {
|
||||
maildir_dir: mdir.path().to_owned(),
|
||||
};
|
||||
let notmuch_config = NotmuchBackendConfig {
|
||||
notmuch_database_dir: mdir.path().to_owned(),
|
||||
};
|
||||
let mut mdir = MaildirBackend::new(&account_config, &mdir_config);
|
||||
let mut notmuch = NotmuchBackend::new(&account_config, ¬much_config, &mut mdir).unwrap();
|
||||
|
||||
// check that a message can be added
|
||||
let msg = include_bytes!("./emails/alice-to-patrick.eml");
|
||||
let hash = notmuch.add_msg("", msg, "inbox seen").unwrap().to_string();
|
||||
|
||||
// check that the added message exists
|
||||
let msg = notmuch.get_msg("", &hash).unwrap();
|
||||
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
|
||||
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
|
||||
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
|
||||
|
||||
// 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);
|
||||
assert_eq!("Plain message", envelope.subject);
|
||||
|
||||
// check that a flag can be added to the message
|
||||
notmuch
|
||||
.add_flags("", &envelope.hash, "flagged passed")
|
||||
.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()));
|
||||
|
||||
// check that the message flags can be changed
|
||||
notmuch
|
||||
.set_flags("", &envelope.hash, "inbox passed")
|
||||
.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()));
|
||||
|
||||
// check that a flag can be removed from the message
|
||||
notmuch.del_flags("", &envelope.hash, "passed").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()));
|
||||
|
||||
// check that the message can be deleted
|
||||
notmuch.del_msg("", &hash).unwrap();
|
||||
assert!(notmuch.get_msg("inbox", &hash).is_err());
|
||||
}
|
Loading…
Reference in a new issue