mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 17:15:12 +00:00
Merge branch 'development'
This commit is contained in:
commit
7b9cfc4512
42
.gitignore
vendored
42
.gitignore
vendored
|
@ -1,10 +1,46 @@
|
|||
# Cargo build directory
|
||||
/target
|
||||
target/
|
||||
debug/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Nix build directory
|
||||
/result
|
||||
/result-lib
|
||||
result
|
||||
result-*
|
||||
|
||||
# Direnv
|
||||
/.envrc
|
||||
/.direnv
|
||||
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
.idea/
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
## Others
|
||||
.metadata/
|
||||
|
|
78
Cargo.lock
generated
78
Cargo.lock
generated
|
@ -235,6 +235,22 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-encoding"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b91dddc343e7eaa27f9764e5bffe57370d957017fdd75244f5045e829a8441"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"memchr 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8684b7c9cb4857dfa1e5b9629ef584ba618c9b93bae60f58cb23f4f271d0468e"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.30"
|
||||
|
@ -441,6 +457,7 @@ dependencies = [
|
|||
"convert_case",
|
||||
"env_logger",
|
||||
"erased-serde",
|
||||
"himalaya-lib",
|
||||
"html-escape",
|
||||
"imap",
|
||||
"imap-proto",
|
||||
|
@ -468,6 +485,29 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "himalaya-lib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"chrono",
|
||||
"convert_case",
|
||||
"html-escape",
|
||||
"imap",
|
||||
"imap-proto",
|
||||
"lettre",
|
||||
"log",
|
||||
"maildir",
|
||||
"mailparse",
|
||||
"md5",
|
||||
"native-tls",
|
||||
"notmuch",
|
||||
"regex",
|
||||
"rfc2047-decoder",
|
||||
"serde",
|
||||
"shellexpand",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"tree_magic",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
|
@ -590,11 +630,13 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.10.0-rc.4"
|
||||
version = "0.10.0-rc.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d8da8f34d086b081c9cc3b57d3bb3b51d16fc06b5c848a188e2f14d58ac2a5"
|
||||
checksum = "0f7e87d9d44162eea7abd87b1a7540fcb10d5e58e8bb4f173178f3dc6e453944"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"email-encoding",
|
||||
"email_address",
|
||||
"fastrand",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
|
@ -605,8 +647,8 @@ dependencies = [
|
|||
"nom 7.1.1",
|
||||
"once_cell",
|
||||
"quoted_printable",
|
||||
"regex",
|
||||
"serde",
|
||||
"socket2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1257,6 +1299,16 @@ version = "1.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.3"
|
||||
|
@ -1365,6 +1417,26 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[workspace]
|
||||
members = ["lib", "cli"]
|
||||
members = ["lib", "cli"]
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
*📢 Announcement: Himalaya receives support help, see the
|
||||
[discussion](https://github.com/soywod/himalaya/discussions/399) for
|
||||
more details.*
|
||||
|
||||
# 📫 Himalaya
|
||||
|
||||
Command-line interface for email management
|
||||
|
|
1
cli/.gitignore
vendored
Normal file
1
cli/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Cargo.lock
|
|
@ -4,8 +4,8 @@ description = "Command-line interface for email management"
|
|||
version = "0.5.10"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2018"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
license-file = "../LICENSE"
|
||||
readme = "../README.md"
|
||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||
keywords = ["cli", "mail", "email", "client", "imap"]
|
||||
homepage = "https://github.com/soywod/himalaya/wiki"
|
||||
|
@ -31,8 +31,9 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions"
|
|||
convert_case = "0.5.0"
|
||||
env_logger = "0.8.3"
|
||||
erased-serde = "0.3.18"
|
||||
himalaya-lib = { path = "../lib" }
|
||||
html-escape = "0.2.9"
|
||||
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
|
||||
lettre = { version = "0.10.0-rc.7", features = ["serde"] }
|
||||
log = "0.4.14"
|
||||
mailparse = "0.13.6"
|
||||
native-tls = "0.2.8"
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
//! IMAP envelope module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the envelope.
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use std::{convert::TryFrom, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use super::{ImapFlag, ImapFlags};
|
||||
|
||||
/// Represents a list of IMAP envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct ImapEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<ImapEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for ImapEnvelopes {
|
||||
type Target = Vec<ImapEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for ImapEnvelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// impl Envelopes for ImapEnvelopes {
|
||||
// //
|
||||
// }
|
||||
|
||||
/// Represents the IMAP envelope. The envelope is just a message
|
||||
/// subset, and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct ImapEnvelope {
|
||||
/// Represents the sequence number of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
||||
pub id: u32,
|
||||
|
||||
/// Represents the flags attached to the message.
|
||||
pub flags: ImapFlags,
|
||||
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
|
||||
/// Represents the internal date of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
|
||||
pub date: Option<String>,
|
||||
}
|
||||
|
||||
impl Table for ImapEnvelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("SENDER").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let flags = self.flags.to_symbols_string();
|
||||
let unseen = !self.flags.contains(&ImapFlag::Seen);
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = self.date.as_deref().unwrap_or_default();
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw envelopes returned by the `imap` crate.
|
||||
pub type RawImapEnvelopes = imap::types::ZeroCopy<Vec<RawImapEnvelope>>;
|
||||
|
||||
impl TryFrom<RawImapEnvelopes> for ImapEnvelopes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_envelopes: RawImapEnvelopes) -> Result<Self, Self::Error> {
|
||||
let mut envelopes = vec![];
|
||||
for raw_envelope in raw_envelopes.iter().rev() {
|
||||
envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?);
|
||||
}
|
||||
Ok(Self { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw envelope returned by the `imap` crate.
|
||||
pub type RawImapEnvelope = imap::types::Fetch;
|
||||
|
||||
impl TryFrom<&RawImapEnvelope> for ImapEnvelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fetch: &RawImapEnvelope) -> Result<ImapEnvelope> {
|
||||
let envelope = fetch
|
||||
.envelope()
|
||||
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
|
||||
|
||||
// Get the sequence number
|
||||
let id = fetch.message;
|
||||
|
||||
// Get the flags
|
||||
let flags = ImapFlags::try_from(fetch.flags())?;
|
||||
|
||||
// Get the subject
|
||||
let subject = envelope
|
||||
.subject
|
||||
.as_ref()
|
||||
.map(|subj| {
|
||||
rfc2047_decoder::decode(subj).context(format!(
|
||||
"cannot decode subject of message {}",
|
||||
fetch.message
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| Ok(String::default()))?;
|
||||
|
||||
// Get the sender
|
||||
let sender = envelope
|
||||
.sender
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.get(0))
|
||||
.or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0)))
|
||||
.ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?;
|
||||
let sender = if let Some(ref name) = sender.name {
|
||||
rfc2047_decoder::decode(&name.to_vec()).context(format!(
|
||||
"cannot decode sender's name of message {}",
|
||||
fetch.message,
|
||||
))?
|
||||
} else {
|
||||
let mbox = sender
|
||||
.mailbox
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message))
|
||||
.and_then(|mbox| {
|
||||
rfc2047_decoder::decode(&mbox.to_vec()).context(format!(
|
||||
"cannot decode sender's mailbox of message {}",
|
||||
fetch.message,
|
||||
))
|
||||
})?;
|
||||
let host = sender
|
||||
.host
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message))
|
||||
.and_then(|host| {
|
||||
rfc2047_decoder::decode(&host.to_vec()).context(format!(
|
||||
"cannot decode sender's host of message {}",
|
||||
fetch.message,
|
||||
))
|
||||
})?;
|
||||
format!("{}@{}", mbox, host)
|
||||
};
|
||||
|
||||
// Get the internal date
|
||||
let date = fetch
|
||||
.internal_date()
|
||||
.map(|date| date.naive_local().to_string());
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
flags,
|
||||
subject,
|
||||
sender,
|
||||
date,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the imap flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum ImapFlag {
|
||||
Seen,
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Draft,
|
||||
Recent,
|
||||
MayCreate,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&str> for ImapFlag {
|
||||
fn from(flag_str: &str) -> Self {
|
||||
match flag_str {
|
||||
"seen" => ImapFlag::Seen,
|
||||
"answered" => ImapFlag::Answered,
|
||||
"flagged" => ImapFlag::Flagged,
|
||||
"deleted" => ImapFlag::Deleted,
|
||||
"draft" => ImapFlag::Draft,
|
||||
"recent" => ImapFlag::Recent,
|
||||
"maycreate" | "may-create" => ImapFlag::MayCreate,
|
||||
flag_str => ImapFlag::Custom(flag_str.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&imap::types::Flag<'_>> for ImapFlag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flag: &imap::types::Flag<'_>) -> Result<Self, Self::Error> {
|
||||
Ok(match flag {
|
||||
imap::types::Flag::Seen => ImapFlag::Seen,
|
||||
imap::types::Flag::Answered => ImapFlag::Answered,
|
||||
imap::types::Flag::Flagged => ImapFlag::Flagged,
|
||||
imap::types::Flag::Deleted => ImapFlag::Deleted,
|
||||
imap::types::Flag::Draft => ImapFlag::Draft,
|
||||
imap::types::Flag::Recent => ImapFlag::Recent,
|
||||
imap::types::Flag::MayCreate => ImapFlag::MayCreate,
|
||||
imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()),
|
||||
_ => return Err(anyhow!("cannot parse imap flag")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the imap flags.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct ImapFlags(pub Vec<ImapFlag>);
|
||||
|
||||
impl ImapFlags {
|
||||
/// Builds a symbols string
|
||||
pub fn to_symbols_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if self.contains(&ImapFlag::Seen) {
|
||||
" "
|
||||
} else {
|
||||
"✷"
|
||||
});
|
||||
flags.push_str(if self.contains(&ImapFlag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&ImapFlag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ImapFlags {
|
||||
type Target = Vec<ImapFlag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ImapFlags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut glue = "";
|
||||
|
||||
for flag in &self.0 {
|
||||
write!(f, "{}", glue)?;
|
||||
match flag {
|
||||
ImapFlag::Seen => write!(f, "\\Seen")?,
|
||||
ImapFlag::Answered => write!(f, "\\Answered")?,
|
||||
ImapFlag::Flagged => write!(f, "\\Flagged")?,
|
||||
ImapFlag::Deleted => write!(f, "\\Deleted")?,
|
||||
ImapFlag::Draft => write!(f, "\\Draft")?,
|
||||
ImapFlag::Recent => write!(f, "\\Recent")?,
|
||||
ImapFlag::MayCreate => write!(f, "\\MayCreate")?,
|
||||
ImapFlag::Custom(custom) => write!(f, "{}", custom)?,
|
||||
}
|
||||
glue = " ";
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<Vec<imap::types::Flag<'a>>> for ImapFlags {
|
||||
fn into(self) -> Vec<imap::types::Flag<'a>> {
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|flag| match flag {
|
||||
ImapFlag::Seen => imap::types::Flag::Seen,
|
||||
ImapFlag::Answered => imap::types::Flag::Answered,
|
||||
ImapFlag::Flagged => imap::types::Flag::Flagged,
|
||||
ImapFlag::Deleted => imap::types::Flag::Deleted,
|
||||
ImapFlag::Draft => imap::types::Flag::Draft,
|
||||
ImapFlag::Recent => imap::types::Flag::Recent,
|
||||
ImapFlag::MayCreate => imap::types::Flag::MayCreate,
|
||||
ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ImapFlags {
|
||||
fn from(flags_str: &str) -> Self {
|
||||
ImapFlags(
|
||||
flags_str
|
||||
.split_whitespace()
|
||||
.map(|flag_str| flag_str.trim().into())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flags: &[imap::types::Flag<'_>]) -> Result<Self, Self::Error> {
|
||||
let mut f = vec![];
|
||||
for flag in flags {
|
||||
f.push(flag.try_into()?);
|
||||
}
|
||||
Ok(Self(f))
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
//! IMAP mailbox module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the mailbox.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::mbox::Mboxes;
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
use super::ImapMboxAttrs;
|
||||
|
||||
/// Represents a list of IMAP mailboxes.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ImapMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<ImapMbox>,
|
||||
}
|
||||
|
||||
impl Deref for ImapMboxes {
|
||||
type Target = Vec<ImapMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for ImapMboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for ImapMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the IMAP mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct ImapMbox {
|
||||
/// Represents the mailbox hierarchie delimiter.
|
||||
pub delim: String,
|
||||
|
||||
/// Represents the mailbox name.
|
||||
pub name: String,
|
||||
|
||||
/// Represents the mailbox attributes.
|
||||
pub attrs: ImapMboxAttrs,
|
||||
}
|
||||
|
||||
impl ImapMbox {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImapMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for ImapMbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("DELIM").bold().underline().white())
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(
|
||||
Cell::new("ATTRIBUTES")
|
||||
.shrinkable()
|
||||
.bold()
|
||||
.underline()
|
||||
.white(),
|
||||
)
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.delim).white())
|
||||
.cell(Cell::new(&self.name).green())
|
||||
.cell(Cell::new(&self.attrs.to_string()).shrinkable().blue())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backends::ImapMboxAttr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_create_new_mbox() {
|
||||
assert_eq!(ImapMbox::default(), ImapMbox::new(""));
|
||||
assert_eq!(
|
||||
ImapMbox {
|
||||
name: "INBOX".into(),
|
||||
..ImapMbox::default()
|
||||
},
|
||||
ImapMbox::new("INBOX")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_display_mbox() {
|
||||
let default_mbox = ImapMbox::default();
|
||||
assert_eq!("", default_mbox.to_string());
|
||||
|
||||
let new_mbox = ImapMbox::new("INBOX");
|
||||
assert_eq!("INBOX", new_mbox.to_string());
|
||||
|
||||
let full_mbox = ImapMbox {
|
||||
delim: ".".into(),
|
||||
name: "Sent".into(),
|
||||
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||
};
|
||||
assert_eq!("Sent", full_mbox.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw mailboxes returned by the `imap` crate.
|
||||
pub type RawImapMboxes = imap::types::ZeroCopy<Vec<RawImapMbox>>;
|
||||
|
||||
impl<'a> From<RawImapMboxes> for ImapMboxes {
|
||||
fn from(raw_mboxes: RawImapMboxes) -> Self {
|
||||
Self {
|
||||
mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw mailbox returned by the `imap` crate.
|
||||
pub type RawImapMbox = imap::types::Name;
|
||||
|
||||
impl<'a> From<&'a RawImapMbox> for ImapMbox {
|
||||
fn from(raw_mbox: &'a RawImapMbox) -> Self {
|
||||
Self {
|
||||
delim: raw_mbox.delimiter().unwrap_or_default().into(),
|
||||
name: raw_mbox.name().into(),
|
||||
attrs: raw_mbox.attributes().into(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
//! IMAP mailbox attribute module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the mailbox attribute.
|
||||
|
||||
/// Represents the raw mailbox attribute returned by the `imap` crate.
|
||||
pub use imap::types::NameAttribute as RawImapMboxAttr;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the attributes of the mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct ImapMboxAttrs(pub Vec<ImapMboxAttr>);
|
||||
|
||||
impl Deref for ImapMboxAttrs {
|
||||
type Target = Vec<ImapMboxAttr>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImapMboxAttrs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut glue = "";
|
||||
for attr in self.iter() {
|
||||
write!(f, "{}{}", glue, attr)?;
|
||||
glue = ", ";
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
|
||||
pub enum ImapMboxAttr {
|
||||
NoInferiors,
|
||||
NoSelect,
|
||||
Marked,
|
||||
Unmarked,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// Makes the attribute displayable.
|
||||
impl Display for ImapMboxAttr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"),
|
||||
ImapMboxAttr::NoSelect => write!(f, "NoSelect"),
|
||||
ImapMboxAttr::Marked => write!(f, "Marked"),
|
||||
ImapMboxAttr::Unmarked => write!(f, "Unmarked"),
|
||||
ImapMboxAttr::Custom(custom) => write!(f, "{}", custom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_display_attrs() {
|
||||
macro_rules! attrs_from {
|
||||
($($attr:expr),*) => {
|
||||
ImapMboxAttrs(vec![$($attr,)*]).to_string()
|
||||
};
|
||||
}
|
||||
|
||||
let empty_attr = attrs_from![];
|
||||
let single_attr = attrs_from![ImapMboxAttr::NoInferiors];
|
||||
let multiple_attrs = attrs_from![
|
||||
ImapMboxAttr::Custom("AttrCustom".into()),
|
||||
ImapMboxAttr::NoInferiors
|
||||
];
|
||||
|
||||
assert_eq!("", empty_attr);
|
||||
assert_eq!("NoInferiors", single_attr);
|
||||
assert!(multiple_attrs.contains("NoInferiors"));
|
||||
assert!(multiple_attrs.contains("AttrCustom"));
|
||||
assert!(multiple_attrs.contains(","));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_display_attr() {
|
||||
macro_rules! attr_from {
|
||||
($attr:ident) => {
|
||||
ImapMboxAttr::$attr.to_string()
|
||||
};
|
||||
($custom:literal) => {
|
||||
ImapMboxAttr::Custom($custom.into()).to_string()
|
||||
};
|
||||
}
|
||||
|
||||
assert_eq!("NoInferiors", attr_from![NoInferiors]);
|
||||
assert_eq!("NoSelect", attr_from![NoSelect]);
|
||||
assert_eq!("Marked", attr_from![Marked]);
|
||||
assert_eq!("Unmarked", attr_from![Unmarked]);
|
||||
assert_eq!("CustomAttr", attr_from!["CustomAttr"]);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs {
|
||||
fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self {
|
||||
Self(raw_attrs.iter().map(ImapMboxAttr::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr {
|
||||
fn from(attr: &'a RawImapMboxAttr<'a>) -> Self {
|
||||
match attr {
|
||||
RawImapMboxAttr::NoInferiors => Self::NoInferiors,
|
||||
RawImapMboxAttr::NoSelect => Self::NoSelect,
|
||||
RawImapMboxAttr::Marked => Self::Marked,
|
||||
RawImapMboxAttr::Unmarked => Self::Unmarked,
|
||||
RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,493 +0,0 @@
|
|||
//! 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, env, fs, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
|
||||
config::{AccountConfig, MaildirBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
/// Represents the maildir backend.
|
||||
pub struct MaildirBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
mdir: maildir::Maildir,
|
||||
}
|
||||
|
||||
impl<'a> MaildirBackend<'a> {
|
||||
pub fn new(
|
||||
account_config: &'a AccountConfig,
|
||||
maildir_config: &'a MaildirBackendConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
account_config,
|
||||
mdir: maildir_config.maildir_dir.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result<PathBuf> {
|
||||
if mdir_path.is_dir() {
|
||||
Ok(mdir_path)
|
||||
} else {
|
||||
Err(anyhow!("cannot read maildir directory {:?}", mdir_path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a maildir instance from a string slice.
|
||||
pub fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> {
|
||||
let dir = self.account_config.get_mbox_alias(dir)?;
|
||||
|
||||
// If the dir points to the inbox folder, creates a maildir
|
||||
// instance from the root folder.
|
||||
if &dir == "inbox" {
|
||||
return self
|
||||
.validate_mdir_path(self.mdir.path().to_owned())
|
||||
.map(maildir::Maildir::from);
|
||||
}
|
||||
|
||||
// If the dir is a valid maildir path, creates a maildir
|
||||
// instance from it. First checks for absolute path,
|
||||
self.validate_mdir_path((&dir).into())
|
||||
// then for relative path to `maildir-dir`,
|
||||
.or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir)))
|
||||
// and finally for relative path to the current directory.
|
||||
.or_else(|_| self.validate_mdir_path(env::current_dir()?.join(&dir)))
|
||||
.or_else(|_| {
|
||||
// Otherwise creates a maildir instance from a maildir
|
||||
// subdirectory by adding a "." in front of the name
|
||||
// as described in the [spec].
|
||||
//
|
||||
// [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
|
||||
self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir)))
|
||||
})
|
||||
.map(maildir::Maildir::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Backend<'a> for MaildirBackend<'a> {
|
||||
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>> {
|
||||
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, 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,
|
||||
dir: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
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!(
|
||||
"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.envelopes = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
// 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 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())
|
||||
.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()
|
||||
)
|
||||
})?;
|
||||
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 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 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 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 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 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 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);
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
//! Maildir mailbox module.
|
||||
//!
|
||||
//! This module provides Maildir types and conversion utilities
|
||||
//! related to the envelope
|
||||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::DateTime;
|
||||
use log::trace;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backends::{MaildirFlag, MaildirFlags},
|
||||
msg::{from_slice_to_addrs, Addr},
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct MaildirEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<MaildirEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for MaildirEnvelopes {
|
||||
type Target = Vec<MaildirEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for MaildirEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for MaildirEnvelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// impl Envelopes for MaildirEnvelopes {
|
||||
// //
|
||||
// }
|
||||
|
||||
/// Represents the envelope. The envelope is just a message subset,
|
||||
/// and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct MaildirEnvelope {
|
||||
/// Represents the id of the message.
|
||||
pub id: String,
|
||||
|
||||
/// Represents the MD5 hash of the message id.
|
||||
pub hash: String,
|
||||
|
||||
/// Represents the flags of the message.
|
||||
pub flags: MaildirFlags,
|
||||
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
|
||||
/// Represents the date of the message.
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl Table for MaildirEnvelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("HASH").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("SENDER").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let hash = self.hash.clone();
|
||||
let unseen = !self.flags.contains(&MaildirFlag::Seen);
|
||||
let flags = self.flags.to_symbols_string();
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = &self.date;
|
||||
Row::new()
|
||||
.cell(Cell::new(hash).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw envelopees returned by the `maildir` crate.
|
||||
pub type RawMaildirEnvelopes = maildir::MailEntries;
|
||||
|
||||
impl<'a> TryFrom<RawMaildirEnvelopes> for MaildirEnvelopes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mail_entries: RawMaildirEnvelopes) -> Result<Self, Self::Error> {
|
||||
let mut envelopes = vec![];
|
||||
for entry in mail_entries {
|
||||
let envelope: MaildirEnvelope = entry
|
||||
.context("cannot decode maildir mail entry")?
|
||||
.try_into()
|
||||
.context("cannot parse maildir mail entry")?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
|
||||
Ok(MaildirEnvelopes { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw envelope returned by the `maildir` crate.
|
||||
pub type RawMaildirEnvelope = maildir::MailEntry;
|
||||
|
||||
impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
|
||||
trace!(">> build envelope from maildir parsed mail");
|
||||
|
||||
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()
|
||||
.context("cannot parse maildir mail entry")?;
|
||||
|
||||
trace!(">> parse headers");
|
||||
for h in parsed_mail.get_headers() {
|
||||
let k = h.get_key();
|
||||
trace!("header key: {:?}", k);
|
||||
|
||||
let v = rfc2047_decoder::decode(h.get_value_raw())
|
||||
.context(format!("cannot decode value from header {:?}", k))?;
|
||||
trace!("header value: {:?}", v);
|
||||
|
||||
match k.to_lowercase().as_str() {
|
||||
"date" => {
|
||||
envelope.date =
|
||||
DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0)
|
||||
.context(format!("cannot parse maildir message date {:?}", v))?
|
||||
.naive_local()
|
||||
.to_string();
|
||||
}
|
||||
"subject" => {
|
||||
envelope.subject = v.into();
|
||||
}
|
||||
"from" => {
|
||||
envelope.sender = from_slice_to_addrs(v)
|
||||
.context(format!("cannot parse header {:?}", k))?
|
||||
.and_then(|senders| {
|
||||
if senders.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(senders)
|
||||
}
|
||||
})
|
||||
.map(|senders| match &senders[0] {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
|
||||
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
|
||||
}
|
||||
Addr::Group(mailparse::GroupInfo { group_name, .. }) => {
|
||||
group_name.to_owned()
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find sender"))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
trace!("<< parse headers");
|
||||
|
||||
trace!("envelope: {:?}", envelope);
|
||||
trace!("<< build envelope from maildir parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Represents the maildir flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum MaildirFlag {
|
||||
Passed,
|
||||
Replied,
|
||||
Seen,
|
||||
Trashed,
|
||||
Draft,
|
||||
Flagged,
|
||||
Custom(char),
|
||||
}
|
||||
|
||||
/// Represents the maildir flags.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct MaildirFlags(pub Vec<MaildirFlag>);
|
||||
|
||||
impl MaildirFlags {
|
||||
/// Builds a symbols string
|
||||
pub fn to_symbols_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if self.contains(&MaildirFlag::Seen) {
|
||||
" "
|
||||
} else {
|
||||
"✷"
|
||||
});
|
||||
flags.push_str(if self.contains(&MaildirFlag::Replied) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&MaildirFlag::Passed) {
|
||||
"↗"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&MaildirFlag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MaildirFlags {
|
||||
type Target = Vec<MaildirFlag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for MaildirFlags {
|
||||
fn to_string(&self) -> String {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|flag| {
|
||||
let flag_char: char = flag.into();
|
||||
flag_char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for MaildirFlags {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flags_str: &str) -> Result<Self, Self::Error> {
|
||||
let mut flags = vec![];
|
||||
for flag_str in flags_str.split_whitespace() {
|
||||
flags.push(flag_str.trim().try_into()?);
|
||||
}
|
||||
Ok(MaildirFlags(flags))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&maildir::MailEntry> for MaildirFlags {
|
||||
fn from(mail_entry: &maildir::MailEntry) -> Self {
|
||||
let mut flags = vec![];
|
||||
for c in mail_entry.flags().chars() {
|
||||
flags.push(match c {
|
||||
'P' => MaildirFlag::Passed,
|
||||
'R' => MaildirFlag::Replied,
|
||||
'S' => MaildirFlag::Seen,
|
||||
'T' => MaildirFlag::Trashed,
|
||||
'D' => MaildirFlag::Draft,
|
||||
'F' => MaildirFlag::Flagged,
|
||||
custom => MaildirFlag::Custom(custom),
|
||||
})
|
||||
}
|
||||
Self(flags)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<char> for &MaildirFlag {
|
||||
fn into(self) -> char {
|
||||
match self {
|
||||
MaildirFlag::Passed => 'P',
|
||||
MaildirFlag::Replied => 'R',
|
||||
MaildirFlag::Seen => 'S',
|
||||
MaildirFlag::Trashed => 'T',
|
||||
MaildirFlag::Draft => 'D',
|
||||
MaildirFlag::Flagged => 'F',
|
||||
MaildirFlag::Custom(custom) => *custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for MaildirFlag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(flag_str: &str) -> Result<Self, Self::Error> {
|
||||
match flag_str {
|
||||
"passed" => Ok(MaildirFlag::Passed),
|
||||
"replied" => Ok(MaildirFlag::Replied),
|
||||
"seen" => Ok(MaildirFlag::Seen),
|
||||
"trashed" => Ok(MaildirFlag::Trashed),
|
||||
"draft" => Ok(MaildirFlag::Draft),
|
||||
"flagged" => Ok(MaildirFlag::Flagged),
|
||||
flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
//! Maildir mailbox module.
|
||||
//!
|
||||
//! This module provides Maildir types and conversion utilities
|
||||
//! related to the mailbox
|
||||
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ffi::OsStr,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mbox::Mboxes,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of Maildir mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct MaildirMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<MaildirMbox>,
|
||||
}
|
||||
|
||||
impl Deref for MaildirMboxes {
|
||||
type Target = Vec<MaildirMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for MaildirMboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for MaildirMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct MaildirMbox {
|
||||
/// Represents the mailbox name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl MaildirMbox {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self { name: name.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MaildirMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for MaildirMbox {
|
||||
fn head() -> Row {
|
||||
Row::new().cell(Cell::new("SUBDIR").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new().cell(Cell::new(&self.name).green())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_create_new_mbox() {
|
||||
assert_eq!(MaildirMbox::default(), MaildirMbox::new(""));
|
||||
assert_eq!(
|
||||
MaildirMbox {
|
||||
name: "INBOX".into(),
|
||||
..MaildirMbox::default()
|
||||
},
|
||||
MaildirMbox::new("INBOX")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_display_mbox() {
|
||||
let default_mbox = MaildirMbox::default();
|
||||
assert_eq!("", default_mbox.to_string());
|
||||
|
||||
let new_mbox = MaildirMbox::new("INBOX");
|
||||
assert_eq!("INBOX", new_mbox.to_string());
|
||||
|
||||
let full_mbox = MaildirMbox {
|
||||
name: "Sent".into(),
|
||||
};
|
||||
assert_eq!("Sent", full_mbox.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a list of raw mailboxes returned by the `maildir` crate.
|
||||
pub type RawMaildirMboxes = maildir::MaildirEntries;
|
||||
|
||||
impl TryFrom<RawMaildirMboxes> for MaildirMboxes {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mail_entries: RawMaildirMboxes) -> Result<Self, Self::Error> {
|
||||
let mut mboxes = vec![];
|
||||
for entry in mail_entries {
|
||||
mboxes.push(entry?.try_into()?);
|
||||
}
|
||||
Ok(MaildirMboxes { mboxes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the raw mailbox returned by the `maildir` crate.
|
||||
pub type RawMaildirMbox = maildir::Maildir;
|
||||
|
||||
impl TryFrom<RawMaildirMbox> for MaildirMbox {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mail_entry: RawMaildirMbox) -> Result<Self, Self::Error> {
|
||||
let subdir_name = mail_entry.path().file_name();
|
||||
Ok(Self {
|
||||
name: subdir_name
|
||||
.and_then(OsStr::to_str)
|
||||
.and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) })
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"cannot parse maildir subdirectory name from path {:?}",
|
||||
subdir_name,
|
||||
)
|
||||
})?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
//! 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 {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<NotmuchEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for NotmuchEnvelopes {
|
||||
type Target = Vec<NotmuchEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NotmuchEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchEnvelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the envelope. The envelope is just a message subset,
|
||||
/// and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
pub struct NotmuchEnvelope {
|
||||
/// Represents the id of the message.
|
||||
pub id: String,
|
||||
|
||||
/// Represents the MD5 hash of the message id.
|
||||
pub hash: String,
|
||||
|
||||
/// Represents the tags of the message.
|
||||
pub flags: Vec<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)
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
//! Notmuch mailbox module.
|
||||
//!
|
||||
//! This module provides Notmuch types and conversion utilities
|
||||
//! related to the mailbox
|
||||
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mbox::Mboxes,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
||||
/// Represents a list of Notmuch mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<NotmuchMbox>,
|
||||
}
|
||||
|
||||
impl Deref for NotmuchMboxes {
|
||||
type Target = Vec<NotmuchMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchMboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mboxes for NotmuchMboxes {
|
||||
//
|
||||
}
|
||||
|
||||
/// Represents the notmuch virtual mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct NotmuchMbox {
|
||||
/// Represents the virtual mailbox name.
|
||||
pub name: String,
|
||||
|
||||
/// Represents the query associated to the virtual mailbox name.
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
impl NotmuchMbox {
|
||||
pub fn new(name: &str, query: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
query: query.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NotmuchMbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for NotmuchMbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("QUERY").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).white())
|
||||
.cell(Cell::new(&self.query).green())
|
||||
}
|
||||
}
|
|
@ -12,8 +12,9 @@ use std::{
|
|||
ops::Deref,
|
||||
};
|
||||
|
||||
use himalaya_lib::account::DeserializedAccountConfig;
|
||||
|
||||
use crate::{
|
||||
config::DeserializedAccountConfig,
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::{Cell, Row, Table},
|
||||
};
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::account::{Account, DeserializedConfig};
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
config::{AccountConfig, Accounts, DeserializedConfig},
|
||||
config::Accounts,
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
};
|
||||
|
||||
|
@ -14,7 +15,7 @@ use crate::{
|
|||
pub fn list<'a, P: PrinterService>(
|
||||
max_width: Option<usize>,
|
||||
config: &DeserializedConfig,
|
||||
account_config: &AccountConfig,
|
||||
account_config: &Account,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!(">> account list handler");
|
||||
|
@ -36,13 +37,13 @@ pub fn list<'a, P: PrinterService>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::account::{
|
||||
Account, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig,
|
||||
};
|
||||
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
config::{DeserializedAccountConfig, DeserializedImapAccountConfig},
|
||||
output::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
use crate::output::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -121,7 +122,7 @@ mod tests {
|
|||
..DeserializedConfig::default()
|
||||
};
|
||||
|
||||
let account_config = AccountConfig::default();
|
||||
let account_config = Account::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
|
||||
assert!(list(None, &config, &account_config, &mut printer).is_ok());
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
/// Represents the text/plain format as defined in the [RFC2646]. The
|
||||
/// format is then used by the table system to adjust the way it is
|
||||
/// rendered.
|
||||
///
|
||||
/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(tag = "type", content = "width", rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
// Forces the content width with a fixed amount of pixels.
|
||||
Fixed(usize),
|
||||
// Makes the content fit the terminal.
|
||||
Auto,
|
||||
// Does not restrict the content.
|
||||
Flowed,
|
||||
}
|
||||
|
||||
impl Default for Format {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Hooks {
|
||||
pub pre_send: Option<String>,
|
||||
}
|
16
cli/src/imap/imap_envelopes.rs
Normal file
16
cli/src/imap/imap_envelopes.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::{
|
||||
backend::{from_imap_fetch, ImapFetch},
|
||||
msg::Envelopes,
|
||||
};
|
||||
|
||||
/// Represents the list of raw envelopes returned by the `imap` crate.
|
||||
pub type ImapFetches = imap::types::ZeroCopy<Vec<ImapFetch>>;
|
||||
|
||||
pub fn from_imap_fetches(fetches: ImapFetches) -> Result<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for fetch in fetches.iter().rev() {
|
||||
envelopes.push(from_imap_fetch(fetch).context("cannot parse imap fetch")?);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
|
@ -2,14 +2,13 @@
|
|||
//!
|
||||
//! This module gathers all IMAP handlers triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::backends::ImapBackend;
|
||||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::backend::ImapBackend;
|
||||
|
||||
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.notify(keepalive, mbox)
|
||||
imap.notify(keepalive, mbox).context("cannot imap notify")
|
||||
}
|
||||
|
||||
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.watch(keepalive, mbox)
|
||||
imap.watch(keepalive, mbox).context("cannot imap watch")
|
||||
}
|
109
cli/src/lib.rs
109
cli/src/lib.rs
|
@ -2,103 +2,39 @@ pub mod mbox {
|
|||
pub mod mbox;
|
||||
pub use mbox::*;
|
||||
|
||||
pub mod mboxes;
|
||||
pub use mboxes::*;
|
||||
|
||||
pub mod mbox_args;
|
||||
pub mod mbox_handlers;
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub mod imap {
|
||||
pub mod imap_args;
|
||||
pub mod imap_handlers;
|
||||
|
||||
pub mod imap_envelopes;
|
||||
pub use imap_envelopes::*;
|
||||
}
|
||||
|
||||
pub mod msg {
|
||||
pub mod envelope;
|
||||
pub use envelope::*;
|
||||
|
||||
pub mod envelopes;
|
||||
pub use envelopes::*;
|
||||
|
||||
pub mod msg_args;
|
||||
|
||||
pub mod msg_handlers;
|
||||
pub mod msg_utils;
|
||||
|
||||
pub mod flag_args;
|
||||
pub mod flag_handlers;
|
||||
|
||||
pub mod tpl_args;
|
||||
pub use tpl_args::TplOverride;
|
||||
|
||||
pub mod tpl_handlers;
|
||||
|
||||
pub mod msg_entity;
|
||||
pub use msg_entity::*;
|
||||
|
||||
pub mod parts_entity;
|
||||
pub use parts_entity::*;
|
||||
|
||||
pub mod addr_entity;
|
||||
pub use addr_entity::*;
|
||||
}
|
||||
|
||||
pub mod backends {
|
||||
pub mod backend;
|
||||
pub use backend::*;
|
||||
|
||||
pub mod id_mapper;
|
||||
pub use id_mapper::*;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub mod imap {
|
||||
pub mod imap_args;
|
||||
|
||||
pub mod imap_backend;
|
||||
pub use imap_backend::*;
|
||||
|
||||
pub mod imap_handlers;
|
||||
|
||||
pub mod imap_mbox;
|
||||
pub use imap_mbox::*;
|
||||
|
||||
pub mod imap_mbox_attr;
|
||||
pub use imap_mbox_attr::*;
|
||||
|
||||
pub mod imap_envelope;
|
||||
pub use imap_envelope::*;
|
||||
|
||||
pub mod imap_flag;
|
||||
pub use imap_flag::*;
|
||||
|
||||
pub mod msg_sort_criterion;
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub use self::imap::*;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub mod maildir {
|
||||
pub mod maildir_backend;
|
||||
pub use maildir_backend::*;
|
||||
|
||||
pub mod maildir_mbox;
|
||||
pub use maildir_mbox::*;
|
||||
|
||||
pub mod maildir_envelope;
|
||||
pub use maildir_envelope::*;
|
||||
|
||||
pub mod maildir_flag;
|
||||
pub use maildir_flag::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub use self::maildir::*;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub mod notmuch {
|
||||
pub mod notmuch_backend;
|
||||
pub use notmuch_backend::*;
|
||||
|
||||
pub mod notmuch_mbox;
|
||||
pub use notmuch_mbox::*;
|
||||
|
||||
pub mod notmuch_envelope;
|
||||
pub use notmuch_envelope::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub use self::notmuch::*;
|
||||
}
|
||||
|
||||
pub mod smtp {
|
||||
|
@ -107,12 +43,6 @@ pub mod smtp {
|
|||
}
|
||||
|
||||
pub mod config {
|
||||
pub mod deserialized_config;
|
||||
pub use deserialized_config::*;
|
||||
|
||||
pub mod deserialized_account_config;
|
||||
pub use deserialized_account_config::*;
|
||||
|
||||
pub mod config_args;
|
||||
|
||||
pub mod account_args;
|
||||
|
@ -120,15 +50,6 @@ pub mod config {
|
|||
|
||||
pub mod account;
|
||||
pub use account::*;
|
||||
|
||||
pub mod account_config;
|
||||
pub use account_config::*;
|
||||
|
||||
pub mod format;
|
||||
pub use format::*;
|
||||
|
||||
pub mod hooks;
|
||||
pub use hooks::*;
|
||||
}
|
||||
|
||||
pub mod compl;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::{
|
||||
account::{Account, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER},
|
||||
backend::Backend,
|
||||
};
|
||||
use std::{convert::TryFrom, env};
|
||||
use url::Url;
|
||||
|
||||
use himalaya::{
|
||||
backends::Backend,
|
||||
compl::{compl_args, compl_handlers},
|
||||
config::{
|
||||
account_args, account_handlers, config_args, AccountConfig, BackendConfig,
|
||||
DeserializedConfig, DEFAULT_INBOX_FOLDER,
|
||||
},
|
||||
config::{account_args, account_handlers, config_args},
|
||||
mbox::{mbox_args, mbox_handlers},
|
||||
msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers},
|
||||
output::{output_args, OutputFmt, StdoutPrinter},
|
||||
|
@ -16,13 +16,16 @@ use himalaya::{
|
|||
};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya::backends::{imap_args, imap_handlers, ImapBackend};
|
||||
use himalaya::imap::{imap_args, imap_handlers};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya_lib::backend::ImapBackend;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use himalaya::backends::MaildirBackend;
|
||||
use himalaya_lib::backend::MaildirBackend;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig};
|
||||
use himalaya_lib::{account::MaildirBackendConfig, backend::NotmuchBackend};
|
||||
|
||||
fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||
let app = clap::App::new(env!("CARGO_PKG_NAME"))
|
||||
|
@ -55,7 +58,7 @@ fn main() -> Result<()> {
|
|||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
let config = DeserializedConfig::from_opt_path(None)?;
|
||||
let (account_config, backend_config) =
|
||||
AccountConfig::from_config_and_opt_account_name(&config, None)?;
|
||||
Account::from_config_and_opt_account_name(&config, None)?;
|
||||
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
|
||||
let url = Url::parse(&raw_args[1])?;
|
||||
let mut smtp = LettreService::from(&account_config);
|
||||
|
@ -111,7 +114,7 @@ fn main() -> Result<()> {
|
|||
// Init entities and services.
|
||||
let config = DeserializedConfig::from_opt_path(m.value_of("config"))?;
|
||||
let (account_config, backend_config) =
|
||||
AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?;
|
||||
Account::from_config_and_opt_account_name(&config, m.value_of("account"))?;
|
||||
let mbox = m
|
||||
.value_of("mbox-source")
|
||||
.or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str()))
|
||||
|
@ -277,8 +280,9 @@ fn main() -> Result<()> {
|
|||
Some(msg_args::Cmd::Send(raw_msg)) => {
|
||||
return msg_handlers::send(raw_msg, &account_config, &mut printer, backend, &mut smtp);
|
||||
}
|
||||
Some(msg_args::Cmd::Write(atts, encrypt)) => {
|
||||
Some(msg_args::Cmd::Write(tpl, atts, encrypt)) => {
|
||||
return msg_handlers::write(
|
||||
tpl,
|
||||
atts,
|
||||
encrypt,
|
||||
&account_config,
|
||||
|
@ -343,5 +347,5 @@ fn main() -> Result<()> {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
backend.disconnect()
|
||||
backend.disconnect().context("cannot disconnect")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
use std::fmt;
|
||||
use himalaya_lib::mbox::Mbox;
|
||||
|
||||
use crate::output::PrintTable;
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable {
|
||||
//
|
||||
impl Table for Mbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("DELIM").bold().underline().white())
|
||||
.cell(Cell::new("NAME").bold().underline().white())
|
||||
.cell(Cell::new("DESC").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.delim).white())
|
||||
.cell(Cell::new(&self.name).blue())
|
||||
.cell(Cell::new(&self.desc).green())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,15 @@
|
|||
//! This module gathers all mailbox actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::{account::Account, backend::Backend};
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::AccountConfig,
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
};
|
||||
use crate::output::{PrintTableOpts, PrinterService};
|
||||
|
||||
/// Lists all mailboxes.
|
||||
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -22,7 +19,8 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
let mboxes = backend.get_mboxes()?;
|
||||
trace!("mailboxes: {:?}", mboxes);
|
||||
printer.print_table(
|
||||
mboxes,
|
||||
// TODO: remove Box
|
||||
Box::new(mboxes),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
|
@ -32,15 +30,15 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{
|
||||
backend::{backend, Backend},
|
||||
mbox::{Mbox, Mboxes},
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
use std::{fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
output::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
use crate::output::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -90,17 +88,17 @@ mod tests {
|
|||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
) -> anyhow::Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
fn print_str<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> Result<()> {
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
|
@ -111,32 +109,29 @@ mod tests {
|
|||
struct TestBackend;
|
||||
|
||||
impl<'a> Backend<'a> for TestBackend {
|
||||
fn add_mbox(&mut self, _: &str) -> Result<()> {
|
||||
fn add_mbox(&mut self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
Ok(Box::new(ImapMboxes {
|
||||
fn get_mboxes(&mut self) -> backend::Result<Mboxes> {
|
||||
Ok(Mboxes {
|
||||
mboxes: vec![
|
||||
ImapMbox {
|
||||
Mbox {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
ImapMbox {
|
||||
Mbox {
|
||||
delim: "/".into(),
|
||||
name: "Sent".into(),
|
||||
attrs: ImapMboxAttrs(vec![
|
||||
ImapMboxAttr::NoInferiors,
|
||||
ImapMboxAttr::Custom("HasNoChildren".into()),
|
||||
]),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
],
|
||||
}))
|
||||
})
|
||||
}
|
||||
fn del_mbox(&mut self, _: &str) -> Result<()> {
|
||||
fn del_mbox(&mut self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result<Box<dyn Envelopes>> {
|
||||
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn search_envelopes(
|
||||
|
@ -146,36 +141,36 @@ mod tests {
|
|||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result<Box<dyn ToString>> {
|
||||
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn get_msg(&mut self, _: &str, _: &str) -> Result<Msg> {
|
||||
fn get_msg(&mut self, _: &str, _: &str) -> backend::Result<Msg> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn del_msg(&mut self, _: &str, _: &str) -> Result<()> {
|
||||
fn del_msg(&mut self, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let config = AccountConfig::default();
|
||||
let config = Account::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
let backend = Box::new(&mut backend);
|
||||
|
@ -184,9 +179,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"DELIM │NAME │ATTRIBUTES \n",
|
||||
"/ │INBOX │NoSelect \n",
|
||||
"/ │Sent │NoInferiors, HasNoChildren \n",
|
||||
"DELIM │NAME │DESC \n",
|
||||
"/ │INBOX │desc \n",
|
||||
"/ │Sent │desc \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
|
|
16
cli/src/mbox/mboxes.rs
Normal file
16
cli/src/mbox/mboxes.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::mbox::Mboxes;
|
||||
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
impl PrintTable for Mboxes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,13 +1,30 @@
|
|||
use std::{any, fmt};
|
||||
use himalaya_lib::msg::{Envelope, Flag};
|
||||
|
||||
use crate::output::PrintTable;
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any {
|
||||
fn as_any(&self) -> &dyn any::Any;
|
||||
}
|
||||
impl Table for Envelope {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("SENDER").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any> Envelopes for T {
|
||||
fn as_any(&self) -> &dyn any::Any {
|
||||
self
|
||||
fn row(&self) -> Row {
|
||||
let id = self.id.to_string();
|
||||
let flags = self.flags.to_symbols_string();
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = self.date.as_deref().unwrap_or_default();
|
||||
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||
}
|
||||
}
|
||||
|
|
16
cli/src/msg/envelopes.rs
Normal file
16
cli/src/msg/envelopes.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::msg::Envelopes;
|
||||
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
impl PrintTable for Envelopes {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -3,8 +3,9 @@
|
|||
//! This module gathers all flag actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::backend::Backend;
|
||||
|
||||
use crate::{backends::Backend, output::PrinterService};
|
||||
use crate::output::PrinterService;
|
||||
|
||||
/// Adds flags to all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
|
|
|
@ -4,11 +4,15 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::msg::TplOverride;
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
mbox::mbox_args,
|
||||
msg::{flag_args, msg_args, tpl_args},
|
||||
msg::{
|
||||
flag_args, msg_args,
|
||||
tpl_args::{self, from_args},
|
||||
},
|
||||
ui::table_arg,
|
||||
};
|
||||
|
||||
|
@ -42,7 +46,7 @@ pub enum Cmd<'a> {
|
|||
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawMsg<'a>),
|
||||
Write(AttachmentPaths<'a>, Encrypt),
|
||||
Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag_args::Cmd<'a>>),
|
||||
Tpl(Option<tpl_args::Cmd<'a>>),
|
||||
|
@ -261,7 +265,8 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
debug!("attachments paths: {:?}", attachment_paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Cmd::Write(attachment_paths, encrypt)));
|
||||
let tpl = from_args(m);
|
||||
return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("template") {
|
||||
|
@ -412,6 +417,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
),
|
||||
SubCommand::with_name("write")
|
||||
.about("Writes a new message")
|
||||
.args(&tpl_args::tpl_args())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("send")
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{
|
||||
account::{Account, DEFAULT_SENT_FOLDER},
|
||||
backend::Backend,
|
||||
msg::{Msg, Part, Parts, TextPlainPart, TplOverride},
|
||||
};
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::addrparse;
|
||||
use std::{
|
||||
|
@ -14,18 +19,16 @@ use std::{
|
|||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::{AccountConfig, DEFAULT_SENT_FOLDER},
|
||||
msg::{Msg, Part, Parts, TextPlainPart},
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
smtp::SmtpService,
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Downloads all message attachments to the user account downloads directory.
|
||||
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -89,17 +92,17 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
backend
|
||||
let msg = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?;
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -109,7 +112,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
imap: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -118,7 +121,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
let msgs = imap.get_envelopes(mbox, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
|
@ -131,7 +134,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
url: &Url,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
|
@ -183,7 +186,7 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
msg.edit_with_editor(config, printer, backend, smtp)?;
|
||||
editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -209,7 +212,7 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -230,18 +233,19 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
backend
|
||||
let msg = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?
|
||||
.add_flags(mbox, seq, "replied")
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?
|
||||
.add_flags(mbox, seq, "replied")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves a raw message to the targetted mailbox.
|
||||
|
@ -281,7 +285,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -290,7 +294,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
|
@ -306,7 +310,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -315,7 +319,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
|
@ -326,7 +330,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
/// Send a raw message.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
raw_msg: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
smtp: &mut S,
|
||||
|
@ -364,16 +368,17 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
|
||||
/// Compose a new message.
|
||||
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
tpl: TplOverride,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
Msg::default()
|
||||
let msg = Msg::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?;
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, tpl, config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::{debug, trace};
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
pub fn local_draft_path() -> PathBuf {
|
||||
let path = env::temp_dir().join("himalaya-draft.eml");
|
||||
trace!("local draft path: {:?}", path);
|
||||
path
|
||||
}
|
||||
|
||||
pub fn remove_local_draft() -> Result<()> {
|
||||
let path = local_draft_path();
|
||||
debug!("remove draft path at {:?}", path);
|
||||
fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path))
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::msg::TplOverride;
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::msg::msg_args;
|
||||
|
@ -13,30 +14,16 @@ type ReplyAll = bool;
|
|||
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||
type Tpl<'a> = &'a str;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct TplOverride<'a> {
|
||||
pub subject: Option<&'a str>,
|
||||
pub from: Option<Vec<&'a str>>,
|
||||
pub to: Option<Vec<&'a str>>,
|
||||
pub cc: Option<Vec<&'a str>>,
|
||||
pub bcc: Option<Vec<&'a str>>,
|
||||
pub headers: Option<Vec<&'a str>>,
|
||||
pub body: Option<&'a str>,
|
||||
pub sig: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
|
||||
fn from(matches: &'a ArgMatches<'a>) -> Self {
|
||||
Self {
|
||||
subject: matches.value_of("subject"),
|
||||
from: matches.values_of("from").map(|v| v.collect()),
|
||||
to: matches.values_of("to").map(|v| v.collect()),
|
||||
cc: matches.values_of("cc").map(|v| v.collect()),
|
||||
bcc: matches.values_of("bcc").map(|v| v.collect()),
|
||||
headers: matches.values_of("headers").map(|v| v.collect()),
|
||||
body: matches.value_of("body"),
|
||||
sig: matches.value_of("signature"),
|
||||
}
|
||||
pub fn from_args<'a>(matches: &'a ArgMatches<'a>) -> TplOverride {
|
||||
TplOverride {
|
||||
subject: matches.value_of("subject"),
|
||||
from: matches.values_of("from").map(|v| v.collect()),
|
||||
to: matches.values_of("to").map(|v| v.collect()),
|
||||
cc: matches.values_of("cc").map(|v| v.collect()),
|
||||
bcc: matches.values_of("bcc").map(|v| v.collect()),
|
||||
headers: matches.values_of("headers").map(|v| v.collect()),
|
||||
body: matches.value_of("body"),
|
||||
sig: matches.value_of("signature"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +43,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
|
||||
if let Some(m) = m.subcommand_matches("new") {
|
||||
info!("new subcommand matched");
|
||||
let tpl = TplOverride::from(m);
|
||||
let tpl = from_args(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Cmd::New(tpl)));
|
||||
}
|
||||
|
@ -67,7 +54,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
debug!("sequence: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
debug!("reply all: {}", all);
|
||||
let tpl = TplOverride::from(m);
|
||||
let tpl = from_args(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Cmd::Reply(seq, all, tpl)));
|
||||
}
|
||||
|
@ -76,7 +63,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
info!("forward subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let tpl = TplOverride::from(m);
|
||||
let tpl = from_args(m);
|
||||
trace!("template args: {:?}", tpl);
|
||||
return Ok(Some(Cmd::Forward(seq, tpl)));
|
||||
}
|
||||
|
|
|
@ -4,20 +4,19 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{
|
||||
account::Account,
|
||||
backend::Backend,
|
||||
msg::{Msg, TplOverride},
|
||||
};
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::AccountConfig,
|
||||
msg::{Msg, TplOverride},
|
||||
output::PrinterService,
|
||||
smtp::SmtpService,
|
||||
};
|
||||
use crate::{output::PrinterService, smtp::SmtpService};
|
||||
|
||||
/// Generate a new message template.
|
||||
pub fn new<'a, P: PrinterService>(
|
||||
opts: TplOverride<'a>,
|
||||
account: &'a AccountConfig,
|
||||
account: &'a Account,
|
||||
printer: &'a mut P,
|
||||
) -> Result<()> {
|
||||
let tpl = Msg::default().to_tpl(opts, account)?;
|
||||
|
@ -30,7 +29,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
all: bool,
|
||||
opts: TplOverride<'a>,
|
||||
mbox: &str,
|
||||
config: &'a AccountConfig,
|
||||
config: &'a Account,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -46,7 +45,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
seq: &str,
|
||||
opts: TplOverride<'a>,
|
||||
mbox: &str,
|
||||
config: &'a AccountConfig,
|
||||
config: &'a Account,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
|
@ -60,7 +59,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
/// Saves a message based on a template.
|
||||
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
|
@ -85,7 +84,7 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
/// Sends a message based on a template.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
mbox: &str,
|
||||
account: &AccountConfig,
|
||||
account: &Account,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
|
|
|
@ -8,12 +8,14 @@ pub trait Print {
|
|||
|
||||
impl Print for &str {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
writeln!(writer, "{}", self).context("cannot write string to writer")
|
||||
writeln!(writer, "{}", self).context("cannot write string to writer")?;
|
||||
Ok(writer.reset()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Print for String {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
self.as_str().print(writer)
|
||||
self.as_str().print(writer)?;
|
||||
Ok(writer.reset()?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::account::TextPlainFormat;
|
||||
use std::io;
|
||||
use termcolor::{self, StandardStream};
|
||||
|
||||
use crate::config::Format;
|
||||
|
||||
pub trait WriteColor: io::Write + termcolor::WriteColor {}
|
||||
|
||||
impl WriteColor for StandardStream {}
|
||||
|
@ -13,6 +12,6 @@ pub trait PrintTable {
|
|||
}
|
||||
|
||||
pub struct PrintTableOpts<'a> {
|
||||
pub format: &'a Format,
|
||||
pub format: &'a TextPlainFormat,
|
||||
pub max_width: Option<usize>,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::{account::Account, msg::Msg};
|
||||
use lettre::{
|
||||
self,
|
||||
transport::smtp::{
|
||||
|
@ -9,14 +10,14 @@ use lettre::{
|
|||
};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd};
|
||||
use crate::output::pipe_cmd;
|
||||
|
||||
pub trait SmtpService {
|
||||
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
|
||||
fn send(&mut self, account: &Account, msg: &Msg) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub struct LettreService<'a> {
|
||||
account: &'a AccountConfig,
|
||||
account: &'a Account,
|
||||
transport: Option<SmtpTransport>,
|
||||
}
|
||||
|
||||
|
@ -55,14 +56,14 @@ impl LettreService<'_> {
|
|||
}
|
||||
|
||||
impl SmtpService for LettreService<'_> {
|
||||
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>> {
|
||||
fn send(&mut self, account: &Account, msg: &Msg) -> Result<Vec<u8>> {
|
||||
let mut raw_msg = msg.into_sendable_msg(account)?.formatted();
|
||||
|
||||
let envelope: lettre::address::Envelope =
|
||||
if let Some(cmd) = account.hooks.pre_send.as_deref() {
|
||||
for cmd in cmd.split('|') {
|
||||
raw_msg = pipe_cmd(cmd.trim(), &raw_msg)
|
||||
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?
|
||||
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?;
|
||||
}
|
||||
let parsed_mail = mailparse::parse_mail(&raw_msg)?;
|
||||
Msg::from_parsed_mail(parsed_mail, account)?.try_into()
|
||||
|
@ -75,8 +76,8 @@ impl SmtpService for LettreService<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
|
||||
fn from(account: &'a AccountConfig) -> Self {
|
||||
impl<'a> From<&'a Account> for LettreService<'a> {
|
||||
fn from(account: &'a Account) -> Self {
|
||||
Self {
|
||||
account,
|
||||
transport: None,
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::debug;
|
||||
use himalaya_lib::{
|
||||
account::{Account, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER},
|
||||
backend::Backend,
|
||||
msg::{local_draft_path, remove_local_draft, Msg, TplOverride},
|
||||
};
|
||||
use log::{debug, info};
|
||||
use std::{env, fs, process::Command};
|
||||
|
||||
use crate::msg::msg_utils;
|
||||
use crate::{
|
||||
output::PrinterService,
|
||||
smtp::SmtpService,
|
||||
ui::choice::{self, PostEditChoice, PreEditChoice},
|
||||
};
|
||||
|
||||
pub fn open_with_tpl(tpl: String) -> Result<String> {
|
||||
let path = msg_utils::local_draft_path();
|
||||
let path = local_draft_path();
|
||||
|
||||
debug!("create draft");
|
||||
fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?;
|
||||
|
@ -24,8 +33,100 @@ pub fn open_with_tpl(tpl: String) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn open_with_draft() -> Result<String> {
|
||||
let path = msg_utils::local_draft_path();
|
||||
let path = local_draft_path();
|
||||
let tpl =
|
||||
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
|
||||
open_with_tpl(tpl)
|
||||
}
|
||||
|
||||
fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &Account) -> Result<Msg> {
|
||||
let tpl = msg.to_tpl(tpl, account)?;
|
||||
let tpl = open_with_tpl(tpl)?;
|
||||
Msg::from_tpl(&tpl).context("cannot parse message from template")
|
||||
}
|
||||
|
||||
pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
mut msg: Msg,
|
||||
tpl: TplOverride,
|
||||
account: &Account,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<Box<&'a mut B>> {
|
||||
info!("start editing with editor");
|
||||
|
||||
let draft = local_draft_path();
|
||||
if draft.exists() {
|
||||
loop {
|
||||
match choice::pre_edit() {
|
||||
Ok(choice) => match choice {
|
||||
PreEditChoice::Edit => {
|
||||
let tpl = open_with_draft()?;
|
||||
msg.merge_with(Msg::from_tpl(&tpl)?);
|
||||
break;
|
||||
}
|
||||
PreEditChoice::Discard => {
|
||||
msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?);
|
||||
break;
|
||||
}
|
||||
PreEditChoice::Quit => return Ok(backend),
|
||||
},
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?);
|
||||
}
|
||||
|
||||
loop {
|
||||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
printer.print_str("Sending message…")?;
|
||||
let sent_msg = smtp.send(account, &msg)?;
|
||||
let sent_folder = account
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
printer.print_str(format!("Adding message to the {:?} folder…", sent_folder))?;
|
||||
backend.add_msg(&sent_folder, &sent_msg, "seen")?;
|
||||
remove_local_draft()?;
|
||||
printer.print_struct("Done!")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Edit) => {
|
||||
msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?);
|
||||
continue;
|
||||
}
|
||||
Ok(PostEditChoice::LocalDraft) => {
|
||||
printer.print_struct("Message successfully saved locally")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::RemoteDraft) => {
|
||||
let tpl = msg.to_tpl(TplOverride::default(), account)?;
|
||||
let draft_folder = account
|
||||
.mailboxes
|
||||
.get("draft")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_DRAFT_FOLDER);
|
||||
backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?;
|
||||
remove_local_draft()?;
|
||||
printer.print_struct(format!("Message successfully saved to {}", draft_folder))?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Discard) => {
|
||||
remove_local_draft()?;
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
|
|
@ -5,15 +5,13 @@
|
|||
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::account::TextPlainFormat;
|
||||
use log::trace;
|
||||
use termcolor::{Color, ColorSpec};
|
||||
use terminal_size;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
config::Format,
|
||||
output::{Print, PrintTableOpts, WriteColor},
|
||||
};
|
||||
use crate::output::{Print, PrintTableOpts, WriteColor};
|
||||
|
||||
/// Defines the default terminal size.
|
||||
/// This is used when the size cannot be determined by the `terminal_size` crate.
|
||||
|
@ -134,7 +132,9 @@ impl Print for Cell {
|
|||
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
|
||||
|
||||
// Writes the colorized cell to stdout
|
||||
write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
|
||||
write!(writer, "{}", self.value)
|
||||
.context(format!(r#"cannot print cell "{}""#, self.value))?;
|
||||
Ok(writer.reset()?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,11 +169,11 @@ where
|
|||
|
||||
/// Writes the table to the writer.
|
||||
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
|
||||
let is_format_flowed = matches!(opts.format, Format::Flowed);
|
||||
let is_format_flowed = matches!(opts.format, TextPlainFormat::Flowed);
|
||||
let max_width = match opts.format {
|
||||
Format::Fixed(width) => opts.max_width.unwrap_or(*width),
|
||||
Format::Flowed => 0,
|
||||
Format::Auto => opts
|
||||
TextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width),
|
||||
TextPlainFormat::Flowed => 0,
|
||||
TextPlainFormat::Auto => opts
|
||||
.max_width
|
||||
.or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize))
|
||||
.unwrap_or(DEFAULT_TERM_WIDTH),
|
||||
|
|
33
flake.nix
33
flake.nix
|
@ -16,19 +16,8 @@
|
|||
(system:
|
||||
let
|
||||
name = "himalaya";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
rust-overlay.overlay
|
||||
(self: super: {
|
||||
# Because rust-overlay bundles multiple rust packages
|
||||
# into one derivation, specify that mega-bundle here,
|
||||
# so that crate2nix will use them automatically.
|
||||
rustc = self.rust-bin.stable.latest.default;
|
||||
cargo = self.rust-bin.stable.latest.default;
|
||||
})
|
||||
];
|
||||
};
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
in
|
||||
rec {
|
||||
# nix build
|
||||
|
@ -68,17 +57,19 @@
|
|||
|
||||
# nix develop
|
||||
devShell = pkgs.mkShell {
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
RUSTUP_TOOLCHAIN = "stable";
|
||||
inputsFrom = builtins.attrValues self.packages.${system};
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
cargo-watch
|
||||
trunk
|
||||
ripgrep
|
||||
rust-analyzer
|
||||
rustfmt
|
||||
nativeBuildInputs = with pkgs; [
|
||||
# Nix LSP + formatter
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
|
||||
# Rust env
|
||||
(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
|
||||
cargo-watch
|
||||
rust-analyzer
|
||||
|
||||
# Notmuch
|
||||
notmuch
|
||||
];
|
||||
};
|
||||
|
|
|
@ -3,4 +3,33 @@ name = "himalaya-lib"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
imap-backend = ["imap", "imap-proto"]
|
||||
maildir-backend = ["maildir", "md5"]
|
||||
notmuch-backend = ["notmuch", "maildir-backend"]
|
||||
default = ["imap-backend", "maildir-backend"]
|
||||
|
||||
[dependencies]
|
||||
ammonia = "3.1.2"
|
||||
chrono = "0.4.19"
|
||||
convert_case = "0.5.0"
|
||||
html-escape = "0.2.9"
|
||||
lettre = { version = "0.10.0-rc.7", features = ["serde"] }
|
||||
log = "0.4.14"
|
||||
mailparse = "0.13.6"
|
||||
native-tls = "0.2.8"
|
||||
regex = "1.5.4"
|
||||
rfc2047-decoder = "0.1.2"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
shellexpand = "2.1.0"
|
||||
thiserror = "1.0.31"
|
||||
toml = "0.5.8"
|
||||
tree_magic = "0.2.3"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
# [optional]
|
||||
imap = { version = "=3.0.0-alpha.4", optional = true }
|
||||
imap-proto = { version = "0.14.3", optional = true }
|
||||
maildir = { version = "0.6.1", optional = true }
|
||||
md5 = { version = "0.7.0", optional = true }
|
||||
notmuch = { version = "0.7.1", optional = true }
|
||||
|
|
|
@ -1,14 +1,78 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
//! Account config module.
|
||||
//!
|
||||
//! This module contains the representation of the user account.
|
||||
|
||||
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::MailAddr;
|
||||
use serde::Deserialize;
|
||||
use shellexpand;
|
||||
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{config::*, output::run_cmd};
|
||||
use crate::process::{self, ProcessError};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
|
||||
|
||||
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
|
||||
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
|
||||
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AccountError {
|
||||
#[error("cannot encrypt file using pgp")]
|
||||
EncryptFileError(#[source] ProcessError),
|
||||
#[error("cannot find encrypt file command from config file")]
|
||||
EncryptFileMissingCmdError,
|
||||
|
||||
#[error("cannot decrypt file using pgp")]
|
||||
DecryptFileError(#[source] ProcessError),
|
||||
#[error("cannot find decrypt file command from config file")]
|
||||
DecryptFileMissingCmdError,
|
||||
|
||||
#[error("cannot get smtp password")]
|
||||
GetSmtpPasswdError(#[source] ProcessError),
|
||||
#[error("cannot get smtp password: password is empty")]
|
||||
GetSmtpPasswdEmptyError,
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[error("cannot get imap password")]
|
||||
GetImapPasswdError(#[source] ProcessError),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[error("cannot get imap password: password is empty")]
|
||||
GetImapPasswdEmptyError,
|
||||
|
||||
#[error("cannot find default account")]
|
||||
FindDefaultAccountError,
|
||||
#[error("cannot find account {0}")]
|
||||
FindAccountError(String),
|
||||
#[error("cannot parse account address {0}")]
|
||||
ParseAccountAddrError(#[source] mailparse::MailParseError, String),
|
||||
#[error("cannot find account address in {0}")]
|
||||
ParseAccountAddrNotFoundError(String),
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
#[error("cannot expand maildir path")]
|
||||
ExpandMaildirPathError(#[source] shellexpand::LookupError<env::VarError>),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[error("cannot expand notmuch path")]
|
||||
ExpandNotmuchDatabasePathError(#[source] shellexpand::LookupError<env::VarError>),
|
||||
#[error("cannot expand mailbox alias {1}")]
|
||||
ExpandMboxAliasError(#[source] shellexpand::LookupError<env::VarError>, String),
|
||||
|
||||
#[error("cannot parse download file name from {0}")]
|
||||
ParseDownloadFileNameError(PathBuf),
|
||||
|
||||
#[error("cannot start the notify mode")]
|
||||
StartNotifyModeError(#[source] ProcessError),
|
||||
}
|
||||
|
||||
/// Represents the user account.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct AccountConfig {
|
||||
pub struct Account {
|
||||
/// Represents the name of the user account.
|
||||
pub name: String,
|
||||
/// Makes this account the default one.
|
||||
|
@ -31,7 +95,7 @@ pub struct AccountConfig {
|
|||
pub watch_cmds: Vec<String>,
|
||||
/// Represents the text/plain format as defined in the
|
||||
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
|
||||
pub format: Format,
|
||||
pub format: TextPlainFormat,
|
||||
/// Overrides the default headers displayed at the top of
|
||||
/// the read message.
|
||||
pub read_headers: Vec<String>,
|
||||
|
@ -61,12 +125,13 @@ pub struct AccountConfig {
|
|||
pub pgp_decrypt_cmd: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> AccountConfig {
|
||||
/// tries to create an account from a config and an optional account name.
|
||||
impl<'a> Account {
|
||||
/// Tries to create an account from a config and an optional
|
||||
/// account name.
|
||||
pub fn from_config_and_opt_account_name(
|
||||
config: &'a DeserializedConfig,
|
||||
account_name: Option<&str>,
|
||||
) -> Result<(AccountConfig, BackendConfig)> {
|
||||
) -> Result<(Account, BackendConfig), AccountError> {
|
||||
info!("begin: parsing account and backend configs from config and account name");
|
||||
|
||||
debug!("account name: {:?}", account_name.unwrap_or("default"));
|
||||
|
@ -87,12 +152,12 @@ impl<'a> AccountConfig {
|
|||
}
|
||||
})
|
||||
.map(|(name, account)| (name.to_owned(), account))
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
.ok_or_else(|| AccountError::FindDefaultAccountError),
|
||||
Some(name) => config
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_owned(), account))
|
||||
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)),
|
||||
.ok_or_else(|| AccountError::FindAccountError(name.to_owned())),
|
||||
}?;
|
||||
|
||||
let base_account = account.to_base();
|
||||
|
@ -136,7 +201,7 @@ impl<'a> AccountConfig {
|
|||
.or_else(|| sig.map(|sig| sig.to_owned()))
|
||||
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
|
||||
|
||||
let account_config = AccountConfig {
|
||||
let account_config = Account {
|
||||
name,
|
||||
display_name: base_account
|
||||
.name
|
||||
|
@ -146,7 +211,11 @@ impl<'a> AccountConfig {
|
|||
downloads_dir,
|
||||
sig,
|
||||
default_page_size,
|
||||
notify_cmd: base_account.notify_cmd.clone(),
|
||||
notify_cmd: base_account
|
||||
.notify_cmd
|
||||
.as_ref()
|
||||
.or_else(|| config.notify_cmd.as_ref())
|
||||
.cloned(),
|
||||
notify_query: base_account
|
||||
.notify_query
|
||||
.as_ref()
|
||||
|
@ -191,13 +260,17 @@ impl<'a> AccountConfig {
|
|||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => {
|
||||
BackendConfig::Maildir(MaildirBackendConfig {
|
||||
maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(),
|
||||
maildir_dir: shellexpand::full(&config.maildir_dir)
|
||||
.map_err(AccountError::ExpandMaildirPathError)?
|
||||
.to_string()
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => {
|
||||
BackendConfig::Notmuch(NotmuchBackendConfig {
|
||||
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)?
|
||||
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)
|
||||
.map_err(AccountError::ExpandNotmuchDatabasePathError)?
|
||||
.to_string()
|
||||
.into(),
|
||||
})
|
||||
|
@ -210,7 +283,7 @@ impl<'a> AccountConfig {
|
|||
}
|
||||
|
||||
/// Builds the full RFC822 compliant address of the user account.
|
||||
pub fn address(&self) -> Result<MailAddr> {
|
||||
pub fn address(&self) -> Result<MailAddr, AccountError> {
|
||||
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
|
||||
let addr = if self.display_name.is_empty() {
|
||||
self.email.clone()
|
||||
|
@ -222,67 +295,63 @@ impl<'a> AccountConfig {
|
|||
};
|
||||
|
||||
Ok(mailparse::addrparse(&addr)
|
||||
.context(format!(
|
||||
"cannot parse account address {:?}",
|
||||
self.display_name
|
||||
))?
|
||||
.map_err(|err| AccountError::ParseAccountAddrError(err, addr.to_owned()))?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))?
|
||||
.ok_or_else(|| AccountError::ParseAccountAddrNotFoundError(addr.to_owned()))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
/// Builds the user account SMTP credentials.
|
||||
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
|
||||
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
|
||||
pub fn smtp_creds(&self) -> Result<SmtpCredentials, AccountError> {
|
||||
let passwd =
|
||||
process::run(&self.smtp_passwd_cmd).map_err(AccountError::GetSmtpPasswdError)?;
|
||||
let passwd = passwd
|
||||
.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||
.to_owned();
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or_else(|| AccountError::GetSmtpPasswdEmptyError)?;
|
||||
|
||||
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
|
||||
Ok(SmtpCredentials::new(
|
||||
self.smtp_login.to_owned(),
|
||||
passwd.to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Encrypts a file.
|
||||
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
|
||||
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<String, AccountError> {
|
||||
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
|
||||
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
|
||||
run_cmd(&encrypt_file_cmd).map(Some).context(format!(
|
||||
"cannot run pgp encrypt command {:?}",
|
||||
encrypt_file_cmd
|
||||
))
|
||||
Ok(process::run(&encrypt_file_cmd).map_err(AccountError::EncryptFileError)?)
|
||||
} else {
|
||||
Ok(None)
|
||||
Err(AccountError::EncryptFileMissingCmdError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypts a file.
|
||||
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
|
||||
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<String, AccountError> {
|
||||
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
|
||||
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
|
||||
run_cmd(&decrypt_file_cmd).map(Some).context(format!(
|
||||
"cannot run pgp decrypt command {:?}",
|
||||
decrypt_file_cmd
|
||||
))
|
||||
Ok(process::run(&decrypt_file_cmd).map_err(AccountError::DecryptFileError)?)
|
||||
} else {
|
||||
Ok(None)
|
||||
Err(AccountError::DecryptFileMissingCmdError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the download path from a file name.
|
||||
pub fn get_download_file_path<S: AsRef<str>>(&self, file_name: S) -> Result<PathBuf> {
|
||||
pub fn get_download_file_path<S: AsRef<str>>(
|
||||
&self,
|
||||
file_name: S,
|
||||
) -> Result<PathBuf, AccountError> {
|
||||
let file_path = self.downloads_dir.join(file_name.as_ref());
|
||||
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
|
||||
.context(format!(
|
||||
"cannot get download file path of {:?}",
|
||||
file_name.as_ref()
|
||||
))
|
||||
}
|
||||
|
||||
/// Gets the unique download path from a file name by adding suffixes in case of name conflicts.
|
||||
/// Gets the unique download path from a file name by adding
|
||||
/// suffixes in case of name conflicts.
|
||||
pub fn get_unique_download_file_path(
|
||||
&self,
|
||||
original_file_path: &PathBuf,
|
||||
is_file: impl Fn(&PathBuf, u8) -> bool,
|
||||
) -> Result<PathBuf> {
|
||||
) -> Result<PathBuf, AccountError> {
|
||||
let mut count = 0;
|
||||
let file_ext = original_file_path
|
||||
.extension()
|
||||
|
@ -298,7 +367,9 @@ impl<'a> AccountConfig {
|
|||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
|
||||
.ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?,
|
||||
.ok_or_else(|| {
|
||||
AccountError::ParseDownloadFileNameError(file_path.to_owned())
|
||||
})?,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -306,7 +377,7 @@ impl<'a> AccountConfig {
|
|||
}
|
||||
|
||||
/// Runs the notify command.
|
||||
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
|
||||
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<(), AccountError> {
|
||||
let subject = subject.as_ref();
|
||||
let sender = sender.as_ref();
|
||||
|
||||
|
@ -317,22 +388,22 @@ impl<'a> AccountConfig {
|
|||
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
|
||||
.unwrap_or(default_cmd);
|
||||
|
||||
debug!("run command: {}", cmd);
|
||||
run_cmd(&cmd).context("cannot run notify cmd")?;
|
||||
process::run(&cmd).map_err(AccountError::StartNotifyModeError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the mailbox alias if exists, otherwise returns the
|
||||
/// mailbox. Also tries to expand shell variables.
|
||||
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String> {
|
||||
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String, AccountError> {
|
||||
let mbox = self
|
||||
.mailboxes
|
||||
.get(&mbox.trim().to_lowercase())
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(mbox);
|
||||
shellexpand::full(mbox)
|
||||
let mbox = shellexpand::full(mbox)
|
||||
.map(String::from)
|
||||
.with_context(|| format!("cannot expand mailbox path {:?}", mbox))
|
||||
.map_err(|err| AccountError::ExpandMboxAliasError(err, mbox.to_owned()))?;
|
||||
Ok(mbox)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,12 +439,14 @@ pub struct ImapBackendConfig {
|
|||
#[cfg(feature = "imap-backend")]
|
||||
impl ImapBackendConfig {
|
||||
/// Gets the IMAP password of the user account.
|
||||
pub fn imap_passwd(&self) -> Result<String> {
|
||||
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
|
||||
pub fn imap_passwd(&self) -> Result<String, AccountError> {
|
||||
let passwd =
|
||||
process::run(&self.imap_passwd_cmd).map_err(AccountError::GetImapPasswdError)?;
|
||||
let passwd = passwd
|
||||
.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||
.to_owned();
|
||||
Ok(passwd)
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or_else(|| AccountError::GetImapPasswdEmptyError)?;
|
||||
Ok(passwd.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,13 +466,39 @@ pub struct NotmuchBackendConfig {
|
|||
pub notmuch_database_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Represents the text/plain format as defined in the [RFC2646].
|
||||
///
|
||||
/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(tag = "type", content = "width", rename_all = "lowercase")]
|
||||
pub enum TextPlainFormat {
|
||||
// Forces the content width with a fixed amount of pixels.
|
||||
Fixed(usize),
|
||||
// Makes the content fit the terminal.
|
||||
Auto,
|
||||
// Does not restrict the content.
|
||||
Flowed,
|
||||
}
|
||||
|
||||
impl Default for TextPlainFormat {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Hooks {
|
||||
pub pre_send: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_get_unique_download_file_path() {
|
||||
let account = AccountConfig::default();
|
||||
let account = Account::default();
|
||||
let path = PathBuf::from("downloads/file.ext");
|
||||
|
||||
// When file path is unique
|
|
@ -1,7 +1,12 @@
|
|||
//! Deserialized account config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of an
|
||||
//! account in the accounts section of the user configuration file.
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::{Format, Hooks};
|
||||
use super::*;
|
||||
|
||||
pub trait ToDeserializedBaseAccountConfig {
|
||||
fn to_base(&self) -> DeserializedBaseAccountConfig;
|
||||
|
@ -53,9 +58,8 @@ macro_rules! make_account_config {
|
|||
pub notify_query: Option<String>,
|
||||
/// Overrides the watch commands for this account.
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
/// Represents the text/plain format as defined in the
|
||||
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
|
||||
pub format: Option<Format>,
|
||||
/// Represents the text/plain format.
|
||||
pub format: Option<TextPlainFormat>,
|
||||
/// Represents the default headers displayed at the top of
|
||||
/// the read message.
|
||||
#[serde(default)]
|
|
@ -1,17 +1,25 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
use log::{debug, trace};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use std::{collections::HashMap, env, fs, io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
use toml;
|
||||
|
||||
use crate::config::DeserializedAccountConfig;
|
||||
use super::*;
|
||||
|
||||
pub const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
|
||||
|
||||
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
|
||||
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
|
||||
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DeserializeConfigError {
|
||||
#[error("cannot read config file")]
|
||||
ReadConfigFile(#[source] io::Error),
|
||||
#[error("cannot parse config file")]
|
||||
ParseConfigFile(#[source] toml::de::Error),
|
||||
#[error("cannot read environment variable {1}")]
|
||||
ReadEnvVar(#[source] env::VarError, &'static str),
|
||||
}
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
|
@ -41,33 +49,38 @@ 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: try to parse config from path");
|
||||
pub fn from_opt_path(path: Option<&str>) -> Result<Self, DeserializeConfigError> {
|
||||
trace!(">> parse config from path");
|
||||
debug!("path: {:?}", path);
|
||||
|
||||
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
let config = toml::from_str(&content).context("cannot parse config file")?;
|
||||
info!("end: try to parse config from path");
|
||||
let content = fs::read_to_string(path).map_err(DeserializeConfigError::ReadConfigFile)?;
|
||||
let config = toml::from_str(&content).map_err(DeserializeConfigError::ParseConfigFile)?;
|
||||
|
||||
trace!("config: {:?}", config);
|
||||
trace!("<< parse config from path");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable.
|
||||
fn path_from_xdg() -> Result<PathBuf> {
|
||||
let path =
|
||||
env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?;
|
||||
/// Tries to get the XDG config file path from XDG_CONFIG_HOME
|
||||
/// environment variable.
|
||||
fn path_from_xdg() -> Result<PathBuf, DeserializeConfigError> {
|
||||
let path = env::var("XDG_CONFIG_HOME")
|
||||
.map_err(|err| DeserializeConfigError::ReadEnvVar(err, "XDG_CONFIG_HOME"))?;
|
||||
let path = PathBuf::from(path).join("himalaya").join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from HOME environment variable.
|
||||
fn path_from_xdg_alt() -> Result<PathBuf> {
|
||||
/// Tries to get the XDG config file path from HOME environment
|
||||
/// variable.
|
||||
fn path_from_xdg_alt() -> Result<PathBuf, DeserializeConfigError> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
|
||||
let path =
|
||||
env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?;
|
||||
let path = PathBuf::from(path)
|
||||
.join(".config")
|
||||
.join("himalaya")
|
||||
|
@ -75,23 +88,24 @@ impl DeserializedConfig {
|
|||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the .himalayarc config file path from HOME environment variable.
|
||||
fn path_from_home() -> Result<PathBuf> {
|
||||
/// Tries to get the .himalayarc config file path from HOME
|
||||
/// environment variable.
|
||||
fn path_from_home() -> Result<PathBuf, DeserializeConfigError> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
|
||||
let path =
|
||||
env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?;
|
||||
let path = PathBuf::from(path).join(".himalayarc");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the config file path.
|
||||
pub fn path() -> Result<PathBuf> {
|
||||
pub fn path() -> Result<PathBuf, DeserializeConfigError> {
|
||||
Self::path_from_xdg()
|
||||
.or_else(|_| Self::path_from_xdg_alt())
|
||||
.or_else(|_| Self::path_from_home())
|
||||
.context("cannot find config path")
|
||||
}
|
||||
}
|
12
lib/src/account/mod.rs
Normal file
12
lib/src/account/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains everything related to the user configuration.
|
||||
|
||||
mod account_config;
|
||||
pub use account_config::*;
|
||||
|
||||
mod deserialized_config;
|
||||
pub use deserialized_config::*;
|
||||
|
||||
mod deserialized_account_config;
|
||||
pub use deserialized_account_config::*;
|
|
@ -3,27 +3,58 @@
|
|||
//! This module exposes the backend trait, which can be used to create
|
||||
//! custom backend implementations.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::result;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
account,
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
msg::{self, Envelopes, Msg},
|
||||
};
|
||||
|
||||
use super::id_mapper;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use super::MaildirError;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use super::NotmuchError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ImapError(#[from] super::imap::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
AccountError(#[from] account::AccountError),
|
||||
|
||||
#[error(transparent)]
|
||||
MsgError(#[from] msg::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
IdMapperError(#[from] id_mapper::Error),
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
#[error(transparent)]
|
||||
MaildirError(#[from] MaildirError),
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[error(transparent)]
|
||||
NotmuchError(#[from] NotmuchError),
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
pub trait Backend<'a> {
|
||||
fn connect(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_mbox(&mut self, mbox: &str) -> Result<()>;
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>>;
|
||||
fn get_mboxes(&mut self) -> Result<Mboxes>;
|
||||
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>>;
|
||||
fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result<Envelopes>;
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
|
@ -31,8 +62,8 @@ pub trait Backend<'a> {
|
|||
sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>>;
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>>;
|
||||
) -> Result<Envelopes>;
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<String>;
|
||||
fn get_msg(&mut self, mbox: &str, id: &str) -> Result<Msg>;
|
||||
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
|
||||
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
|
|
@ -1,43 +1,56 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::OpenOptions,
|
||||
io::{BufRead, BufReader, Write},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
collections, fs,
|
||||
io::{self, prelude::*},
|
||||
ops, path, result,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("cannot parse id mapper cache line {0}")]
|
||||
ParseLineError(String),
|
||||
#[error("cannot find message id from short hash {0}")]
|
||||
FindFromShortHashError(String),
|
||||
#[error("the short hash {0} matches more than one hash: {1}")]
|
||||
MatchShortHashError(String, String),
|
||||
|
||||
#[error("cannot open id mapper file: {1}")]
|
||||
OpenHashMapFileError(#[source] io::Error, path::PathBuf),
|
||||
#[error("cannot write id mapper file: {1}")]
|
||||
WriteHashMapFileError(#[source] io::Error, path::PathBuf),
|
||||
#[error("cannot read line from id mapper file")]
|
||||
ReadHashMapFileLineError(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdMapper {
|
||||
path: PathBuf,
|
||||
map: HashMap<String, String>,
|
||||
path: path::PathBuf,
|
||||
map: collections::HashMap<String, String>,
|
||||
short_hash_len: usize,
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn new(dir: &Path) -> Result<Self> {
|
||||
pub fn new(dir: &path::Path) -> Result<Self> {
|
||||
let mut mapper = Self::default();
|
||||
mapper.path = dir.join(".himalaya-id-map");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&mapper.path)
|
||||
.context("cannot open id hash map file")?;
|
||||
let reader = BufReader::new(file);
|
||||
.map_err(|err| Error::OpenHashMapFileError(err, mapper.path.to_owned()))?;
|
||||
let reader = io::BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let line =
|
||||
line.context("cannot read line from maildir envelopes id mapper cache file")?;
|
||||
let line = line.map_err(Error::ReadHashMapFileLineError)?;
|
||||
if mapper.short_hash_len == 0 {
|
||||
mapper.short_hash_len = 2.max(line.parse().unwrap_or(2));
|
||||
} else {
|
||||
let (hash, id) = line.split_once(' ').ok_or_else(|| {
|
||||
anyhow!(
|
||||
"cannot parse line {:?} from maildir envelopes id mapper cache file",
|
||||
line
|
||||
)
|
||||
})?;
|
||||
let (hash, id) = line
|
||||
.split_once(' ')
|
||||
.ok_or_else(|| Error::ParseLineError(line.to_owned()))?;
|
||||
mapper.insert(hash.to_owned(), id.to_owned());
|
||||
}
|
||||
}
|
||||
|
@ -51,24 +64,16 @@ impl IdMapper {
|
|||
.filter(|hash| hash.starts_with(short_hash))
|
||||
.collect();
|
||||
if matching_hashes.len() == 0 {
|
||||
Err(anyhow!(
|
||||
"cannot find maildir message id from short hash {:?}",
|
||||
short_hash,
|
||||
))
|
||||
Err(Error::FindFromShortHashError(short_hash.to_owned()))
|
||||
} else if matching_hashes.len() > 1 {
|
||||
Err(anyhow!(
|
||||
"the short hash {:?} matches more than one hash: {}",
|
||||
short_hash,
|
||||
Err(Error::MatchShortHashError(
|
||||
short_hash.to_owned(),
|
||||
matching_hashes
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
.context(format!(
|
||||
"cannot find maildir message id from short hash {:?}",
|
||||
short_hash
|
||||
)))
|
||||
.join(", "),
|
||||
))
|
||||
} else {
|
||||
Ok(self.get(matching_hashes[0]).unwrap().to_owned())
|
||||
}
|
||||
|
@ -98,28 +103,28 @@ impl IdMapper {
|
|||
|
||||
self.short_hash_len = short_hash_len;
|
||||
|
||||
OpenOptions::new()
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&self.path)
|
||||
.context("cannot open maildir id hash map cache")?
|
||||
.map_err(|err| Error::OpenHashMapFileError(err, self.path.to_owned()))?
|
||||
.write(format!("{}\n{}", short_hash_len, entries).as_bytes())
|
||||
.context("cannot write maildir id hash map cache")?;
|
||||
.map_err(|err| Error::WriteHashMapFileError(err, self.path.to_owned()))?;
|
||||
|
||||
Ok(short_hash_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IdMapper {
|
||||
type Target = HashMap<String, String>;
|
||||
impl ops::Deref for IdMapper {
|
||||
type Target = collections::HashMap<String, String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for IdMapper {
|
||||
impl ops::DerefMut for IdMapper {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
86
lib/src/backend/imap/error.rs
Normal file
86
lib/src/backend/imap/error.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use std::result;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
account,
|
||||
msg::{self, Flags},
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("cannot get envelope of message {0}")]
|
||||
GetEnvelopeError(u32),
|
||||
#[error("cannot get sender of message {0}")]
|
||||
GetSenderError(u32),
|
||||
#[error("cannot get imap session")]
|
||||
GetSessionError,
|
||||
#[error("cannot retrieve message {0}'s uid")]
|
||||
GetMsgUidError(u32),
|
||||
#[error("cannot find message {0}")]
|
||||
FindMsgError(String),
|
||||
#[error("cannot parse sort criterion {0}")]
|
||||
ParseSortCriterionError(String),
|
||||
|
||||
#[error("cannot decode subject of message {1}")]
|
||||
DecodeSubjectError(#[source] rfc2047_decoder::Error, u32),
|
||||
#[error("cannot decode sender name of message {1}")]
|
||||
DecodeSenderNameError(#[source] rfc2047_decoder::Error, u32),
|
||||
#[error("cannot decode sender mailbox of message {1}")]
|
||||
DecodeSenderMboxError(#[source] rfc2047_decoder::Error, u32),
|
||||
#[error("cannot decode sender host of message {1}")]
|
||||
DecodeSenderHostError(#[source] rfc2047_decoder::Error, u32),
|
||||
|
||||
#[error("cannot create tls connector")]
|
||||
CreateTlsConnectorError(#[source] native_tls::Error),
|
||||
#[error("cannot connect to imap server")]
|
||||
ConnectImapServerError(#[source] imap::Error),
|
||||
#[error("cannot login to imap server")]
|
||||
LoginImapServerError(#[source] imap::Error),
|
||||
#[error("cannot search new messages")]
|
||||
SearchNewMsgsError(#[source] imap::Error),
|
||||
#[error("cannot examine mailbox {1}")]
|
||||
ExamineMboxError(#[source] imap::Error, String),
|
||||
#[error("cannot start the idle mode")]
|
||||
StartIdleModeError(#[source] imap::Error),
|
||||
#[error("cannot parse message {1}")]
|
||||
ParseMsgError(#[source] mailparse::MailParseError, String),
|
||||
#[error("cannot fetch new messages envelope")]
|
||||
FetchNewMsgsEnvelopeError(#[source] imap::Error),
|
||||
#[error("cannot get uid of message {0}")]
|
||||
GetUidError(u32),
|
||||
#[error("cannot create mailbox {1}")]
|
||||
CreateMboxError(#[source] imap::Error, String),
|
||||
#[error("cannot list mailboxes")]
|
||||
ListMboxesError(#[source] imap::Error),
|
||||
#[error("cannot delete mailbox {1}")]
|
||||
DeleteMboxError(#[source] imap::Error, String),
|
||||
#[error("cannot select mailbox {1}")]
|
||||
SelectMboxError(#[source] imap::Error, String),
|
||||
#[error("cannot fetch messages within range {1}")]
|
||||
FetchMsgsByRangeError(#[source] imap::Error, String),
|
||||
#[error("cannot fetch messages by sequence {1}")]
|
||||
FetchMsgsBySeqError(#[source] imap::Error, String),
|
||||
#[error("cannot append message to mailbox {1}")]
|
||||
AppendMsgError(#[source] imap::Error, String),
|
||||
#[error("cannot sort messages in mailbox {1} with query: {2}")]
|
||||
SortMsgsError(#[source] imap::Error, String, String),
|
||||
#[error("cannot search messages in mailbox {1} with query: {2}")]
|
||||
SearchMsgsError(#[source] imap::Error, String, String),
|
||||
#[error("cannot expunge mailbox {1}")]
|
||||
ExpungeError(#[source] imap::Error, String),
|
||||
#[error("cannot add flags {1} to message(s) {2}")]
|
||||
AddFlagsError(#[source] imap::Error, Flags, String),
|
||||
#[error("cannot set flags {1} to message(s) {2}")]
|
||||
SetFlagsError(#[source] imap::Error, Flags, String),
|
||||
#[error("cannot delete flags {1} to message(s) {2}")]
|
||||
DelFlagsError(#[source] imap::Error, Flags, String),
|
||||
#[error("cannot logout from imap server")]
|
||||
LogoutError(#[source] imap::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
AccountError(#[from] account::AccountError),
|
||||
#[error(transparent)]
|
||||
MsgError(#[from] msg::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
|
@ -2,38 +2,32 @@
|
|||
//!
|
||||
//! This module contains the definition of the IMAP backend.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use imap::types::NameAttribute;
|
||||
use log::{debug, log_enabled, trace, Level};
|
||||
use native_tls::{TlsConnector, TlsStream};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::{TryFrom, TryInto},
|
||||
net::TcpStream,
|
||||
thread,
|
||||
};
|
||||
use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread};
|
||||
|
||||
use crate::{
|
||||
backends::{
|
||||
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
|
||||
account::{Account, ImapBackendConfig},
|
||||
backend::{
|
||||
backend::Result, from_imap_fetch, from_imap_fetches,
|
||||
imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend,
|
||||
},
|
||||
config::{AccountConfig, ImapBackendConfig},
|
||||
mbox::Mboxes,
|
||||
msg::{Envelopes, Msg},
|
||||
output::run_cmd,
|
||||
mbox::{Mbox, Mboxes},
|
||||
msg::{Envelopes, Flags, Msg},
|
||||
process,
|
||||
};
|
||||
|
||||
use super::ImapFlags;
|
||||
|
||||
type ImapSess = imap::Session<TlsStream<TcpStream>>;
|
||||
|
||||
pub struct ImapBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
account_config: &'a Account,
|
||||
imap_config: &'a ImapBackendConfig,
|
||||
sess: Option<ImapSess>,
|
||||
}
|
||||
|
||||
impl<'a> ImapBackend<'a> {
|
||||
pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self {
|
||||
pub fn new(account_config: &'a Account, imap_config: &'a ImapBackendConfig) -> Self {
|
||||
Self {
|
||||
account_config,
|
||||
imap_config,
|
||||
|
@ -49,7 +43,7 @@ impl<'a> ImapBackend<'a> {
|
|||
.danger_accept_invalid_certs(self.imap_config.imap_insecure)
|
||||
.danger_accept_invalid_hostnames(self.imap_config.imap_insecure)
|
||||
.build()
|
||||
.context("cannot create TLS connector")?;
|
||||
.map_err(Error::CreateTlsConnectorError)?;
|
||||
|
||||
debug!("create client");
|
||||
debug!("host: {}", self.imap_config.imap_host);
|
||||
|
@ -62,7 +56,7 @@ impl<'a> ImapBackend<'a> {
|
|||
}
|
||||
let client = client_builder
|
||||
.connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?))
|
||||
.context("cannot connect to IMAP server")?;
|
||||
.map_err(Error::ConnectImapServerError)?;
|
||||
|
||||
debug!("create session");
|
||||
debug!("login: {}", self.imap_config.imap_login);
|
||||
|
@ -72,23 +66,24 @@ impl<'a> ImapBackend<'a> {
|
|||
&self.imap_config.imap_login,
|
||||
&self.imap_config.imap_passwd()?,
|
||||
)
|
||||
.map_err(|res| res.0)
|
||||
.context("cannot login to IMAP server")?;
|
||||
.map_err(|res| Error::LoginImapServerError(res.0))?;
|
||||
sess.debug = log_enabled!(Level::Trace);
|
||||
self.sess = Some(sess);
|
||||
}
|
||||
|
||||
match self.sess {
|
||||
let sess = match self.sess {
|
||||
Some(ref mut sess) => Ok(sess),
|
||||
None => Err(anyhow!("cannot get IMAP session")),
|
||||
}
|
||||
None => Err(Error::GetSessionError),
|
||||
}?;
|
||||
|
||||
Ok(sess)
|
||||
}
|
||||
|
||||
fn search_new_msgs(&mut self, query: &str) -> Result<Vec<u32>> {
|
||||
let uids: Vec<u32> = self
|
||||
.sess()?
|
||||
.uid_search(query)
|
||||
.context("cannot search new messages")?
|
||||
.map_err(Error::SearchNewMsgsError)?
|
||||
.into_iter()
|
||||
.collect();
|
||||
debug!("found {} new messages", uids.len());
|
||||
|
@ -103,7 +98,7 @@ impl<'a> ImapBackend<'a> {
|
|||
debug!("examine mailbox {:?}", mbox);
|
||||
self.sess()?
|
||||
.examine(mbox)
|
||||
.context(format!("cannot examine mailbox {}", mbox))?;
|
||||
.map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?;
|
||||
|
||||
debug!("init messages hashset");
|
||||
let mut msgs_set: HashSet<u32> = self
|
||||
|
@ -125,7 +120,7 @@ impl<'a> ImapBackend<'a> {
|
|||
false
|
||||
})
|
||||
})
|
||||
.context("cannot start the idle mode")?;
|
||||
.map_err(Error::StartIdleModeError)?;
|
||||
|
||||
let uids: Vec<u32> = self
|
||||
.search_new_msgs(&self.account_config.notify_query)?
|
||||
|
@ -144,13 +139,11 @@ impl<'a> ImapBackend<'a> {
|
|||
let fetches = self
|
||||
.sess()?
|
||||
.uid_fetch(uids, "(UID ENVELOPE)")
|
||||
.context("cannot fetch new messages enveloppe")?;
|
||||
.map_err(Error::FetchNewMsgsEnvelopeError)?;
|
||||
|
||||
for fetch in fetches.iter() {
|
||||
let msg = ImapEnvelope::try_from(fetch)?;
|
||||
let uid = fetch.uid.ok_or_else(|| {
|
||||
anyhow!("cannot retrieve message {}'s UID", fetch.message)
|
||||
})?;
|
||||
let msg = from_imap_fetch(fetch)?;
|
||||
let uid = fetch.uid.ok_or_else(|| Error::GetUidError(fetch.message))?;
|
||||
|
||||
let from = msg.sender.to_owned().into();
|
||||
self.account_config.run_notify_cmd(&msg.subject, &from)?;
|
||||
|
@ -173,7 +166,7 @@ impl<'a> ImapBackend<'a> {
|
|||
|
||||
self.sess()?
|
||||
.examine(mbox)
|
||||
.context(format!("cannot examine mailbox `{}`", mbox))?;
|
||||
.map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?;
|
||||
|
||||
loop {
|
||||
debug!("begin loop");
|
||||
|
@ -187,14 +180,14 @@ impl<'a> ImapBackend<'a> {
|
|||
false
|
||||
})
|
||||
})
|
||||
.context("cannot start the idle mode")?;
|
||||
.map_err(Error::StartIdleModeError)?;
|
||||
|
||||
let cmds = self.account_config.watch_cmds.clone();
|
||||
thread::spawn(move || {
|
||||
debug!("batch execution of {} cmd(s)", cmds.len());
|
||||
cmds.iter().for_each(|cmd| {
|
||||
debug!("running command {:?}…", cmd);
|
||||
let res = run_cmd(cmd);
|
||||
let res = process::run(cmd);
|
||||
debug!("{:?}", res);
|
||||
})
|
||||
});
|
||||
|
@ -206,40 +199,70 @@ impl<'a> ImapBackend<'a> {
|
|||
|
||||
impl<'a> Backend<'a> for ImapBackend<'a> {
|
||||
fn add_mbox(&mut self, mbox: &str) -> Result<()> {
|
||||
trace!(">> add mailbox");
|
||||
|
||||
self.sess()?
|
||||
.create(mbox)
|
||||
.context(format!("cannot create imap mailbox {:?}", mbox))
|
||||
.map_err(|err| Error::CreateMboxError(err, mbox.to_owned()))?;
|
||||
|
||||
trace!("<< add mailbox");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
let mboxes: ImapMboxes = self
|
||||
fn get_mboxes(&mut self) -> Result<Mboxes> {
|
||||
trace!(">> get imap mailboxes");
|
||||
|
||||
let imap_mboxes = self
|
||||
.sess()?
|
||||
.list(Some(""), Some("*"))
|
||||
.context("cannot list mailboxes")?
|
||||
.into();
|
||||
Ok(Box::new(mboxes))
|
||||
.map_err(Error::ListMboxesError)?;
|
||||
let mboxes = Mboxes {
|
||||
mboxes: imap_mboxes
|
||||
.iter()
|
||||
.map(|imap_mbox| Mbox {
|
||||
delim: imap_mbox.delimiter().unwrap_or_default().into(),
|
||||
name: imap_mbox.name().into(),
|
||||
desc: imap_mbox
|
||||
.attributes()
|
||||
.iter()
|
||||
.map(|attr| match attr {
|
||||
NameAttribute::Marked => "Marked",
|
||||
NameAttribute::Unmarked => "Unmarked",
|
||||
NameAttribute::NoSelect => "NoSelect",
|
||||
NameAttribute::NoInferiors => "NoInferiors",
|
||||
NameAttribute::Custom(custom) => custom.trim_start_matches('\\'),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
trace!("imap mailboxes: {:?}", mboxes);
|
||||
trace!("<< get imap mailboxes");
|
||||
Ok(mboxes)
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, mbox: &str) -> Result<()> {
|
||||
trace!(">> delete imap mailbox");
|
||||
|
||||
self.sess()?
|
||||
.delete(mbox)
|
||||
.context(format!("cannot delete imap mailbox {:?}", mbox))
|
||||
.map_err(|err| Error::DeleteMboxError(err, mbox.to_owned()))?;
|
||||
|
||||
trace!("<< delete imap mailbox");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result<Envelopes> {
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?
|
||||
.exists as usize;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
if last_seq == 0 {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
return Ok(Envelopes::default());
|
||||
}
|
||||
|
||||
let range = if page_size > 0 {
|
||||
|
@ -255,9 +278,10 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(format!("cannot fetch messages within range {:?}", range))?;
|
||||
let envelopes: ImapEnvelopes = fetches.try_into()?;
|
||||
Ok(Box::new(envelopes))
|
||||
.map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?;
|
||||
|
||||
let envelopes = from_imap_fetches(fetches)?;
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
|
@ -267,15 +291,15 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
) -> Result<Envelopes> {
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?
|
||||
.exists;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
if last_seq == 0 {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
return Ok(Envelopes::default());
|
||||
}
|
||||
|
||||
let begin = page * page_size;
|
||||
|
@ -283,10 +307,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
let seqs: Vec<String> = if sort.is_empty() {
|
||||
self.sess()?
|
||||
.search(query)
|
||||
.context(format!(
|
||||
"cannot find envelopes in {:?} with query {:?}",
|
||||
mbox, query
|
||||
))?
|
||||
.map_err(|err| Error::SearchMsgsError(err, mbox.to_owned(), query.to_owned()))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect()
|
||||
|
@ -295,56 +316,55 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
let charset = imap::extensions::sort::SortCharset::Utf8;
|
||||
self.sess()?
|
||||
.sort(&sort, charset, query)
|
||||
.context(format!(
|
||||
"cannot find envelopes in {:?} with query {:?}",
|
||||
mbox, query
|
||||
))?
|
||||
.map_err(|err| Error::SortMsgsError(err, mbox.to_owned(), query.to_owned()))?
|
||||
.iter()
|
||||
.map(|seq| seq.to_string())
|
||||
.collect()
|
||||
};
|
||||
if seqs.is_empty() {
|
||||
return Ok(Box::new(ImapEnvelopes::default()));
|
||||
return Ok(Envelopes::default());
|
||||
}
|
||||
|
||||
let range = seqs[begin..end.min(seqs.len())].join(",");
|
||||
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))
|
||||
.map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?;
|
||||
|
||||
let envelopes = from_imap_fetches(fetches)?;
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<String> {
|
||||
let flags: Flags = flags.into();
|
||||
self.sess()?
|
||||
.append(mbox, msg)
|
||||
.flags(<ImapFlags as Into<Vec<imap::types::Flag<'a>>>>::into(flags))
|
||||
.flags(into_imap_flags(&flags))
|
||||
.finish()
|
||||
.context(format!("cannot append message to {:?}", mbox))?;
|
||||
.map_err(|err| Error::AppendMsgError(err, mbox.to_owned()))?;
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?
|
||||
.exists;
|
||||
Ok(Box::new(last_seq))
|
||||
Ok(last_seq.to_string())
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, mbox: &str, seq: &str) -> Result<Msg> {
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?;
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(seq, "(FLAGS INTERNALDATE BODY[])")
|
||||
.context(format!("cannot fetch messages {:?}", seq))?;
|
||||
.map_err(|err| Error::FetchMsgsBySeqError(err, seq.to_owned()))?;
|
||||
let fetch = fetches
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find message {:?}", seq))?;
|
||||
.ok_or_else(|| Error::FindMsgError(seq.to_owned()))?;
|
||||
let msg_raw = fetch.body().unwrap_or_default().to_owned();
|
||||
let mut msg = Msg::from_parsed_mail(
|
||||
mailparse::parse_mail(&msg_raw).context("cannot parse message")?,
|
||||
mailparse::parse_mail(&msg_raw)
|
||||
.map_err(|err| Error::ParseMsgError(err, seq.to_owned()))?,
|
||||
self.account_config,
|
||||
)?;
|
||||
msg.raw = msg_raw;
|
||||
|
@ -370,46 +390,52 @@ impl<'a> Backend<'a> for ImapBackend<'a> {
|
|||
}
|
||||
|
||||
fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
let flags: Flags = flags.into();
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?;
|
||||
self.sess()?
|
||||
.store(seq_range, format!("+FLAGS ({})", flags))
|
||||
.context(format!("cannot add flags {:?}", &flags))?;
|
||||
.map_err(|err| Error::AddFlagsError(err, flags.to_owned(), seq_range.to_owned()))?;
|
||||
self.sess()?
|
||||
.expunge()
|
||||
.context(format!("cannot expunge mailbox {:?}", mbox))?;
|
||||
.map_err(|err| Error::ExpungeError(err, mbox.to_owned()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
let flags: Flags = flags.into();
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?;
|
||||
self.sess()?
|
||||
.store(seq_range, format!("FLAGS ({})", flags))
|
||||
.context(format!("cannot set flags {:?}", &flags))?;
|
||||
.map_err(|err| Error::SetFlagsError(err, flags.to_owned(), seq_range.to_owned()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
|
||||
let flags: ImapFlags = flags.into();
|
||||
let flags: Flags = flags.into();
|
||||
self.sess()?
|
||||
.select(mbox)
|
||||
.context(format!("cannot select mailbox {:?}", mbox))?;
|
||||
.map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?;
|
||||
self.sess()?
|
||||
.store(seq_range, format!("-FLAGS ({})", flags))
|
||||
.context(format!("cannot remove flags {:?}", &flags))?;
|
||||
.map_err(|err| Error::DelFlagsError(err, flags.to_owned(), seq_range.to_owned()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> Result<()> {
|
||||
trace!(">> imap logout");
|
||||
|
||||
if let Some(ref mut sess) = self.sess {
|
||||
debug!("logout from IMAP server");
|
||||
sess.logout().context("cannot logout from IMAP server")?;
|
||||
debug!("logout from imap server");
|
||||
sess.logout().map_err(Error::LogoutError)?;
|
||||
} else {
|
||||
debug!("no session found");
|
||||
}
|
||||
|
||||
trace!("<< imap logout");
|
||||
Ok(())
|
||||
}
|
||||
}
|
78
lib/src/backend/imap/imap_envelope.rs
Normal file
78
lib/src/backend/imap/imap_envelope.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
//! IMAP envelope module.
|
||||
//!
|
||||
//! This module provides IMAP types and conversion utilities related
|
||||
//! to the envelope.
|
||||
|
||||
use rfc2047_decoder;
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
from_imap_flags,
|
||||
imap::{Error, Result},
|
||||
},
|
||||
msg::Envelope,
|
||||
};
|
||||
|
||||
/// Represents the raw envelope returned by the `imap` crate.
|
||||
pub type ImapFetch = imap::types::Fetch;
|
||||
|
||||
pub fn from_imap_fetch(fetch: &ImapFetch) -> Result<Envelope> {
|
||||
let envelope = fetch
|
||||
.envelope()
|
||||
.ok_or_else(|| Error::GetEnvelopeError(fetch.message))?;
|
||||
|
||||
let id = fetch.message.to_string();
|
||||
|
||||
let flags = from_imap_flags(fetch.flags());
|
||||
|
||||
let subject = envelope
|
||||
.subject
|
||||
.as_ref()
|
||||
.map(|subj| {
|
||||
rfc2047_decoder::decode(subj)
|
||||
.map_err(|err| Error::DecodeSubjectError(err, fetch.message))
|
||||
})
|
||||
.unwrap_or_else(|| Ok(String::default()))?;
|
||||
|
||||
let sender = envelope
|
||||
.sender
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.get(0))
|
||||
.or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0)))
|
||||
.ok_or_else(|| Error::GetSenderError(fetch.message))?;
|
||||
let sender = if let Some(ref name) = sender.name {
|
||||
rfc2047_decoder::decode(&name.to_vec())
|
||||
.map_err(|err| Error::DecodeSenderNameError(err, fetch.message))?
|
||||
} else {
|
||||
let mbox = sender
|
||||
.mailbox
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::GetSenderError(fetch.message))
|
||||
.and_then(|mbox| {
|
||||
rfc2047_decoder::decode(&mbox.to_vec())
|
||||
.map_err(|err| Error::DecodeSenderNameError(err, fetch.message))
|
||||
})?;
|
||||
let host = sender
|
||||
.host
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::GetSenderError(fetch.message))
|
||||
.and_then(|host| {
|
||||
rfc2047_decoder::decode(&host.to_vec())
|
||||
.map_err(|err| Error::DecodeSenderNameError(err, fetch.message))
|
||||
})?;
|
||||
format!("{}@{}", mbox, host)
|
||||
};
|
||||
|
||||
let date = fetch
|
||||
.internal_date()
|
||||
.map(|date| date.naive_local().to_string());
|
||||
|
||||
Ok(Envelope {
|
||||
id: id.clone(),
|
||||
internal_id: id,
|
||||
flags,
|
||||
subject,
|
||||
sender,
|
||||
date,
|
||||
})
|
||||
}
|
18
lib/src/backend/imap/imap_envelopes.rs
Normal file
18
lib/src/backend/imap/imap_envelopes.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use crate::{
|
||||
backend::{
|
||||
imap::{from_imap_fetch, Result},
|
||||
ImapFetch,
|
||||
},
|
||||
msg::Envelopes,
|
||||
};
|
||||
|
||||
/// Represents the list of raw envelopes returned by the `imap` crate.
|
||||
pub type ImapFetches = imap::types::ZeroCopy<Vec<ImapFetch>>;
|
||||
|
||||
pub fn from_imap_fetches(fetches: ImapFetches) -> Result<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for fetch in fetches.iter().rev() {
|
||||
envelopes.push(from_imap_fetch(fetch)?);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
15
lib/src/backend/imap/imap_flag.rs
Normal file
15
lib/src/backend/imap/imap_flag.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::msg::Flag;
|
||||
|
||||
pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag {
|
||||
match imap_flag {
|
||||
imap::types::Flag::Seen => Flag::Seen,
|
||||
imap::types::Flag::Answered => Flag::Answered,
|
||||
imap::types::Flag::Flagged => Flag::Flagged,
|
||||
imap::types::Flag::Deleted => Flag::Deleted,
|
||||
imap::types::Flag::Draft => Flag::Draft,
|
||||
imap::types::Flag::Recent => Flag::Recent,
|
||||
imap::types::Flag::MayCreate => Flag::Custom(String::from("MayCreate")),
|
||||
imap::types::Flag::Custom(flag) => Flag::Custom(flag.to_string()),
|
||||
flag => Flag::Custom(flag.to_string()),
|
||||
}
|
||||
}
|
23
lib/src/backend/imap/imap_flags.rs
Normal file
23
lib/src/backend/imap/imap_flags.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use crate::{
|
||||
backend::from_imap_flag,
|
||||
msg::{Flag, Flags},
|
||||
};
|
||||
|
||||
pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec<imap::types::Flag<'a>> {
|
||||
flags
|
||||
.iter()
|
||||
.map(|flag| match flag {
|
||||
Flag::Seen => imap::types::Flag::Seen,
|
||||
Flag::Answered => imap::types::Flag::Answered,
|
||||
Flag::Flagged => imap::types::Flag::Flagged,
|
||||
Flag::Deleted => imap::types::Flag::Deleted,
|
||||
Flag::Draft => imap::types::Flag::Draft,
|
||||
Flag::Recent => imap::types::Flag::Recent,
|
||||
Flag::Custom(flag) => imap::types::Flag::Custom(flag.into()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn from_imap_flags(imap_flags: &[imap::types::Flag<'_>]) -> Flags {
|
||||
imap_flags.iter().map(from_imap_flag).collect()
|
||||
}
|
|
@ -3,9 +3,10 @@
|
|||
//! This module regroups everything related to deserialization of
|
||||
//! message sort criteria.
|
||||
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{convert::TryFrom, ops::Deref};
|
||||
|
||||
use crate::backend::imap::Error;
|
||||
|
||||
/// Represents the message sort criteria. It is just a wrapper around
|
||||
/// the `imap::extensions::sort::SortCriterion`.
|
||||
pub struct SortCriteria<'a>(Vec<imap::extensions::sort::SortCriterion<'a>>);
|
||||
|
@ -53,7 +54,7 @@ impl<'a> TryFrom<&'a str> for SortCriteria<'a> {
|
|||
"to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
|
||||
&imap::extensions::sort::SortCriterion::To,
|
||||
)),
|
||||
_ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)),
|
||||
_ => Err(Error::ParseSortCriterionError(criterion_str.to_owned())),
|
||||
}?);
|
||||
}
|
||||
Ok(Self(criteria))
|
49
lib/src/backend/maildir/error.rs
Normal file
49
lib/src/backend/maildir/error.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use std::{io, path};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MaildirError {
|
||||
#[error("cannot find maildir sender")]
|
||||
FindSenderError,
|
||||
#[error("cannot read maildir directory {0}")]
|
||||
ReadDirError(path::PathBuf),
|
||||
#[error("cannot parse maildir subdirectory {0}")]
|
||||
ParseSubdirError(path::PathBuf),
|
||||
#[error("cannot get maildir envelopes at page {0}")]
|
||||
GetEnvelopesOutOfBoundsError(usize),
|
||||
#[error("cannot search maildir envelopes: feature not implemented")]
|
||||
SearchEnvelopesUnimplementedError,
|
||||
#[error("cannot get maildir message {0}")]
|
||||
GetMsgError(String),
|
||||
#[error("cannot decode maildir entry")]
|
||||
DecodeEntryError(#[source] io::Error),
|
||||
#[error("cannot parse maildir message")]
|
||||
ParseMsgError(#[source] maildir::MailEntryError),
|
||||
#[error("cannot decode header {0}")]
|
||||
DecodeHeaderError(#[source] rfc2047_decoder::Error, String),
|
||||
#[error("cannot parse maildir message header {0}")]
|
||||
ParseHeaderError(#[source] mailparse::MailParseError, String),
|
||||
#[error("cannot create maildir subdirectory {1}")]
|
||||
CreateSubdirError(#[source] io::Error, String),
|
||||
#[error("cannot decode maildir subdirectory")]
|
||||
DecodeSubdirError(#[source] io::Error),
|
||||
#[error("cannot delete subdirectories at {1}")]
|
||||
DeleteAllDirError(#[source] io::Error, path::PathBuf),
|
||||
#[error("cannot get current directory")]
|
||||
GetCurrentDirError(#[source] io::Error),
|
||||
#[error("cannot store maildir message with flags")]
|
||||
StoreWithFlagsError(#[source] maildir::MaildirError),
|
||||
#[error("cannot copy maildir message")]
|
||||
CopyMsgError(#[source] io::Error),
|
||||
#[error("cannot move maildir message")]
|
||||
MoveMsgError(#[source] io::Error),
|
||||
#[error("cannot delete maildir message")]
|
||||
DelMsgError(#[source] io::Error),
|
||||
#[error("cannot add maildir flags")]
|
||||
AddFlagsError(#[source] io::Error),
|
||||
#[error("cannot set maildir flags")]
|
||||
SetFlagsError(#[source] io::Error),
|
||||
#[error("cannot remove maildir flags")]
|
||||
DelFlagsError(#[source] io::Error),
|
||||
}
|
356
lib/src/backend/maildir/maildir_backend.rs
Normal file
356
lib/src/backend/maildir/maildir_backend.rs
Normal file
|
@ -0,0 +1,356 @@
|
|||
//! Maildir backend module.
|
||||
//!
|
||||
//! This module contains the definition of the maildir backend and its
|
||||
//! traits implementation.
|
||||
|
||||
use log::{debug, info, trace};
|
||||
use std::{env, ffi::OsStr, fs, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
account::{Account, MaildirBackendConfig},
|
||||
backend::{backend::Result, maildir_envelopes, maildir_flags, Backend, IdMapper},
|
||||
mbox::{Mbox, Mboxes},
|
||||
msg::{Envelopes, Flags, Msg},
|
||||
};
|
||||
|
||||
use super::MaildirError;
|
||||
|
||||
/// Represents the maildir backend.
|
||||
pub struct MaildirBackend<'a> {
|
||||
account_config: &'a Account,
|
||||
mdir: maildir::Maildir,
|
||||
}
|
||||
|
||||
impl<'a> MaildirBackend<'a> {
|
||||
pub fn new(account_config: &'a Account, maildir_config: &'a MaildirBackendConfig) -> Self {
|
||||
Self {
|
||||
account_config,
|
||||
mdir: maildir_config.maildir_dir.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result<PathBuf> {
|
||||
let path = if mdir_path.is_dir() {
|
||||
Ok(mdir_path)
|
||||
} else {
|
||||
Err(MaildirError::ReadDirError(mdir_path.to_owned()))
|
||||
}?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Creates a maildir instance from a string slice.
|
||||
pub fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> {
|
||||
let dir = self.account_config.get_mbox_alias(dir)?;
|
||||
|
||||
// If the dir points to the inbox folder, creates a maildir
|
||||
// instance from the root folder.
|
||||
if &dir == "inbox" {
|
||||
return self
|
||||
.validate_mdir_path(self.mdir.path().to_owned())
|
||||
.map(maildir::Maildir::from);
|
||||
}
|
||||
|
||||
// If the dir is a valid maildir path, creates a maildir
|
||||
// instance from it. First checks for absolute path,
|
||||
self.validate_mdir_path((&dir).into())
|
||||
// then for relative path to `maildir-dir`,
|
||||
.or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir)))
|
||||
// and finally for relative path to the current directory.
|
||||
.or_else(|_| {
|
||||
self.validate_mdir_path(
|
||||
env::current_dir()
|
||||
.map_err(MaildirError::GetCurrentDirError)?
|
||||
.join(&dir),
|
||||
)
|
||||
})
|
||||
.or_else(|_| {
|
||||
// Otherwise creates a maildir instance from a maildir
|
||||
// subdirectory by adding a "." in front of the name
|
||||
// as described in the [spec].
|
||||
//
|
||||
// [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
|
||||
self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir)))
|
||||
})
|
||||
.map(maildir::Maildir::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Backend<'a> for MaildirBackend<'a> {
|
||||
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)
|
||||
.map_err(|err| MaildirError::CreateSubdirError(err, subdir.to_owned()))?;
|
||||
|
||||
info!("<< add maildir subdir");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Mboxes> {
|
||||
trace!(">> get maildir mailboxes");
|
||||
|
||||
let mut mboxes = Mboxes::default();
|
||||
for (name, desc) in &self.account_config.mailboxes {
|
||||
mboxes.push(Mbox {
|
||||
delim: String::from("/"),
|
||||
name: name.into(),
|
||||
desc: desc.into(),
|
||||
})
|
||||
}
|
||||
for entry in self.mdir.list_subdirs() {
|
||||
let dir = entry.map_err(MaildirError::DecodeSubdirError)?;
|
||||
let dirname = dir.path().file_name();
|
||||
mboxes.push(Mbox {
|
||||
delim: String::from("/"),
|
||||
name: dirname
|
||||
.and_then(OsStr::to_str)
|
||||
.and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) })
|
||||
.ok_or_else(|| MaildirError::ParseSubdirError(dir.path().to_owned()))?
|
||||
.into(),
|
||||
..Mbox::default()
|
||||
});
|
||||
}
|
||||
|
||||
trace!("maildir mailboxes: {:?}", mboxes);
|
||||
trace!("<< get maildir mailboxes");
|
||||
Ok(mboxes)
|
||||
}
|
||||
|
||||
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)
|
||||
.map_err(|err| MaildirError::DeleteAllDirError(err, path.to_owned()))?;
|
||||
|
||||
info!("<< delete maildir dir");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_envelopes(&mut self, dir: &str, page_size: usize, page: usize) -> Result<Envelopes> {
|
||||
info!(">> get maildir envelopes");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let mdir = self.get_mdir_from_dir(dir)?;
|
||||
|
||||
// Reads envelopes from the "cur" folder of the selected
|
||||
// maildir.
|
||||
let mut envelopes = maildir_envelopes::from_maildir_entries(mdir.list_cur())?;
|
||||
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(MaildirError::GetEnvelopesOutOfBoundsError(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.envelopes = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
// 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.id.to_owned(), env.internal_id.to_owned()))
|
||||
.collect();
|
||||
mapper.append(entries)?
|
||||
};
|
||||
debug!("short hash length: {:?}", short_hash_len);
|
||||
|
||||
// Shorten envelopes hash.
|
||||
envelopes
|
||||
.iter_mut()
|
||||
.for_each(|env| env.id = env.id[0..short_hash_len].to_owned());
|
||||
|
||||
info!("<< get maildir envelopes");
|
||||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
_dir: &str,
|
||||
_query: &str,
|
||||
_sort: &str,
|
||||
_page_size: usize,
|
||||
_page: usize,
|
||||
) -> Result<Envelopes> {
|
||||
info!(">> search maildir envelopes");
|
||||
info!("<< search maildir envelopes");
|
||||
Err(MaildirError::SearchEnvelopesUnimplementedError)?
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result<String> {
|
||||
info!(">> add maildir message");
|
||||
debug!("dir: {:?}", dir);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let flags = Flags::from(flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self.get_mdir_from_dir(dir)?;
|
||||
let id = mdir
|
||||
.store_cur_with_flags(msg, &maildir_flags::to_normalized_string(&flags))
|
||||
.map_err(MaildirError::StoreWithFlagsError)?;
|
||||
debug!("id: {:?}", id);
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
debug!("hash: {:?}", hash);
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
let mut mapper = IdMapper::new(mdir.path())?;
|
||||
mapper.append(vec![(hash.clone(), id.clone())])?;
|
||||
|
||||
info!("<< add maildir message");
|
||||
Ok(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)?;
|
||||
let id = IdMapper::new(mdir.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let mut mail_entry = mdir
|
||||
.find(&id)
|
||||
.ok_or_else(|| MaildirError::GetMsgError(id.to_owned()))?;
|
||||
let parsed_mail = mail_entry.parsed().map_err(MaildirError::ParseMsgError)?;
|
||||
let msg = Msg::from_parsed_mail(parsed_mail, self.account_config)?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< get maildir message");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
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)?;
|
||||
let mdir_dst = self.get_mdir_from_dir(dir_dst)?;
|
||||
let id = IdMapper::new(mdir_src.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
|
||||
mdir_src
|
||||
.copy_to(&id, &mdir_dst)
|
||||
.map_err(MaildirError::CopyMsgError)?;
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
let mut mapper = IdMapper::new(mdir_dst.path())?;
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
mapper.append(vec![(hash.clone(), id.clone())])?;
|
||||
|
||||
info!("<< copy maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)?;
|
||||
let mdir_dst = self.get_mdir_from_dir(dir_dst)?;
|
||||
let id = IdMapper::new(mdir_src.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
|
||||
mdir_src
|
||||
.move_to(&id, &mdir_dst)
|
||||
.map_err(MaildirError::MoveMsgError)?;
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
let mut mapper = IdMapper::new(mdir_dst.path())?;
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
mapper.append(vec![(hash.clone(), id.clone())])?;
|
||||
|
||||
info!("<< move maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)?;
|
||||
let id = IdMapper::new(mdir.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.delete(&id).map_err(MaildirError::DelMsgError)?;
|
||||
|
||||
info!("<< delete maildir message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
let flags = Flags::from(flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self.get_mdir_from_dir(dir)?;
|
||||
let id = IdMapper::new(mdir.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
|
||||
mdir.add_flags(&id, &maildir_flags::to_normalized_string(&flags))
|
||||
.map_err(MaildirError::AddFlagsError)?;
|
||||
|
||||
info!("<< add maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
let flags = Flags::from(flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self.get_mdir_from_dir(dir)?;
|
||||
let id = IdMapper::new(mdir.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.set_flags(&id, &maildir_flags::to_normalized_string(&flags))
|
||||
.map_err(MaildirError::SetFlagsError)?;
|
||||
|
||||
info!("<< set maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
let flags = Flags::from(flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
|
||||
let mdir = self.get_mdir_from_dir(dir)?;
|
||||
let id = IdMapper::new(mdir.path())?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
mdir.remove_flags(&id, &maildir_flags::to_normalized_string(&flags))
|
||||
.map_err(MaildirError::DelFlagsError)?;
|
||||
|
||||
info!("<< delete maildir message flags");
|
||||
Ok(())
|
||||
}
|
||||
}
|
72
lib/src/backend/maildir/maildir_envelope.rs
Normal file
72
lib/src/backend/maildir/maildir_envelope.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use chrono::DateTime;
|
||||
use log::trace;
|
||||
|
||||
use crate::{
|
||||
backend::{backend::Result, maildir_flags},
|
||||
msg::{from_slice_to_addrs, Addr, Envelope},
|
||||
};
|
||||
|
||||
use super::MaildirError;
|
||||
|
||||
/// Represents the raw envelope returned by the `maildir` crate.
|
||||
pub type MaildirEnvelope = maildir::MailEntry;
|
||||
|
||||
pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result<Envelope> {
|
||||
trace!(">> build envelope from maildir parsed mail");
|
||||
|
||||
let mut envelope = Envelope::default();
|
||||
|
||||
envelope.internal_id = entry.id().to_owned();
|
||||
envelope.id = format!("{:x}", md5::compute(&envelope.internal_id));
|
||||
envelope.flags = maildir_flags::from_maildir_entry(&entry);
|
||||
|
||||
let parsed_mail = entry.parsed().map_err(MaildirError::ParseMsgError)?;
|
||||
|
||||
trace!(">> parse headers");
|
||||
for h in parsed_mail.get_headers() {
|
||||
let k = h.get_key();
|
||||
trace!("header key: {:?}", k);
|
||||
|
||||
let v = rfc2047_decoder::decode(h.get_value_raw())
|
||||
.map_err(|err| MaildirError::DecodeHeaderError(err, k.to_owned()))?;
|
||||
trace!("header value: {:?}", v);
|
||||
|
||||
match k.to_lowercase().as_str() {
|
||||
"date" => {
|
||||
envelope.date =
|
||||
DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0)
|
||||
.map(|date| date.naive_local().to_string())
|
||||
.ok()
|
||||
}
|
||||
"subject" => {
|
||||
envelope.subject = v.into();
|
||||
}
|
||||
"from" => {
|
||||
envelope.sender = from_slice_to_addrs(v)
|
||||
.map_err(|err| MaildirError::ParseHeaderError(err, k.to_owned()))?
|
||||
.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(|| MaildirError::FindSenderError)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
trace!("<< parse headers");
|
||||
|
||||
trace!("envelope: {:?}", envelope);
|
||||
trace!("<< build envelope from maildir parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
21
lib/src/backend/maildir/maildir_envelopes.rs
Normal file
21
lib/src/backend/maildir/maildir_envelopes.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
//! Maildir mailbox module.
|
||||
//!
|
||||
//! This module provides Maildir types and conversion utilities
|
||||
//! related to the envelope.
|
||||
|
||||
use crate::{backend::backend::Result, msg::Envelopes};
|
||||
|
||||
use super::{maildir_envelope, MaildirError};
|
||||
|
||||
/// Represents a list of raw envelopees returned by the `maildir`
|
||||
/// crate.
|
||||
pub type MaildirEnvelopes = maildir::MailEntries;
|
||||
|
||||
pub fn from_maildir_entries(mail_entries: MaildirEnvelopes) -> Result<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for entry in mail_entries {
|
||||
let entry = entry.map_err(MaildirError::DecodeEntryError)?;
|
||||
envelopes.push(maildir_envelope::from_maildir_entry(entry)?);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
24
lib/src/backend/maildir/maildir_flag.rs
Normal file
24
lib/src/backend/maildir/maildir_flag.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use crate::msg::Flag;
|
||||
|
||||
pub fn from_char(c: char) -> Flag {
|
||||
match c {
|
||||
'r' | 'R' => Flag::Answered,
|
||||
's' | 'S' => Flag::Seen,
|
||||
't' | 'T' => Flag::Deleted,
|
||||
'd' | 'D' => Flag::Draft,
|
||||
'f' | 'F' => Flag::Flagged,
|
||||
'p' | 'P' => Flag::Custom(String::from("Passed")),
|
||||
flag => Flag::Custom(flag.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_normalized_char(flag: &Flag) -> Option<char> {
|
||||
match flag {
|
||||
Flag::Answered => Some('R'),
|
||||
Flag::Seen => Some('S'),
|
||||
Flag::Deleted => Some('T'),
|
||||
Flag::Draft => Some('D'),
|
||||
Flag::Flagged => Some('F'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
11
lib/src/backend/maildir/maildir_flags.rs
Normal file
11
lib/src/backend/maildir/maildir_flags.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use crate::msg::Flags;
|
||||
|
||||
use super::maildir_flag;
|
||||
|
||||
pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags {
|
||||
entry.flags().chars().map(maildir_flag::from_char).collect()
|
||||
}
|
||||
|
||||
pub fn to_normalized_string(flags: &Flags) -> String {
|
||||
String::from_iter(flags.iter().filter_map(maildir_flag::to_normalized_char))
|
||||
}
|
73
lib/src/backend/mod.rs
Normal file
73
lib/src/backend/mod.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
pub mod backend;
|
||||
pub use backend::*;
|
||||
|
||||
pub mod id_mapper;
|
||||
pub use id_mapper::*;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub mod imap {
|
||||
pub mod imap_backend;
|
||||
pub use imap_backend::*;
|
||||
|
||||
pub mod imap_envelopes;
|
||||
pub use imap_envelopes::*;
|
||||
|
||||
pub mod imap_envelope;
|
||||
pub use imap_envelope::*;
|
||||
|
||||
pub mod imap_flags;
|
||||
pub use imap_flags::*;
|
||||
|
||||
pub mod imap_flag;
|
||||
pub use imap_flag::*;
|
||||
|
||||
pub mod msg_sort_criterion;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub use self::imap::*;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub mod maildir {
|
||||
pub mod maildir_backend;
|
||||
pub use maildir_backend::*;
|
||||
|
||||
pub mod maildir_envelopes;
|
||||
pub use maildir_envelopes::*;
|
||||
|
||||
pub mod maildir_envelope;
|
||||
pub use maildir_envelope::*;
|
||||
|
||||
pub mod maildir_flags;
|
||||
pub use maildir_flags::*;
|
||||
|
||||
pub mod maildir_flag;
|
||||
pub use maildir_flag::*;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub use self::maildir::*;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub mod notmuch {
|
||||
pub mod notmuch_backend;
|
||||
pub use notmuch_backend::*;
|
||||
|
||||
pub mod notmuch_envelopes;
|
||||
pub use notmuch_envelopes::*;
|
||||
|
||||
pub mod notmuch_envelope;
|
||||
pub use notmuch_envelope::*;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub use self::notmuch::*;
|
49
lib/src/backend/notmuch/error.rs
Normal file
49
lib/src/backend/notmuch/error.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use std::io;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NotmuchError {
|
||||
#[error("cannot parse notmuch message header {1}")]
|
||||
ParseMsgHeaderError(#[source] notmuch::Error, String),
|
||||
#[error("cannot parse notmuch message date {1}")]
|
||||
ParseMsgDateError(#[source] chrono::ParseError, String),
|
||||
#[error("cannot find notmuch message header {0}")]
|
||||
FindMsgHeaderError(String),
|
||||
#[error("cannot find notmuch message sender")]
|
||||
FindSenderError,
|
||||
#[error("cannot parse notmuch message senders {1}")]
|
||||
ParseSendersError(#[source] mailparse::MailParseError, String),
|
||||
#[error("cannot open notmuch database")]
|
||||
OpenDbError(#[source] notmuch::Error),
|
||||
#[error("cannot build notmuch query")]
|
||||
BuildQueryError(#[source] notmuch::Error),
|
||||
#[error("cannot search notmuch envelopes")]
|
||||
SearchEnvelopesError(#[source] notmuch::Error),
|
||||
#[error("cannot get notmuch envelopes at page {0}")]
|
||||
GetEnvelopesOutOfBoundsError(usize),
|
||||
#[error("cannot add notmuch mailbox: feature not implemented")]
|
||||
AddMboxUnimplementedError,
|
||||
#[error("cannot delete notmuch mailbox: feature not implemented")]
|
||||
DelMboxUnimplementedError,
|
||||
#[error("cannot copy notmuch message: feature not implemented")]
|
||||
CopyMsgUnimplementedError,
|
||||
#[error("cannot move notmuch message: feature not implemented")]
|
||||
MoveMsgUnimplementedError,
|
||||
#[error("cannot index notmuch message")]
|
||||
IndexFileError(#[source] notmuch::Error),
|
||||
#[error("cannot find notmuch message")]
|
||||
FindMsgError(#[source] notmuch::Error),
|
||||
#[error("cannot find notmuch message")]
|
||||
FindMsgEmptyError,
|
||||
#[error("cannot read notmuch raw message from file")]
|
||||
ReadMsgError(#[source] io::Error),
|
||||
#[error("cannot parse notmuch raw message")]
|
||||
ParseMsgError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot delete notmuch message")]
|
||||
DelMsgError(#[source] notmuch::Error),
|
||||
#[error("cannot add notmuch tag")]
|
||||
AddTagError(#[source] notmuch::Error),
|
||||
#[error("cannot delete notmuch tag")]
|
||||
DelTagError(#[source] notmuch::Error),
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
use std::{convert::TryInto, fs};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
use std::fs;
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
|
||||
config::{AccountConfig, NotmuchBackendConfig},
|
||||
mbox::Mboxes,
|
||||
account::{Account, NotmuchBackendConfig},
|
||||
backend::{
|
||||
backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError,
|
||||
},
|
||||
mbox::{Mbox, Mboxes},
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
|
||||
/// Represents the Notmuch backend.
|
||||
pub struct NotmuchBackend<'a> {
|
||||
account_config: &'a AccountConfig,
|
||||
account_config: &'a Account,
|
||||
notmuch_config: &'a NotmuchBackendConfig,
|
||||
pub mdir: &'a mut MaildirBackend<'a>,
|
||||
db: notmuch::Database,
|
||||
|
@ -20,7 +20,7 @@ pub struct NotmuchBackend<'a> {
|
|||
|
||||
impl<'a> NotmuchBackend<'a> {
|
||||
pub fn new(
|
||||
account_config: &'a AccountConfig,
|
||||
account_config: &'a Account,
|
||||
notmuch_config: &'a NotmuchBackendConfig,
|
||||
mdir: &'a mut MaildirBackend<'a>,
|
||||
) -> Result<NotmuchBackend<'a>> {
|
||||
|
@ -34,12 +34,7 @@ impl<'a> NotmuchBackend<'a> {
|
|||
notmuch_config.notmuch_database_dir.clone(),
|
||||
notmuch::DatabaseMode::ReadWrite,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot open notmuch database at {:?}",
|
||||
notmuch_config.notmuch_database_dir
|
||||
)
|
||||
})?,
|
||||
.map_err(NotmuchError::OpenDbError)?,
|
||||
};
|
||||
|
||||
info!("<< create new notmuch backend");
|
||||
|
@ -51,17 +46,17 @@ impl<'a> NotmuchBackend<'a> {
|
|||
query: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
) -> Result<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))?;
|
||||
.map_err(NotmuchError::BuildQueryError)?;
|
||||
let mut envelopes = notmuch_envelopes::from_notmuch_msgs(
|
||||
query_builder
|
||||
.search_messages()
|
||||
.map_err(NotmuchError::SearchEnvelopesError)?,
|
||||
)?;
|
||||
debug!("envelopes len: {:?}", envelopes.len());
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
|
@ -69,10 +64,7 @@ impl<'a> NotmuchBackend<'a> {
|
|||
let page_begin = page * page_size;
|
||||
debug!("page begin: {:?}", page_begin);
|
||||
if page_begin > envelopes.len() {
|
||||
return Err(anyhow!(
|
||||
"cannot get notmuch envelopes at page {:?} (out of bounds)",
|
||||
page_begin + 1,
|
||||
));
|
||||
return Err(NotmuchError::GetEnvelopesOutOfBoundsError(page_begin + 1))?;
|
||||
}
|
||||
let page_end = envelopes.len().min(page_begin + page_size);
|
||||
debug!("page end: {:?}", page_end);
|
||||
|
@ -91,7 +83,7 @@ impl<'a> NotmuchBackend<'a> {
|
|||
let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?;
|
||||
let entries = envelopes
|
||||
.iter()
|
||||
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
|
||||
.map(|env| (env.id.to_owned(), env.internal_id.to_owned()))
|
||||
.collect();
|
||||
mapper.append(entries)?
|
||||
};
|
||||
|
@ -100,9 +92,9 @@ impl<'a> NotmuchBackend<'a> {
|
|||
// Shorten envelopes hash.
|
||||
envelopes
|
||||
.iter_mut()
|
||||
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
|
||||
.for_each(|env| env.id = env.id[0..short_hash_len].to_owned());
|
||||
|
||||
Ok(Box::new(envelopes))
|
||||
Ok(envelopes)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,33 +102,31 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
fn add_mbox(&mut self, _mbox: &str) -> Result<()> {
|
||||
info!(">> add notmuch mailbox");
|
||||
info!("<< add notmuch mailbox");
|
||||
Err(anyhow!(
|
||||
"cannot add notmuch mailbox: feature not implemented"
|
||||
))
|
||||
Err(NotmuchError::AddMboxUnimplementedError)?
|
||||
}
|
||||
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
info!(">> get notmuch virtual mailboxes");
|
||||
fn get_mboxes(&mut self) -> Result<Mboxes> {
|
||||
trace!(">> get notmuch virtual mailboxes");
|
||||
|
||||
let mut mboxes: Vec<_> = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.iter()
|
||||
.map(|(k, v)| NotmuchMbox::new(k, v))
|
||||
.collect();
|
||||
trace!("virtual mailboxes: {:?}", mboxes);
|
||||
let mut mboxes = Mboxes::default();
|
||||
for (name, desc) in &self.account_config.mailboxes {
|
||||
mboxes.push(Mbox {
|
||||
name: name.into(),
|
||||
desc: desc.into(),
|
||||
..Mbox::default()
|
||||
})
|
||||
}
|
||||
mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
|
||||
info!("<< get notmuch virtual mailboxes");
|
||||
Ok(Box::new(NotmuchMboxes { mboxes }))
|
||||
trace!("notmuch virtual mailboxes: {:?}", mboxes);
|
||||
trace!("<< get notmuch virtual mailboxes");
|
||||
Ok(mboxes)
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, _mbox: &str) -> Result<()> {
|
||||
info!(">> delete notmuch mailbox");
|
||||
info!("<< delete notmuch mailbox");
|
||||
Err(anyhow!(
|
||||
"cannot delete notmuch mailbox: feature not implemented"
|
||||
))
|
||||
Err(NotmuchError::DelMboxUnimplementedError)?
|
||||
}
|
||||
|
||||
fn get_envelopes(
|
||||
|
@ -144,7 +134,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
virt_mbox: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
) -> Result<Envelopes> {
|
||||
info!(">> get notmuch envelopes");
|
||||
debug!("virtual mailbox: {:?}", virt_mbox);
|
||||
debug!("page size: {:?}", page_size);
|
||||
|
@ -170,7 +160,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
_sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Box<dyn Envelopes>> {
|
||||
) -> Result<Envelopes> {
|
||||
info!(">> search notmuch envelopes");
|
||||
debug!("virtual mailbox: {:?}", virt_mbox);
|
||||
debug!("query: {:?}", query);
|
||||
|
@ -193,61 +183,42 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
Ok(envelopes)
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result<Box<dyn ToString>> {
|
||||
fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result<String> {
|
||||
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("", msg, "seen")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot add notmuch message to maildir {:?}",
|
||||
self.notmuch_config.notmuch_database_dir
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
let hash = self.mdir.add_msg("", msg, "seen")?;
|
||||
debug!("hash: {:?}", hash);
|
||||
|
||||
// Retrieves the file path of the added message by its maildir
|
||||
// identifier.
|
||||
let mut mapper = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?;
|
||||
let id = mapper
|
||||
.find(&hash)
|
||||
.with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?;
|
||||
let mut mapper = IdMapper::new(dir)?;
|
||||
let id = mapper.find(&hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let file_path = dir.join("cur").join(format!("{}:2,S", id));
|
||||
debug!("file path: {:?}", file_path);
|
||||
|
||||
println!("file_path: {:?}", file_path);
|
||||
// Adds the message to the notmuch database by indexing it.
|
||||
let id = self
|
||||
.db
|
||||
.index_file(&file_path, None)
|
||||
.with_context(|| format!("cannot index notmuch message from file {:?}", file_path))?
|
||||
.map_err(NotmuchError::IndexFileError)?
|
||||
.id()
|
||||
.to_string();
|
||||
let hash = format!("{:x}", md5::compute(&id));
|
||||
|
||||
// Appends hash entry to the id mapper cache file.
|
||||
mapper
|
||||
.append(vec![(hash.clone(), id.clone())])
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot append hash {:?} with id {:?} to id mapper",
|
||||
hash, id
|
||||
)
|
||||
})?;
|
||||
mapper.append(vec![(hash.clone(), id.clone())])?;
|
||||
|
||||
// Attaches tags to the notmuch message.
|
||||
self.add_flags("", &hash, tags)
|
||||
.with_context(|| format!("cannot add flags to notmuch message {:?}", id))?;
|
||||
self.add_flags("", &hash, tags)?;
|
||||
|
||||
info!("<< add notmuch envelopes");
|
||||
Ok(Box::new(hash))
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
fn get_msg(&mut self, _: &str, short_hash: &str) -> Result<Msg> {
|
||||
|
@ -255,31 +226,19 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
debug!("short hash: {:?}", short_hash);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
let id = IdMapper::new(dir)?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let msg_file_path = self
|
||||
.db
|
||||
.find_message(&id)
|
||||
.with_context(|| format!("cannot find notmuch message {:?}", id))?
|
||||
.ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))?
|
||||
.map_err(NotmuchError::FindMsgError)?
|
||||
.ok_or_else(|| NotmuchError::FindMsgEmptyError)?
|
||||
.filename()
|
||||
.to_owned();
|
||||
debug!("message file path: {:?}", msg_file_path);
|
||||
let raw_msg = fs::read(&msg_file_path).with_context(|| {
|
||||
format!("cannot read notmuch message from file {:?}", msg_file_path)
|
||||
})?;
|
||||
let msg = mailparse::parse_mail(&raw_msg)
|
||||
.with_context(|| format!("cannot parse raw notmuch message {:?}", id))?;
|
||||
let msg = Msg::from_parsed_mail(msg, &self.account_config)
|
||||
.with_context(|| format!("cannot parse notmuch message {:?}", id))?;
|
||||
let raw_msg = fs::read(&msg_file_path).map_err(NotmuchError::ReadMsgError)?;
|
||||
let msg = mailparse::parse_mail(&raw_msg).map_err(NotmuchError::ParseMsgError)?;
|
||||
let msg = Msg::from_parsed_mail(msg, &self.account_config)?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< get notmuch message");
|
||||
|
@ -289,17 +248,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> {
|
||||
info!(">> copy notmuch message");
|
||||
info!("<< copy notmuch message");
|
||||
Err(anyhow!(
|
||||
"cannot copy notmuch message: feature not implemented"
|
||||
))
|
||||
Err(NotmuchError::CopyMsgUnimplementedError)?
|
||||
}
|
||||
|
||||
fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> {
|
||||
info!(">> move notmuch message");
|
||||
info!("<< move notmuch message");
|
||||
Err(anyhow!(
|
||||
"cannot move notmuch message: feature not implemented"
|
||||
))
|
||||
Err(NotmuchError::MoveMsgUnimplementedError)?
|
||||
}
|
||||
|
||||
fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> {
|
||||
|
@ -307,27 +262,19 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
debug!("short hash: {:?}", short_hash);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
let id = IdMapper::new(dir)?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let msg_file_path = self
|
||||
.db
|
||||
.find_message(&id)
|
||||
.with_context(|| format!("cannot find notmuch message {:?}", id))?
|
||||
.ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))?
|
||||
.map_err(NotmuchError::FindMsgError)?
|
||||
.ok_or_else(|| NotmuchError::FindMsgEmptyError)?
|
||||
.filename()
|
||||
.to_owned();
|
||||
debug!("message file path: {:?}", msg_file_path);
|
||||
self.db
|
||||
.remove_message(msg_file_path)
|
||||
.with_context(|| format!("cannot delete notmuch message {:?}", id))?;
|
||||
.map_err(NotmuchError::DelMsgError)?;
|
||||
|
||||
info!("<< delete notmuch message");
|
||||
Ok(())
|
||||
|
@ -338,15 +285,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
let id = IdMapper::new(dir)?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
|
@ -354,15 +293,14 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
.map_err(NotmuchError::BuildQueryError)?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
.map_err(NotmuchError::SearchEnvelopesError)?;
|
||||
|
||||
for msg in msgs {
|
||||
for tag in tags.iter() {
|
||||
msg.add_tag(*tag).with_context(|| {
|
||||
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
|
||||
})?
|
||||
msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,15 +313,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
let id = IdMapper::new(dir)?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
|
@ -391,18 +321,15 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
.map_err(NotmuchError::BuildQueryError)?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
.map_err(NotmuchError::SearchEnvelopesError)?;
|
||||
for msg in msgs {
|
||||
msg.remove_all_tags().with_context(|| {
|
||||
format!("cannot remove all tags from notmuch message {:?}", msg.id())
|
||||
})?;
|
||||
msg.remove_all_tags().map_err(NotmuchError::DelTagError)?;
|
||||
|
||||
for tag in tags.iter() {
|
||||
msg.add_tag(*tag).with_context(|| {
|
||||
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
|
||||
})?
|
||||
msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -415,15 +342,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
debug!("tags: {:?}", tags);
|
||||
|
||||
let dir = &self.notmuch_config.notmuch_database_dir;
|
||||
let id = IdMapper::new(dir)
|
||||
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
|
||||
.find(short_hash)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot find notmuch message from short hash {:?}",
|
||||
short_hash
|
||||
)
|
||||
})?;
|
||||
let id = IdMapper::new(dir)?.find(short_hash)?;
|
||||
debug!("id: {:?}", id);
|
||||
let query = format!("id:{}", id);
|
||||
debug!("query: {:?}", query);
|
||||
|
@ -431,19 +350,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
let query_builder = self
|
||||
.db
|
||||
.create_query(&query)
|
||||
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
|
||||
.map_err(NotmuchError::BuildQueryError)?;
|
||||
let msgs = query_builder
|
||||
.search_messages()
|
||||
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
|
||||
.map_err(NotmuchError::SearchEnvelopesError)?;
|
||||
for msg in msgs {
|
||||
for tag in tags.iter() {
|
||||
msg.remove_tag(*tag).with_context(|| {
|
||||
format!(
|
||||
"cannot delete tag {:?} from notmuch message {:?}",
|
||||
tag,
|
||||
msg.id()
|
||||
)
|
||||
})?
|
||||
msg.remove_tag(*tag).map_err(NotmuchError::DelTagError)?;
|
||||
}
|
||||
}
|
||||
|
73
lib/src/backend/notmuch/notmuch_envelope.rs
Normal file
73
lib/src/backend/notmuch/notmuch_envelope.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
//! Notmuch mailbox module.
|
||||
//!
|
||||
//! This module provides Notmuch types and conversion utilities
|
||||
//! related to the envelope
|
||||
|
||||
use chrono::DateTime;
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
backend::{backend::Result, NotmuchError},
|
||||
msg::{from_slice_to_addrs, Addr, Envelope, Flag},
|
||||
};
|
||||
|
||||
/// Represents the raw envelope returned by the `notmuch` crate.
|
||||
pub type RawNotmuchEnvelope = notmuch::Message;
|
||||
|
||||
pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result<Envelope> {
|
||||
info!("begin: try building envelope from notmuch parsed mail");
|
||||
|
||||
let internal_id = raw_envelope.id().to_string();
|
||||
let id = format!("{:x}", md5::compute(&internal_id));
|
||||
let subject = raw_envelope
|
||||
.header("subject")
|
||||
.map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("subject")))?
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let sender = raw_envelope
|
||||
.header("from")
|
||||
.map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("from")))?
|
||||
.ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("from")))?
|
||||
.to_string();
|
||||
let sender = from_slice_to_addrs(&sender)
|
||||
.map_err(|err| NotmuchError::ParseSendersError(err, sender.to_owned()))?
|
||||
.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(|| NotmuchError::FindSenderError)?;
|
||||
let date = raw_envelope
|
||||
.header("date")
|
||||
.map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("date")))?
|
||||
.ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("date")))?
|
||||
.to_string();
|
||||
let date = DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0)
|
||||
.map_err(|err| NotmuchError::ParseMsgDateError(err, date.to_owned()))
|
||||
.map(|date| date.naive_local().to_string())
|
||||
.ok();
|
||||
|
||||
let envelope = Envelope {
|
||||
id,
|
||||
internal_id,
|
||||
flags: raw_envelope
|
||||
.tags()
|
||||
.map(|tag| Flag::Custom(tag.to_string()))
|
||||
.collect(),
|
||||
subject,
|
||||
sender,
|
||||
date,
|
||||
};
|
||||
trace!("envelope: {:?}", envelope);
|
||||
|
||||
info!("end: try building envelope from notmuch parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
16
lib/src/backend/notmuch/notmuch_envelopes.rs
Normal file
16
lib/src/backend/notmuch/notmuch_envelopes.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use crate::{backend::backend::Result, msg::Envelopes};
|
||||
|
||||
use super::notmuch_envelope;
|
||||
|
||||
/// Represents a list of raw envelopees returned by the `notmuch`
|
||||
/// crate.
|
||||
pub type RawNotmuchEnvelopes = notmuch::Messages;
|
||||
|
||||
pub fn from_notmuch_msgs(msgs: RawNotmuchEnvelopes) -> Result<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for msg in msgs {
|
||||
let envelope = notmuch_envelope::from_notmuch_msg(msg)?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = 2 + 2;
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
mod process;
|
||||
|
||||
pub mod account;
|
||||
pub mod backend;
|
||||
pub mod mbox;
|
||||
pub mod msg;
|
||||
|
|
23
lib/src/mbox/mbox.rs
Normal file
23
lib/src/mbox/mbox.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
//! Mailbox module.
|
||||
//!
|
||||
//! This module contains the representation of the mailbox.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
/// Represents the mailbox.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Mbox {
|
||||
/// Represents the mailbox hierarchie delimiter.
|
||||
pub delim: String,
|
||||
/// Represents the mailbox name.
|
||||
pub name: String,
|
||||
/// Represents the mailbox description.
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Mbox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
29
lib/src/mbox/mboxes.rs
Normal file
29
lib/src/mbox/mboxes.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
//! Mailboxes module.
|
||||
//!
|
||||
//! This module contains the representation of the mailboxes.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use super::Mbox;
|
||||
|
||||
/// Represents the list of mailboxes.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Mboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<Mbox>,
|
||||
}
|
||||
|
||||
impl ops::Deref for Mboxes {
|
||||
type Target = Vec<Mbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Mboxes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.mboxes
|
||||
}
|
||||
}
|
9
lib/src/mbox/mod.rs
Normal file
9
lib/src/mbox/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
//! Mailbox module.
|
||||
//!
|
||||
//! This module contains everything related to mailboxes.
|
||||
|
||||
mod mbox;
|
||||
pub use mbox::*;
|
||||
|
||||
mod mboxes;
|
||||
pub use mboxes::*;
|
|
@ -2,9 +2,10 @@
|
|||
//!
|
||||
//! This module regroups email address entities and converters.
|
||||
|
||||
use anyhow::Result;
|
||||
use mailparse;
|
||||
use std::fmt::Debug;
|
||||
use std::{fmt, result};
|
||||
|
||||
use crate::msg::Result;
|
||||
|
||||
/// Defines a single email address.
|
||||
pub type Addr = mailparse::MailAddr;
|
||||
|
@ -13,7 +14,9 @@ pub type Addr = mailparse::MailAddr;
|
|||
pub type Addrs = mailparse::MailAddrList;
|
||||
|
||||
/// Converts a slice into an optional list of addresses.
|
||||
pub fn from_slice_to_addrs<S: AsRef<str> + Debug>(addrs: S) -> Result<Option<Addrs>> {
|
||||
pub fn from_slice_to_addrs<S: AsRef<str> + fmt::Debug>(
|
||||
addrs: S,
|
||||
) -> result::Result<Option<Addrs>, mailparse::MailParseError> {
|
||||
let addrs = mailparse::addrparse(addrs.as_ref())?;
|
||||
Ok(if addrs.is_empty() { None } else { Some(addrs) })
|
||||
}
|
21
lib/src/msg/envelope.rs
Normal file
21
lib/src/msg/envelope.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::Flags;
|
||||
|
||||
/// Represents the message envelope. The envelope is just a message
|
||||
/// subset, and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, Serialize)]
|
||||
pub struct Envelope {
|
||||
/// Represents the message identifier.
|
||||
pub id: String,
|
||||
/// Represents the internal message identifier.
|
||||
pub internal_id: String,
|
||||
/// Represents the message flags.
|
||||
pub flags: Flags,
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
/// Represents the internal date of the message.
|
||||
pub date: Option<String>,
|
||||
}
|
25
lib/src/msg/envelopes.rs
Normal file
25
lib/src/msg/envelopes.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use super::Envelope;
|
||||
|
||||
/// Represents the list of envelopes.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Envelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<Envelope>,
|
||||
}
|
||||
|
||||
impl ops::Deref for Envelopes {
|
||||
type Target = Vec<Envelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Envelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
56
lib/src/msg/error.rs
Normal file
56
lib/src/msg/error.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use std::{
|
||||
env, io,
|
||||
path::{self, PathBuf},
|
||||
result,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::account;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("cannot expand attachment path {1}")]
|
||||
ExpandAttachmentPathError(#[source] shellexpand::LookupError<env::VarError>, String),
|
||||
#[error("cannot read attachment at {1}")]
|
||||
ReadAttachmentError(#[source] io::Error, PathBuf),
|
||||
#[error("cannot parse template")]
|
||||
ParseTplError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot parse content type of attachment {1}")]
|
||||
ParseAttachmentContentTypeError(#[source] lettre::message::header::ContentTypeErr, String),
|
||||
#[error("cannot write temporary multipart on the disk")]
|
||||
WriteTmpMultipartError(#[source] io::Error),
|
||||
#[error("cannot write temporary multipart on the disk")]
|
||||
BuildSendableMsgError(#[source] lettre::error::Error),
|
||||
#[error("cannot parse {1} value: {2}")]
|
||||
ParseHeaderError(#[source] mailparse::MailParseError, String, String),
|
||||
#[error("cannot build envelope")]
|
||||
BuildEnvelopeError(#[source] lettre::error::Error),
|
||||
#[error("cannot get file name of attachment {0}")]
|
||||
GetAttachmentFilenameError(PathBuf),
|
||||
#[error("cannot parse recipient")]
|
||||
ParseRecipientError,
|
||||
|
||||
#[error("cannot parse message or address")]
|
||||
ParseAddressError(#[from] lettre::address::AddressError),
|
||||
|
||||
#[error(transparent)]
|
||||
AccountError(#[from] account::AccountError),
|
||||
|
||||
#[error("cannot get content type of multipart")]
|
||||
GetMultipartContentTypeError,
|
||||
#[error("cannot find encrypted part of multipart")]
|
||||
GetEncryptedPartMultipartError,
|
||||
#[error("cannot parse encrypted part of multipart")]
|
||||
ParseEncryptedPartError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot get body from encrypted part")]
|
||||
GetEncryptedPartBodyError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot write encrypted part to temporary file")]
|
||||
WriteEncryptedPartBodyError(#[source] io::Error),
|
||||
#[error("cannot write encrypted part to temporary file")]
|
||||
DecryptPartError(#[source] account::AccountError),
|
||||
|
||||
#[error("cannot delete local draft: {1}")]
|
||||
DeleteLocalDraftError(#[source] io::Error, path::PathBuf),
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
27
lib/src/msg/flag.rs
Normal file
27
lib/src/msg/flag.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use serde::Serialize;
|
||||
|
||||
/// Represents the flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum Flag {
|
||||
Seen,
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Draft,
|
||||
Recent,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&str> for Flag {
|
||||
fn from(flag_str: &str) -> Self {
|
||||
match flag_str {
|
||||
"seen" => Flag::Seen,
|
||||
"answered" | "replied" => Flag::Answered,
|
||||
"flagged" => Flag::Flagged,
|
||||
"deleted" | "trashed" => Flag::Deleted,
|
||||
"draft" => Flag::Draft,
|
||||
"recent" => Flag::Recent,
|
||||
flag => Flag::Custom(flag.into()),
|
||||
}
|
||||
}
|
||||
}
|
88
lib/src/msg/flags.rs
Normal file
88
lib/src/msg/flags.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use serde::Serialize;
|
||||
use std::{fmt, ops};
|
||||
|
||||
use super::Flag;
|
||||
|
||||
/// Represents the list of flags.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Flags(pub Vec<Flag>);
|
||||
|
||||
impl Flags {
|
||||
/// Builds a symbols string.
|
||||
pub fn to_symbols_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if self.contains(&Flag::Seen) {
|
||||
" "
|
||||
} else {
|
||||
"✷"
|
||||
});
|
||||
flags.push_str(if self.contains(&Flag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&Flag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Flags {
|
||||
type Target = Vec<Flag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Flags {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Flags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut glue = "";
|
||||
|
||||
for flag in &self.0 {
|
||||
write!(f, "{}", glue)?;
|
||||
match flag {
|
||||
Flag::Seen => write!(f, "\\Seen")?,
|
||||
Flag::Answered => write!(f, "\\Answered")?,
|
||||
Flag::Flagged => write!(f, "\\Flagged")?,
|
||||
Flag::Deleted => write!(f, "\\Deleted")?,
|
||||
Flag::Draft => write!(f, "\\Draft")?,
|
||||
Flag::Recent => write!(f, "\\Recent")?,
|
||||
Flag::Custom(flag) => write!(f, "{}", flag)?,
|
||||
}
|
||||
glue = " ";
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Flags {
|
||||
fn from(flags: &str) -> Self {
|
||||
Flags(
|
||||
flags
|
||||
.split_whitespace()
|
||||
.map(|flag| flag.trim().into())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Flag> for Flags {
|
||||
fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
|
||||
let mut flags = Flags::default();
|
||||
for flag in iter {
|
||||
flags.push(flag);
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
29
lib/src/msg/mod.rs
Normal file
29
lib/src/msg/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
mod flag;
|
||||
pub use flag::*;
|
||||
|
||||
mod flags;
|
||||
pub use flags::*;
|
||||
|
||||
mod envelope;
|
||||
pub use envelope::*;
|
||||
|
||||
mod envelopes;
|
||||
pub use envelopes::*;
|
||||
|
||||
mod parts;
|
||||
pub use parts::*;
|
||||
|
||||
mod addr;
|
||||
pub use addr::*;
|
||||
|
||||
mod tpl;
|
||||
pub use tpl::*;
|
||||
|
||||
mod msg;
|
||||
pub use msg::*;
|
||||
|
||||
mod msg_utils;
|
||||
pub use msg_utils::*;
|
|
@ -1,5 +1,4 @@
|
|||
use ammonia;
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
use convert_case::{Case, Casing};
|
||||
use html_escape;
|
||||
|
@ -14,20 +13,14 @@ use std::{
|
|||
fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
use tree_magic;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM},
|
||||
account::{Account, DEFAULT_SIG_DELIM},
|
||||
msg::{
|
||||
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils,
|
||||
Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,
|
||||
},
|
||||
output::PrinterService,
|
||||
smtp::SmtpService,
|
||||
ui::{
|
||||
choice::{self, PostEditChoice, PreEditChoice},
|
||||
editor,
|
||||
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr,
|
||||
Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -173,7 +166,7 @@ impl Msg {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result<Self> {
|
||||
pub fn into_reply(mut self, all: bool, account: &Account) -> Result<Self> {
|
||||
let account_addr = account.address()?;
|
||||
|
||||
// In-Reply-To
|
||||
|
@ -271,7 +264,7 @@ impl Msg {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn into_forward(mut self, account: &AccountConfig) -> Result<Self> {
|
||||
pub fn into_forward(mut self, account: &Account) -> Result<Self> {
|
||||
let account_addr = account.address()?;
|
||||
|
||||
let prev_subject = self.subject.to_owned();
|
||||
|
@ -327,99 +320,6 @@ impl Msg {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
fn _edit_with_editor(&self, account: &AccountConfig) -> Result<Self> {
|
||||
let tpl = self.to_tpl(TplOverride::default(), account)?;
|
||||
let tpl = editor::open_with_tpl(tpl)?;
|
||||
Self::from_tpl(&tpl)
|
||||
}
|
||||
|
||||
pub fn edit_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
mut self,
|
||||
account: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<Box<&'a mut B>> {
|
||||
info!("start editing with editor");
|
||||
|
||||
let draft = msg_utils::local_draft_path();
|
||||
if draft.exists() {
|
||||
loop {
|
||||
match choice::pre_edit() {
|
||||
Ok(choice) => match choice {
|
||||
PreEditChoice::Edit => {
|
||||
let tpl = editor::open_with_draft()?;
|
||||
self.merge_with(Msg::from_tpl(&tpl)?);
|
||||
break;
|
||||
}
|
||||
PreEditChoice::Discard => {
|
||||
self.merge_with(self._edit_with_editor(account)?);
|
||||
break;
|
||||
}
|
||||
PreEditChoice::Quit => return Ok(backend),
|
||||
},
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.merge_with(self._edit_with_editor(account)?);
|
||||
}
|
||||
|
||||
loop {
|
||||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
printer.print_str("Sending message…")?;
|
||||
let sent_msg = smtp.send(account, &self)?;
|
||||
let sent_folder = account
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
printer
|
||||
.print_str(format!("Adding message to the {:?} folder…", sent_folder))?;
|
||||
backend.add_msg(&sent_folder, &sent_msg, "seen")?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
printer.print_struct("Done!")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Edit) => {
|
||||
self.merge_with(self._edit_with_editor(account)?);
|
||||
continue;
|
||||
}
|
||||
Ok(PostEditChoice::LocalDraft) => {
|
||||
printer.print_struct("Message successfully saved locally")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::RemoteDraft) => {
|
||||
let tpl = self.to_tpl(TplOverride::default(), account)?;
|
||||
let draft_folder = account
|
||||
.mailboxes
|
||||
.get("draft")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_DRAFT_FOLDER);
|
||||
backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
printer
|
||||
.print_struct(format!("Message successfully saved to {}", draft_folder))?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Discard) => {
|
||||
msg_utils::remove_local_draft()?;
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
pub fn encrypt(mut self, encrypt: bool) -> Self {
|
||||
self.encrypt = encrypt;
|
||||
self
|
||||
|
@ -428,14 +328,15 @@ impl Msg {
|
|||
pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result<Self> {
|
||||
for path in attachments_paths {
|
||||
let path = shellexpand::full(path)
|
||||
.context(format!(r#"cannot expand attachment path "{}""#, path))?;
|
||||
.map_err(|err| Error::ExpandAttachmentPathError(err, path.to_owned()))?;
|
||||
let path = PathBuf::from(path.to_string());
|
||||
let filename: String = path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("cannot get file name of attachment {:?}", path))?
|
||||
.ok_or_else(|| Error::GetAttachmentFilenameError(path.to_owned()))?
|
||||
.to_string_lossy()
|
||||
.into();
|
||||
let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?;
|
||||
let content =
|
||||
fs::read(&path).map_err(|err| Error::ReadAttachmentError(err, path.to_owned()))?;
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
|
||||
self.parts.push(Part::Binary(BinaryPart {
|
||||
|
@ -479,7 +380,7 @@ impl Msg {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_tpl(&self, opts: TplOverride, account: &AccountConfig) -> Result<String> {
|
||||
pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> Result<String> {
|
||||
let account_addr: Addrs = vec![account.address()?].into();
|
||||
let mut tpl = String::default();
|
||||
|
||||
|
@ -559,13 +460,13 @@ impl Msg {
|
|||
info!("begin: building message from template");
|
||||
trace!("template: {:?}", tpl);
|
||||
|
||||
let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?;
|
||||
let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).map_err(Error::ParseTplError)?;
|
||||
|
||||
info!("end: building message from template");
|
||||
Self::from_parsed_mail(parsed_mail, &AccountConfig::default())
|
||||
Self::from_parsed_mail(parsed_mail, &Account::default())
|
||||
}
|
||||
|
||||
pub fn into_sendable_msg(&self, account: &AccountConfig) -> Result<lettre::Message> {
|
||||
pub fn into_sendable_msg(&self, account: &Account) -> Result<lettre::Message> {
|
||||
let mut msg_builder = lettre::Message::builder()
|
||||
.message_id(self.message_id.to_owned())
|
||||
.subject(self.subject.to_owned());
|
||||
|
@ -610,10 +511,9 @@ impl Msg {
|
|||
for part in self.attachments() {
|
||||
multipart = multipart.singlepart(Attachment::new(part.filename.clone()).body(
|
||||
part.content,
|
||||
part.mime.parse().context(format!(
|
||||
"cannot parse content type of attachment {}",
|
||||
part.filename
|
||||
))?,
|
||||
part.mime.parse().map_err(|err| {
|
||||
Error::ParseAttachmentContentTypeError(err, part.filename)
|
||||
})?,
|
||||
))
|
||||
}
|
||||
multipart
|
||||
|
@ -621,16 +521,15 @@ impl Msg {
|
|||
|
||||
if self.encrypt {
|
||||
let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string());
|
||||
fs::write(multipart_buffer.clone(), multipart.formatted())?;
|
||||
fs::write(multipart_buffer.clone(), multipart.formatted())
|
||||
.map_err(Error::WriteTmpMultipartError)?;
|
||||
let addr = self
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.clone().extract_single_info())
|
||||
.map(|addr| addr.addr)
|
||||
.ok_or_else(|| anyhow!("cannot find recipient"))?;
|
||||
let encrypted_multipart = account
|
||||
.pgp_encrypt_file(&addr, multipart_buffer.clone())?
|
||||
.ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?;
|
||||
.ok_or_else(|| Error::ParseRecipientError)?;
|
||||
let encrypted_multipart = account.pgp_encrypt_file(&addr, multipart_buffer.clone())?;
|
||||
trace!("encrypted multipart: {:#?}", encrypted_multipart);
|
||||
multipart = MultiPart::encrypted(String::from("application/pgp-encrypted"))
|
||||
.singlepart(
|
||||
|
@ -647,12 +546,12 @@ impl Msg {
|
|||
|
||||
msg_builder
|
||||
.multipart(multipart)
|
||||
.context("cannot build sendable message")
|
||||
.map_err(Error::BuildSendableMsgError)
|
||||
}
|
||||
|
||||
pub fn from_parsed_mail(
|
||||
parsed_mail: mailparse::ParsedMail<'_>,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
) -> Result<Self> {
|
||||
trace!(">> build message from parsed mail");
|
||||
trace!("parsed mail: {:?}", parsed_mail);
|
||||
|
@ -683,24 +582,24 @@ impl Msg {
|
|||
}
|
||||
},
|
||||
"from" => {
|
||||
msg.from = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
msg.from = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"to" => {
|
||||
msg.to = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
msg.to = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"reply-to" => {
|
||||
msg.reply_to = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
msg.reply_to = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"cc" => {
|
||||
msg.cc = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
msg.cc = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"bcc" => {
|
||||
msg.bcc = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
msg.bcc = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
key => {
|
||||
msg.headers.insert(key.to_lowercase(), val);
|
||||
|
@ -709,8 +608,7 @@ impl Msg {
|
|||
trace!("<< parse header");
|
||||
}
|
||||
|
||||
msg.parts = Parts::from_parsed_mail(config, &parsed_mail)
|
||||
.context("cannot parsed message mime parts")?;
|
||||
msg.parts = Parts::from_parsed_mail(config, &parsed_mail)?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< build message from parsed mail");
|
||||
|
@ -725,7 +623,7 @@ impl Msg {
|
|||
&self,
|
||||
text_mime: &str,
|
||||
headers: Vec<&str>,
|
||||
config: &AccountConfig,
|
||||
config: &Account,
|
||||
) -> Result<String> {
|
||||
let mut all_headers = vec![];
|
||||
for h in config.read_headers.iter() {
|
||||
|
@ -837,7 +735,7 @@ impl TryInto<lettre::address::Envelope> for &Msg {
|
|||
.as_ref()
|
||||
.map(from_addrs_to_sendable_addrs)
|
||||
.unwrap_or(Ok(vec![]))?;
|
||||
Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?)
|
||||
Ok(lettre::address::Envelope::new(from, to).map_err(Error::BuildEnvelopeError)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -852,10 +750,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_into_reply() {
|
||||
let config = AccountConfig {
|
||||
let config = Account {
|
||||
display_name: "Test".into(),
|
||||
email: "test-account@local".into(),
|
||||
..AccountConfig::default()
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
// Checks that:
|
||||
|
@ -991,7 +889,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_to_readable() {
|
||||
let config = AccountConfig::default();
|
||||
let config = Account::default();
|
||||
let msg = Msg {
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: String::from("hello, world!"),
|
||||
|
@ -1054,14 +952,14 @@ mod tests {
|
|||
.unwrap()
|
||||
);
|
||||
|
||||
let config = AccountConfig {
|
||||
let config = Account {
|
||||
read_headers: vec![
|
||||
"CusTOM-heaDER".into(),
|
||||
"Subject".into(),
|
||||
"from".into(),
|
||||
"cc".into(),
|
||||
],
|
||||
..AccountConfig::default()
|
||||
..Account::default()
|
||||
};
|
||||
// header present but empty in msg headers, empty config
|
||||
assert_eq!(
|
24
lib/src/msg/msg_utils.rs
Normal file
24
lib/src/msg/msg_utils.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use log::{debug, trace};
|
||||
use std::{env, fs, path};
|
||||
|
||||
use crate::msg::{Error, Result};
|
||||
|
||||
pub fn local_draft_path() -> path::PathBuf {
|
||||
trace!(">> get local draft path");
|
||||
|
||||
let path = env::temp_dir().join("himalaya-draft.eml");
|
||||
debug!("local draft path: {:?}", path);
|
||||
|
||||
trace!("<< get local draft path");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn remove_local_draft() -> Result<()> {
|
||||
trace!(">> remove local draft");
|
||||
|
||||
let path = local_draft_path();
|
||||
fs::remove_file(&path).map_err(|err| Error::DeleteLocalDraftError(err, path))?;
|
||||
|
||||
trace!("<< remove local draft");
|
||||
Ok(())
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use mailparse::MailHeaderMap;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
|
@ -7,7 +6,7 @@ use std::{
|
|||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::AccountConfig;
|
||||
use crate::{account::Account, msg};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextPlainPart {
|
||||
|
@ -51,11 +50,17 @@ impl Parts {
|
|||
}
|
||||
|
||||
pub fn from_parsed_mail<'a>(
|
||||
account: &'a AccountConfig,
|
||||
account: &'a Account,
|
||||
part: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Result<Self> {
|
||||
) -> msg::Result<Self> {
|
||||
let mut parts = vec![];
|
||||
build_parts_map_rec(account, part, &mut parts)?;
|
||||
if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none()
|
||||
{
|
||||
let content = part.get_body().unwrap_or_default();
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else {
|
||||
build_parts_map_rec(account, part, &mut parts)?;
|
||||
}
|
||||
Ok(Self(parts))
|
||||
}
|
||||
}
|
||||
|
@ -75,10 +80,10 @@ impl DerefMut for Parts {
|
|||
}
|
||||
|
||||
fn build_parts_map_rec(
|
||||
account: &AccountConfig,
|
||||
account: &Account,
|
||||
parsed_mail: &mailparse::ParsedMail,
|
||||
parts: &mut Vec<Part>,
|
||||
) -> Result<()> {
|
||||
) -> msg::Result<()> {
|
||||
if parsed_mail.subparts.is_empty() {
|
||||
let cdisp = parsed_mail.get_content_disposition();
|
||||
match cdisp.disposition {
|
||||
|
@ -105,23 +110,22 @@ fn build_parts_map_rec(
|
|||
} else if ctype.starts_with("text/html") {
|
||||
parts.push(Part::TextHtml(TextHtmlPart { content }))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let ctype = parsed_mail
|
||||
.get_headers()
|
||||
.get_first_value("content-type")
|
||||
.ok_or_else(|| anyhow!("cannot get content type of multipart"))?;
|
||||
.ok_or_else(|| msg::Error::GetMultipartContentTypeError)?;
|
||||
if ctype.starts_with("multipart/encrypted") {
|
||||
let decrypted_part = parsed_mail
|
||||
.subparts
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("cannot find encrypted part of multipart"))
|
||||
.and_then(|part| decrypt_part(account, part))
|
||||
.context("cannot decrypt part of multipart")?;
|
||||
.ok_or_else(|| msg::Error::GetEncryptedPartMultipartError)
|
||||
.and_then(|part| decrypt_part(account, part))?;
|
||||
let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes())
|
||||
.context("cannot parse decrypted part of multipart")?;
|
||||
.map_err(msg::Error::ParseEncryptedPartError)?;
|
||||
build_parts_map_rec(account, &parsed_mail, parts)?;
|
||||
} else {
|
||||
for part in parsed_mail.subparts.iter() {
|
||||
|
@ -133,14 +137,14 @@ fn build_parts_map_rec(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result<String> {
|
||||
fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> msg::Result<String> {
|
||||
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
|
||||
let msg_body = msg
|
||||
.get_body()
|
||||
.context("cannot get body from encrypted part")?;
|
||||
fs::write(msg_path.clone(), &msg_body)
|
||||
.context(format!("cannot write encrypted part to temporary file"))?;
|
||||
account
|
||||
.pgp_decrypt_file(msg_path.clone())?
|
||||
.ok_or_else(|| anyhow!("cannot find pgp decrypt command in config"))
|
||||
.map_err(msg::Error::GetEncryptedPartBodyError)?;
|
||||
fs::write(msg_path.clone(), &msg_body).map_err(msg::Error::WriteEncryptedPartBodyError)?;
|
||||
let content = account
|
||||
.pgp_decrypt_file(msg_path.clone())
|
||||
.map_err(msg::Error::DecryptPartError)?;
|
||||
Ok(content)
|
||||
}
|
15
lib/src/msg/tpl.rs
Normal file
15
lib/src/msg/tpl.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
//! Module related to message template CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to message template.
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
||||
pub struct TplOverride<'a> {
|
||||
pub subject: Option<&'a str>,
|
||||
pub from: Option<Vec<&'a str>>,
|
||||
pub to: Option<Vec<&'a str>>,
|
||||
pub cc: Option<Vec<&'a str>>,
|
||||
pub bcc: Option<Vec<&'a str>>,
|
||||
pub headers: Option<Vec<&'a str>>,
|
||||
pub body: Option<&'a str>,
|
||||
pub sig: Option<&'a str>,
|
||||
}
|
34
lib/src/process.rs
Normal file
34
lib/src/process.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
//! Process module.
|
||||
//!
|
||||
//! This module contains cross platform helpers around the
|
||||
//! `std::process` crate.
|
||||
|
||||
use log::{debug, trace};
|
||||
use std::{io, process::Command, string};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProcessError {
|
||||
#[error("cannot run command {1:?}")]
|
||||
RunCmdError(#[source] io::Error, String),
|
||||
|
||||
#[error("cannot parse command output")]
|
||||
ParseCmdOutputError(#[source] string::FromUtf8Error),
|
||||
}
|
||||
|
||||
pub fn run(cmd: &str) -> Result<String, ProcessError> {
|
||||
debug!(">> run command");
|
||||
debug!("command: {}", cmd);
|
||||
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd").args(&["/C", cmd]).output()
|
||||
} else {
|
||||
Command::new("sh").arg("-c").arg(cmd).output()
|
||||
};
|
||||
let output = output.map_err(|err| ProcessError::RunCmdError(err, cmd.to_string()))?;
|
||||
let output = String::from_utf8(output.stdout).map_err(ProcessError::ParseCmdOutputError)?;
|
||||
|
||||
trace!("command output: {}", output);
|
||||
debug!("<< run command");
|
||||
Ok(output)
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya::{
|
||||
backends::{Backend, ImapBackend, ImapEnvelopes},
|
||||
config::{AccountConfig, ImapBackendConfig},
|
||||
use himalaya_lib::{
|
||||
account::{Account, ImapBackendConfig},
|
||||
backend::{Backend, ImapBackend},
|
||||
};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[test]
|
||||
fn test_imap_backend() {
|
||||
// configure accounts
|
||||
let account_config = AccountConfig {
|
||||
let account_config = Account {
|
||||
smtp_host: "localhost".into(),
|
||||
smtp_port: 3465,
|
||||
smtp_starttls: false,
|
||||
smtp_insecure: true,
|
||||
smtp_login: "inbox@localhost".into(),
|
||||
smtp_passwd_cmd: "echo 'password'".into(),
|
||||
..AccountConfig::default()
|
||||
..Account::default()
|
||||
};
|
||||
let imap_config = ImapBackendConfig {
|
||||
imap_host: "localhost".into(),
|
||||
|
@ -46,7 +46,6 @@ fn test_imap_backend() {
|
|||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap();
|
||||
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert_eq!("alice@localhost", envelope.sender);
|
||||
|
@ -56,20 +55,16 @@ fn test_imap_backend() {
|
|||
imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
|
||||
.unwrap();
|
||||
let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap();
|
||||
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap();
|
||||
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
|
||||
// check that the message can be moved
|
||||
imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
|
||||
.unwrap();
|
||||
let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap();
|
||||
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
assert_eq!(0, envelopes.len());
|
||||
let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap();
|
||||
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
assert_eq!(2, envelopes.len());
|
||||
let id = envelopes.first().unwrap().id.to_string();
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
use maildir::Maildir;
|
||||
use std::{collections::HashMap, env, fs, iter::FromIterator};
|
||||
|
||||
use himalaya::{
|
||||
backends::{Backend, MaildirBackend, MaildirEnvelopes, MaildirFlag},
|
||||
config::{AccountConfig, MaildirBackendConfig},
|
||||
use himalaya_lib::{
|
||||
account::{Account, MaildirBackendConfig},
|
||||
backend::{Backend, MaildirBackend},
|
||||
msg::Flag,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
@ -18,9 +19,9 @@ fn test_maildir_backend() {
|
|||
mdir_sub.create_dirs().unwrap();
|
||||
|
||||
// configure accounts
|
||||
let account_config = AccountConfig {
|
||||
let account_config = Account {
|
||||
mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]),
|
||||
..AccountConfig::default()
|
||||
..Account::default()
|
||||
};
|
||||
let mdir_config = MaildirBackendConfig {
|
||||
maildir_dir: mdir.path().to_owned(),
|
||||
|
@ -33,7 +34,7 @@ fn test_maildir_backend() {
|
|||
|
||||
// check that a message can be added
|
||||
let msg = include_bytes!("./emails/alice-to-patrick.eml");
|
||||
let hash = mdir.add_msg("inbox", msg, "seen").unwrap().to_string();
|
||||
let hash = mdir.add_msg("inbox", msg, "seen").unwrap();
|
||||
|
||||
// check that the added message exists
|
||||
let msg = mdir.get_msg("inbox", &hash).unwrap();
|
||||
|
@ -43,48 +44,42 @@ fn test_maildir_backend() {
|
|||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = mdir.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
assert_eq!("alice@localhost", envelope.sender);
|
||||
assert_eq!("Plain message", envelope.subject);
|
||||
|
||||
// check that a flag can be added to the message
|
||||
mdir.add_flags("inbox", &envelope.hash, "flagged passed")
|
||||
.unwrap();
|
||||
mdir.add_flags("inbox", &envelope.id, "flagged").unwrap();
|
||||
let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap();
|
||||
let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&MaildirFlag::Seen));
|
||||
assert!(envelope.flags.contains(&MaildirFlag::Flagged));
|
||||
assert!(envelope.flags.contains(&MaildirFlag::Passed));
|
||||
assert!(envelope.flags.contains(&Flag::Seen));
|
||||
assert!(envelope.flags.contains(&Flag::Flagged));
|
||||
|
||||
// check that the message flags can be changed
|
||||
mdir.set_flags("inbox", &envelope.hash, "passed").unwrap();
|
||||
mdir.set_flags("inbox", &envelope.id, "answered").unwrap();
|
||||
let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap();
|
||||
let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(!envelope.flags.contains(&MaildirFlag::Seen));
|
||||
assert!(!envelope.flags.contains(&MaildirFlag::Flagged));
|
||||
assert!(envelope.flags.contains(&MaildirFlag::Passed));
|
||||
assert!(!envelope.flags.contains(&Flag::Seen));
|
||||
assert!(!envelope.flags.contains(&Flag::Flagged));
|
||||
assert!(envelope.flags.contains(&Flag::Answered));
|
||||
|
||||
// check that a flag can be removed from the message
|
||||
mdir.del_flags("inbox", &envelope.hash, "passed").unwrap();
|
||||
mdir.del_flags("inbox", &envelope.id, "answered").unwrap();
|
||||
let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap();
|
||||
let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(!envelope.flags.contains(&MaildirFlag::Seen));
|
||||
assert!(!envelope.flags.contains(&MaildirFlag::Flagged));
|
||||
assert!(!envelope.flags.contains(&MaildirFlag::Passed));
|
||||
assert!(!envelope.flags.contains(&Flag::Seen));
|
||||
assert!(!envelope.flags.contains(&Flag::Flagged));
|
||||
assert!(!envelope.flags.contains(&Flag::Answered));
|
||||
|
||||
// check that the message can be copied
|
||||
mdir.copy_msg("inbox", "subdir", &envelope.hash).unwrap();
|
||||
mdir.copy_msg("inbox", "subdir", &envelope.id).unwrap();
|
||||
assert!(mdir.get_msg("inbox", &hash).is_ok());
|
||||
assert!(mdir.get_msg("subdir", &hash).is_ok());
|
||||
assert!(mdir_subdir.get_msg("inbox", &hash).is_ok());
|
||||
|
||||
// check that the message can be moved
|
||||
mdir.move_msg("inbox", "subdir", &envelope.hash).unwrap();
|
||||
mdir.move_msg("inbox", "subdir", &envelope.id).unwrap();
|
||||
assert!(mdir.get_msg("inbox", &hash).is_err());
|
||||
assert!(mdir.get_msg("subdir", &hash).is_ok());
|
||||
assert!(mdir_subdir.get_msg("inbox", &hash).is_ok());
|
|
@ -2,14 +2,16 @@
|
|||
use std::{collections::HashMap, env, fs, iter::FromIterator};
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya::{
|
||||
backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes},
|
||||
config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig},
|
||||
use himalaya_lib::{
|
||||
account::{Account, MaildirBackendConfig, NotmuchBackendConfig},
|
||||
backend::{Backend, MaildirBackend, NotmuchBackend},
|
||||
};
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[test]
|
||||
fn test_notmuch_backend() {
|
||||
use himalaya_lib::msg::Flag;
|
||||
|
||||
// set up maildir folders and notmuch database
|
||||
let mdir: maildir::Maildir = env::temp_dir().join("himalaya-test-notmuch").into();
|
||||
if let Err(_) = fs::remove_dir_all(mdir.path()) {}
|
||||
|
@ -42,7 +44,6 @@ fn test_notmuch_backend() {
|
|||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
assert_eq!("alice@localhost", envelope.sender);
|
||||
|
@ -50,37 +51,34 @@ fn test_notmuch_backend() {
|
|||
|
||||
// check that a flag can be added to the message
|
||||
notmuch
|
||||
.add_flags("", &envelope.hash, "flagged passed")
|
||||
.add_flags("", &envelope.id, "flagged answered")
|
||||
.unwrap();
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&"inbox".into()));
|
||||
assert!(envelope.flags.contains(&"seen".into()));
|
||||
assert!(envelope.flags.contains(&"flagged".into()));
|
||||
assert!(envelope.flags.contains(&"passed".into()));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("inbox".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("seen".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("flagged".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("answered".into())));
|
||||
|
||||
// check that the message flags can be changed
|
||||
notmuch
|
||||
.set_flags("", &envelope.hash, "inbox passed")
|
||||
.set_flags("", &envelope.id, "inbox answered")
|
||||
.unwrap();
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&"inbox".into()));
|
||||
assert!(!envelope.flags.contains(&"seen".into()));
|
||||
assert!(!envelope.flags.contains(&"flagged".into()));
|
||||
assert!(envelope.flags.contains(&"passed".into()));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("inbox".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("seen".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("flagged".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("answered".into())));
|
||||
|
||||
// check that a flag can be removed from the message
|
||||
notmuch.del_flags("", &envelope.hash, "passed").unwrap();
|
||||
notmuch.del_flags("", &envelope.id, "answered").unwrap();
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&"inbox".into()));
|
||||
assert!(!envelope.flags.contains(&"seen".into()));
|
||||
assert!(!envelope.flags.contains(&"flagged".into()));
|
||||
assert!(!envelope.flags.contains(&"passed".into()));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("inbox".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("seen".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("flagged".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("answered".into())));
|
||||
|
||||
// check that the message can be deleted
|
||||
notmuch.del_msg("", &hash).unwrap();
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
|
@ -3,7 +3,7 @@ let s:trim = function("himalaya#shared#utils#trim")
|
|||
let s:cli = function("himalaya#shared#cli#call")
|
||||
let s:plain_req = function("himalaya#request#plain")
|
||||
|
||||
let s:msg_id = 0
|
||||
let s:msg_id = ""
|
||||
let s:draft = ""
|
||||
let s:attachment_paths = []
|
||||
|
||||
|
@ -47,16 +47,17 @@ function! himalaya#msg#read()
|
|||
try
|
||||
let pos = getpos(".")
|
||||
let s:msg_id = s:get_focused_msg_id()
|
||||
if empty(s:msg_id) || s:msg_id == "HASH" | return | endif
|
||||
let account = himalaya#account#curr()
|
||||
let mbox = himalaya#mbox#curr_mbox()
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s read %d",
|
||||
\"--account %s --mailbox %s read %s",
|
||||
\[shellescape(account), shellescape(mbox), s:msg_id],
|
||||
\printf("Fetching message %d", s:msg_id),
|
||||
\printf("Fetching message %s", s:msg_id),
|
||||
\1,
|
||||
\)
|
||||
call s:close_open_buffers('Himalaya read message')
|
||||
execute printf("silent! botright new Himalaya read message [%d]", s:msg_id)
|
||||
execute printf("silent! botright new Himalaya read message [%s]", s:msg_id)
|
||||
setlocal modifiable
|
||||
silent execute "%d"
|
||||
call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
|
||||
|
@ -98,12 +99,12 @@ function! himalaya#msg#reply()
|
|||
let mbox = himalaya#mbox#curr_mbox()
|
||||
let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s template reply %d",
|
||||
\"--account %s --mailbox %s template reply %s",
|
||||
\[shellescape(account), shellescape(mbox), msg_id],
|
||||
\"Fetching reply template",
|
||||
\0,
|
||||
\)
|
||||
execute printf("silent! edit Himalaya reply [%d]", msg_id)
|
||||
execute printf("silent! edit Himalaya reply [%s]", msg_id)
|
||||
call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
|
||||
silent execute "$d"
|
||||
setlocal filetype=himalaya-msg-write
|
||||
|
@ -124,12 +125,12 @@ function! himalaya#msg#reply_all()
|
|||
let mbox = himalaya#mbox#curr_mbox()
|
||||
let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s template reply %d --all",
|
||||
\"--account %s --mailbox %s template reply %s --all",
|
||||
\[shellescape(account), shellescape(mbox), msg_id],
|
||||
\"Fetching reply all template",
|
||||
\0
|
||||
\)
|
||||
execute printf("silent! edit Himalaya reply all [%d]", msg_id)
|
||||
execute printf("silent! edit Himalaya reply all [%s]", msg_id)
|
||||
call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
|
||||
silent execute "$d"
|
||||
setlocal filetype=himalaya-msg-write
|
||||
|
@ -150,12 +151,12 @@ function! himalaya#msg#forward()
|
|||
let mbox = himalaya#mbox#curr_mbox()
|
||||
let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s template forward %d",
|
||||
\"--account %s --mailbox %s template forward %s",
|
||||
\[shellescape(account), shellescape(mbox), msg_id],
|
||||
\"Fetching forward template",
|
||||
\0
|
||||
\)
|
||||
execute printf("silent! edit Himalaya forward [%d]", msg_id)
|
||||
execute printf("silent! edit Himalaya forward [%s]", msg_id)
|
||||
call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
|
||||
silent execute "$d"
|
||||
setlocal filetype=himalaya-msg-write
|
||||
|
@ -180,7 +181,7 @@ function! himalaya#msg#_copy(target_mbox)
|
|||
let account = himalaya#account#curr()
|
||||
let source_mbox = himalaya#mbox#curr_mbox()
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s copy %d %s",
|
||||
\"--account %s --mailbox %s copy %s %s",
|
||||
\[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)],
|
||||
\"Copying message",
|
||||
\1,
|
||||
|
@ -201,14 +202,14 @@ endfunction
|
|||
function! himalaya#msg#_move(target_mbox)
|
||||
try
|
||||
let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id
|
||||
let choice = input(printf("Are you sure you want to move the message %d? (y/N) ", msg_id))
|
||||
let choice = input(printf("Are you sure you want to move the message %s? (y/N) ", msg_id))
|
||||
redraw | echo
|
||||
if choice != "y" | return | endif
|
||||
let pos = getpos(".")
|
||||
let account = himalaya#account#curr()
|
||||
let source_mbox = himalaya#mbox#curr_mbox()
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s move %d %s",
|
||||
\"--account %s --mailbox %s move %s %s",
|
||||
\[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)],
|
||||
\"Moving message",
|
||||
\1,
|
||||
|
@ -294,7 +295,7 @@ function! himalaya#msg#attachments()
|
|||
let mbox = himalaya#mbox#curr_mbox()
|
||||
let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id
|
||||
let msg = s:cli(
|
||||
\"--account %s --mailbox %s attachments %d",
|
||||
\"--account %s --mailbox %s attachments %s",
|
||||
\[shellescape(account), shellescape(mbox), msg_id],
|
||||
\"Downloading attachments",
|
||||
\0
|
||||
|
@ -375,7 +376,7 @@ function! s:bufwidth()
|
|||
endfunction
|
||||
|
||||
function! s:get_msg_id(line)
|
||||
return matchstr(a:line, '[0-9]*')
|
||||
return matchstr(a:line, '[0-9a-zA-Z]*')
|
||||
endfunction
|
||||
|
||||
function! s:get_focused_msg_id()
|
||||
|
|
Loading…
Reference in a new issue