mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 17:15:12 +00:00
commit
a44e94bd6f
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.9] - 2022-03-12
|
||||
|
||||
### Added
|
||||
|
||||
- SMTP pre-send hook [#178]
|
||||
- Customize headers to show at the top of a read message [#338]
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve `attachments` command [#281]
|
||||
|
||||
### Fixed
|
||||
|
||||
- `In-Reply-To` not set properly when replying to a message [#323]
|
||||
- `Cc` missing or invalid when replying to a message [#324]
|
||||
- Notmuch backend hangs [#329]
|
||||
- Maildir e2e tests [#335]
|
||||
- JSON API for listings [#331]
|
||||
|
||||
## [0.5.8] - 2022-03-04
|
||||
|
||||
### Added
|
||||
|
@ -338,7 +357,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Password from command [#22]
|
||||
- Set up README [#20]
|
||||
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.8...HEAD
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.9...HEAD
|
||||
[0.5.9]: https://github.com/soywod/himalaya/compare/v0.5.8...v0.5.9
|
||||
[0.5.8]: https://github.com/soywod/himalaya/compare/v0.5.7...v0.5.8
|
||||
[0.5.7]: https://github.com/soywod/himalaya/compare/v0.5.6...v0.5.7
|
||||
[0.5.6]: https://github.com/soywod/himalaya/compare/v0.5.5...v0.5.6
|
||||
|
@ -444,6 +464,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#162]: https://github.com/soywod/himalaya/issues/162
|
||||
[#176]: https://github.com/soywod/himalaya/issues/176
|
||||
[#172]: https://github.com/soywod/himalaya/issues/172
|
||||
[#178]: https://github.com/soywod/himalaya/issues/178
|
||||
[#181]: https://github.com/soywod/himalaya/issues/181
|
||||
[#185]: https://github.com/soywod/himalaya/issues/185
|
||||
[#186]: https://github.com/soywod/himalaya/issues/186
|
||||
|
@ -470,6 +491,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#271]: https://github.com/soywod/himalaya/issues/271
|
||||
[#276]: https://github.com/soywod/himalaya/issues/276
|
||||
[#280]: https://github.com/soywod/himalaya/issues/280
|
||||
[#281]: https://github.com/soywod/himalaya/issues/281
|
||||
[#288]: https://github.com/soywod/himalaya/issues/288
|
||||
[#289]: https://github.com/soywod/himalaya/issues/289
|
||||
[#298]: https://github.com/soywod/himalaya/issues/298
|
||||
|
@ -480,3 +502,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#309]: https://github.com/soywod/himalaya/issues/309
|
||||
[#318]: https://github.com/soywod/himalaya/issues/318
|
||||
[#321]: https://github.com/soywod/himalaya/issues/321
|
||||
[#323]: https://github.com/soywod/himalaya/issues/323
|
||||
[#324]: https://github.com/soywod/himalaya/issues/324
|
||||
[#329]: https://github.com/soywod/himalaya/issues/329
|
||||
[#331]: https://github.com/soywod/himalaya/issues/331
|
||||
[#335]: https://github.com/soywod/himalaya/issues/335
|
||||
[#338]: https://github.com/soywod/himalaya/issues/338
|
||||
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -167,6 +167,12 @@ dependencies = [
|
|||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.2"
|
||||
|
@ -436,13 +442,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "himalaya"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
"atty",
|
||||
"chrono",
|
||||
"clap",
|
||||
"convert_case",
|
||||
"env_logger",
|
||||
"erased-serde",
|
||||
"html-escape",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2018"
|
||||
license-file = "LICENSE"
|
||||
|
@ -28,6 +28,7 @@ anyhow = "1.0.44"
|
|||
atty = "0.2.14"
|
||||
chrono = "0.4.19"
|
||||
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
|
||||
convert_case = "0.5.0"
|
||||
env_logger = "0.8.3"
|
||||
erased-serde = "0.3.18"
|
||||
html-escape = "0.2.9"
|
||||
|
|
|
@ -26,7 +26,6 @@ impl IdMapper {
|
|||
.open(&mapper.path)
|
||||
.context("cannot open id hash map file")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
let line =
|
||||
line.context("cannot read line from maildir envelopes id mapper cache file")?;
|
||||
|
@ -83,13 +82,13 @@ impl IdMapper {
|
|||
|
||||
for (hash, id) in self.iter() {
|
||||
loop {
|
||||
let short_hash = &hash[0..self.short_hash_len];
|
||||
let short_hash = &hash[0..short_hash_len];
|
||||
let conflict_found = self
|
||||
.map
|
||||
.keys()
|
||||
.find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash)
|
||||
.is_some();
|
||||
if self.short_hash_len > 32 || !conflict_found {
|
||||
if short_hash_len > 32 || !conflict_found {
|
||||
break;
|
||||
}
|
||||
short_hash_len += 1;
|
||||
|
|
|
@ -15,21 +15,24 @@ use super::{ImapFlag, ImapFlags};
|
|||
|
||||
/// Represents a list of IMAP envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct ImapEnvelopes(pub Vec<ImapEnvelope>);
|
||||
pub struct ImapEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<ImapEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for ImapEnvelopes {
|
||||
type Target = Vec<ImapEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for ImapEnvelopes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +102,7 @@ impl TryFrom<RawImapEnvelopes> for ImapEnvelopes {
|
|||
for raw_envelope in raw_envelopes.iter().rev() {
|
||||
envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?);
|
||||
}
|
||||
Ok(Self(envelopes))
|
||||
Ok(Self { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
//! to the mailbox.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::Deref;
|
||||
|
||||
|
@ -16,22 +17,25 @@ use crate::{
|
|||
use super::ImapMboxAttrs;
|
||||
|
||||
/// Represents a list of IMAP mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct ImapMboxes(pub Vec<ImapMbox>);
|
||||
#[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.0
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for ImapMboxes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +134,9 @@ pub type RawImapMboxes = imap::types::ZeroCopy<Vec<RawImapMbox>>;
|
|||
|
||||
impl<'a> From<RawImapMboxes> for ImapMboxes {
|
||||
fn from(raw_mboxes: RawImapMboxes) -> Self {
|
||||
Self(raw_mboxes.iter().map(ImapMbox::from).collect())
|
||||
Self {
|
||||
mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::{debug, info, trace};
|
||||
use std::{convert::TryInto, fs, path::PathBuf};
|
||||
use std::{convert::TryInto, env, fs, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
|
||||
|
@ -41,30 +41,32 @@ impl<'a> MaildirBackend<'a> {
|
|||
|
||||
/// 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" {
|
||||
self.validate_mdir_path(self.mdir.path().to_owned())
|
||||
.map(maildir::Maildir::from)
|
||||
} else {
|
||||
// If the dir is a valid maildir path, creates a maildir instance from it.
|
||||
self.validate_mdir_path(dir.into())
|
||||
.or_else(|_| {
|
||||
// Otherwise creates a maildir instance from a
|
||||
// maildir subdirectory by adding a "." in front
|
||||
// of the name as described in the spec:
|
||||
// https://cr.yp.to/proto/maildir.html
|
||||
let dir = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.get(dir)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(dir);
|
||||
let path = self.mdir.path().join(format!(".{}", dir));
|
||||
self.validate_mdir_path(path)
|
||||
})
|
||||
.map(maildir::Maildir::from)
|
||||
if &dir == "inbox" {
|
||||
return self
|
||||
.validate_mdir_path(self.mdir.path().to_owned())
|
||||
.map(maildir::Maildir::from);
|
||||
}
|
||||
|
||||
// If the dir is a valid maildir path, creates a maildir
|
||||
// instance from it. First checks for absolute path,
|
||||
self.validate_mdir_path((&dir).into())
|
||||
// then for relative path to `maildir-dir`,
|
||||
.or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir)))
|
||||
// and finally for relative path to the current directory.
|
||||
.or_else(|_| self.validate_mdir_path(env::current_dir()?.join(&dir)))
|
||||
.or_else(|_| {
|
||||
// Otherwise creates a maildir instance from a maildir
|
||||
// subdirectory by adding a "." in front of the name
|
||||
// as described in the [spec].
|
||||
//
|
||||
// [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
|
||||
self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir)))
|
||||
})
|
||||
.map(maildir::Maildir::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,7 +151,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> {
|
|||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
|
||||
// Applies pagination boundaries.
|
||||
envelopes.0 = envelopes[page_begin..page_end].to_owned();
|
||||
envelopes.envelopes = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::DateTime;
|
||||
use log::{debug, info, trace};
|
||||
use log::trace;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Deref, DerefMut},
|
||||
|
@ -20,27 +20,30 @@ use crate::{
|
|||
|
||||
/// Represents a list of envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct MaildirEnvelopes(pub Vec<MaildirEnvelope>);
|
||||
pub struct MaildirEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<MaildirEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for MaildirEnvelopes {
|
||||
type Target = Vec<MaildirEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for MaildirEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for MaildirEnvelopes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +117,7 @@ impl<'a> TryFrom<RawMaildirEnvelopes> for MaildirEnvelopes {
|
|||
envelopes.push(envelope);
|
||||
}
|
||||
|
||||
Ok(MaildirEnvelopes(envelopes))
|
||||
Ok(MaildirEnvelopes { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +128,7 @@ impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
|
|||
type Error = Error;
|
||||
|
||||
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
|
||||
info!("begin: try building envelope from maildir parsed mail");
|
||||
trace!(">> build envelope from maildir parsed mail");
|
||||
|
||||
let mut envelope = Self::default();
|
||||
|
||||
|
@ -139,14 +142,14 @@ impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
|
|||
.parsed()
|
||||
.context("cannot parse maildir mail entry")?;
|
||||
|
||||
debug!("begin: parse headers");
|
||||
trace!(">> parse headers");
|
||||
for h in parsed_mail.get_headers() {
|
||||
let k = h.get_key();
|
||||
debug!("header key: {:?}", k);
|
||||
trace!("header key: {:?}", k);
|
||||
|
||||
let v = rfc2047_decoder::decode(h.get_value_raw())
|
||||
.context(format!("cannot decode value from header {:?}", k))?;
|
||||
debug!("header value: {:?}", v);
|
||||
trace!("header value: {:?}", v);
|
||||
|
||||
match k.to_lowercase().as_str() {
|
||||
"date" => {
|
||||
|
@ -182,10 +185,10 @@ impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
debug!("end: parse headers");
|
||||
trace!("<< parse headers");
|
||||
|
||||
trace!("envelope: {:?}", envelope);
|
||||
info!("end: try building envelope from maildir parsed mail");
|
||||
trace!("<< build envelope from maildir parsed mail");
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,21 +19,24 @@ use crate::{
|
|||
|
||||
/// Represents a list of Maildir mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct MaildirMboxes(pub Vec<MaildirMbox>);
|
||||
pub struct MaildirMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<MaildirMbox>,
|
||||
}
|
||||
|
||||
impl Deref for MaildirMboxes {
|
||||
type Target = Vec<MaildirMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for MaildirMboxes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +116,7 @@ impl TryFrom<RawMaildirMboxes> for MaildirMboxes {
|
|||
for entry in mail_entries {
|
||||
mboxes.push(entry?.try_into()?);
|
||||
}
|
||||
Ok(MaildirMboxes(mboxes))
|
||||
Ok(MaildirMboxes { mboxes })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ impl<'a> NotmuchBackend<'a> {
|
|||
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
|
||||
|
||||
// Applies pagination boundaries.
|
||||
envelopes.0 = envelopes[page_begin..page_end].to_owned();
|
||||
envelopes.envelopes = envelopes[page_begin..page_end].to_owned();
|
||||
|
||||
// Appends envelopes hash to the id mapper cache file and
|
||||
// calculates the new short hash length. The short hash length
|
||||
|
@ -118,17 +118,17 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
info!(">> get notmuch virtual mailboxes");
|
||||
|
||||
let mut virt_mboxes: Vec<_> = self
|
||||
let mut mboxes: Vec<_> = self
|
||||
.account_config
|
||||
.mailboxes
|
||||
.iter()
|
||||
.map(|(k, v)| NotmuchMbox::new(k, v))
|
||||
.collect();
|
||||
trace!("virtual mailboxes: {:?}", virt_mboxes);
|
||||
virt_mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
trace!("virtual mailboxes: {:?}", mboxes);
|
||||
mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
|
||||
info!("<< get notmuch virtual mailboxes");
|
||||
Ok(Box::new(NotmuchMboxes(virt_mboxes)))
|
||||
Ok(Box::new(NotmuchMboxes { mboxes }))
|
||||
}
|
||||
|
||||
fn del_mbox(&mut self, _mbox: &str) -> Result<()> {
|
||||
|
@ -202,7 +202,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
|
|||
// Adds the message to the maildir folder and gets its hash.
|
||||
let hash = self
|
||||
.mdir
|
||||
.add_msg("inbox", msg, "seen")
|
||||
.add_msg("", msg, "seen")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"cannot add notmuch message to maildir {:?}",
|
||||
|
|
|
@ -19,27 +19,30 @@ use crate::{
|
|||
|
||||
/// Represents a list of envelopes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchEnvelopes(pub Vec<NotmuchEnvelope>);
|
||||
pub struct NotmuchEnvelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<NotmuchEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for NotmuchEnvelopes {
|
||||
type Target = Vec<NotmuchEnvelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NotmuchEnvelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchEnvelopes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +110,7 @@ impl<'a> TryFrom<RawNotmuchEnvelopes> for NotmuchEnvelopes {
|
|||
.context("cannot parse notmuch mail entry")?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
Ok(NotmuchEnvelopes(envelopes))
|
||||
Ok(NotmuchEnvelopes { envelopes })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,21 +17,24 @@ use crate::{
|
|||
|
||||
/// Represents a list of Notmuch mailboxes.
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct NotmuchMboxes(pub Vec<NotmuchMbox>);
|
||||
pub struct NotmuchMboxes {
|
||||
#[serde(rename = "response")]
|
||||
pub mboxes: Vec<NotmuchMbox>,
|
||||
}
|
||||
|
||||
impl Deref for NotmuchMboxes {
|
||||
type Target = Vec<NotmuchMbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for NotmuchMboxes {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,10 +31,10 @@ impl Deref for Accounts {
|
|||
}
|
||||
|
||||
impl PrintTable for Accounts {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,16 @@ pub struct AccountConfig {
|
|||
/// Represents the text/plain format as defined in the
|
||||
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
|
||||
pub format: Format,
|
||||
/// Overrides the default headers displayed at the top of
|
||||
/// the read message.
|
||||
pub read_headers: Vec<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// Represents hooks.
|
||||
pub hooks: Hooks,
|
||||
|
||||
/// Represents the SMTP host.
|
||||
pub smtp_host: String,
|
||||
/// Represents the SMTP port.
|
||||
|
@ -154,7 +160,9 @@ impl<'a> AccountConfig {
|
|||
.unwrap_or(&vec![])
|
||||
.to_owned(),
|
||||
format: base_account.format.unwrap_or_default(),
|
||||
read_headers: base_account.read_headers,
|
||||
mailboxes: base_account.mailboxes.clone(),
|
||||
hooks: base_account.hooks.unwrap_or_default(),
|
||||
default: base_account.default.unwrap_or_default(),
|
||||
email: base_account.email.to_owned(),
|
||||
|
||||
|
@ -203,8 +211,7 @@ impl<'a> AccountConfig {
|
|||
|
||||
/// Builds the full RFC822 compliant address of the user account.
|
||||
pub fn address(&self) -> Result<MailAddr> {
|
||||
let has_special_chars =
|
||||
"()<>[]:;@.,".contains(|special_char| self.display_name.contains(special_char));
|
||||
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
|
||||
let addr = if self.display_name.is_empty() {
|
||||
self.email.clone()
|
||||
} else if has_special_chars {
|
||||
|
@ -314,6 +321,19 @@ impl<'a> AccountConfig {
|
|||
run_cmd(&cmd).context("cannot run notify cmd")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the mailbox alias if exists, otherwise returns the
|
||||
/// mailbox. Also tries to expand shell variables.
|
||||
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String> {
|
||||
let mbox = self
|
||||
.mailboxes
|
||||
.get(&mbox.trim().to_lowercase())
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(mbox);
|
||||
shellexpand::full(mbox)
|
||||
.map(String::from)
|
||||
.with_context(|| format!("cannot expand mailbox path {:?}", mbox))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all existing kind of account (backend).
|
||||
|
|
|
@ -49,11 +49,11 @@ mod tests {
|
|||
#[test]
|
||||
fn it_should_match_cmds_accounts() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWritter {
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWritter {
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
|
@ -66,7 +66,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWritter {
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -80,11 +80,11 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWritter {}
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writter: StringWritter,
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl PrinterService for PrinterServiceTest {
|
||||
|
@ -93,10 +93,16 @@ mod tests {
|
|||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
data.print_table(&mut self.writter, opts)?;
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print<T: serde::Serialize + Print>(&mut self, _data: T) -> Result<()> {
|
||||
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
|
@ -126,7 +132,7 @@ mod tests {
|
|||
"account-1 │imap │yes \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writter.content
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::Format;
|
||||
use crate::config::{Format, Hooks};
|
||||
|
||||
pub trait ToDeserializedBaseAccountConfig {
|
||||
fn to_base(&self) -> DeserializedBaseAccountConfig;
|
||||
|
@ -45,7 +45,7 @@ macro_rules! make_account_config {
|
|||
pub signature: Option<String>,
|
||||
/// Overrides the signature delimiter for this account.
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Overrides the default page size for this account.
|
||||
/// Overrides the default page size for this account.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Overrides the notify command for this account.
|
||||
pub notify_cmd: Option<String>,
|
||||
|
@ -56,6 +56,10 @@ macro_rules! make_account_config {
|
|||
/// Represents the text/plain format as defined in the
|
||||
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
|
||||
pub format: Option<Format>,
|
||||
/// Represents the default headers displayed at the top of
|
||||
/// the read message.
|
||||
#[serde(default)]
|
||||
pub read_headers: Vec<String>,
|
||||
|
||||
/// Makes this account the default one.
|
||||
pub default: Option<bool>,
|
||||
|
@ -84,6 +88,9 @@ macro_rules! make_account_config {
|
|||
#[serde(default)]
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// Represents hooks.
|
||||
pub hooks: Option<Hooks>,
|
||||
|
||||
$(pub $element: $ty),*
|
||||
}
|
||||
|
||||
|
@ -99,6 +106,7 @@ macro_rules! make_account_config {
|
|||
notify_query: self.notify_query.clone(),
|
||||
watch_cmds: self.watch_cmds.clone(),
|
||||
format: self.format.clone(),
|
||||
read_headers: self.read_headers.clone(),
|
||||
|
||||
default: self.default.clone(),
|
||||
email: self.email.clone(),
|
||||
|
@ -114,6 +122,7 @@ macro_rules! make_account_config {
|
|||
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
|
||||
|
||||
mailboxes: self.mailboxes.clone(),
|
||||
hooks: self.hooks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ pub struct DeserializedConfig {
|
|||
pub downloads_dir: Option<PathBuf>,
|
||||
/// Represents the signature of the user.
|
||||
pub signature: Option<String>,
|
||||
/// Overrides the default signature delimiter "`--\n `".
|
||||
/// Overrides the default signature delimiter "`-- \n`".
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: Option<usize>,
|
||||
|
|
7
src/config/hooks.rs
Normal file
7
src/config/hooks.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Hooks {
|
||||
pub pre_send: Option<String>,
|
||||
}
|
|
@ -126,6 +126,9 @@ pub mod config {
|
|||
|
||||
pub mod format;
|
||||
pub use format::*;
|
||||
|
||||
pub mod hooks;
|
||||
pub use hooks::*;
|
||||
}
|
||||
|
||||
pub mod compl;
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -221,8 +221,17 @@ fn main() -> Result<()> {
|
|||
Some(msg_args::Cmd::Move(seq, mbox_dst)) => {
|
||||
return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend);
|
||||
}
|
||||
Some(msg_args::Cmd::Read(seq, text_mime, raw)) => {
|
||||
return msg_handlers::read(seq, text_mime, raw, mbox, &mut printer, backend);
|
||||
Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => {
|
||||
return msg_handlers::read(
|
||||
seq,
|
||||
text_mime,
|
||||
raw,
|
||||
headers,
|
||||
mbox,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend,
|
||||
);
|
||||
}
|
||||
Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => {
|
||||
return msg_handlers::reply(
|
||||
|
|
|
@ -47,11 +47,11 @@ mod tests {
|
|||
#[test]
|
||||
fn it_should_list_mboxes() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWritter {
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWritter {
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
|
@ -64,7 +64,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWritter {
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -78,11 +78,11 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWritter {}
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writter: StringWritter,
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl PrinterService for PrinterServiceTest {
|
||||
|
@ -91,10 +91,16 @@ mod tests {
|
|||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
data.print_table(&mut self.writter, opts)?;
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print<T: serde::Serialize + Print>(&mut self, _data: T) -> Result<()> {
|
||||
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
|
@ -109,21 +115,23 @@ mod tests {
|
|||
unimplemented!();
|
||||
}
|
||||
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||
Ok(Box::new(ImapMboxes(vec![
|
||||
ImapMbox {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||
},
|
||||
ImapMbox {
|
||||
delim: "/".into(),
|
||||
name: "Sent".into(),
|
||||
attrs: ImapMboxAttrs(vec![
|
||||
ImapMboxAttr::NoInferiors,
|
||||
ImapMboxAttr::Custom("HasNoChildren".into()),
|
||||
]),
|
||||
},
|
||||
])))
|
||||
Ok(Box::new(ImapMboxes {
|
||||
mboxes: vec![
|
||||
ImapMbox {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||
},
|
||||
ImapMbox {
|
||||
delim: "/".into(),
|
||||
name: "Sent".into(),
|
||||
attrs: ImapMboxAttrs(vec![
|
||||
ImapMboxAttr::NoInferiors,
|
||||
ImapMboxAttr::Custom("HasNoChildren".into()),
|
||||
]),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
fn del_mbox(&mut self, _: &str) -> Result<()> {
|
||||
unimplemented!();
|
||||
|
@ -181,7 +189,7 @@ mod tests {
|
|||
"/ │Sent │NoInferiors, HasNoChildren \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writter.content
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.add_flags(mbox, seq_range, flags)?;
|
||||
printer.print(format!(
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully added to message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
|
@ -32,7 +32,7 @@ pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.del_flags(mbox, seq_range, flags)?;
|
||||
printer.print(format!(
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully removed from message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
|
@ -48,7 +48,7 @@ pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.set_flags(mbox, seq_range, flags)?;
|
||||
printer.print(format!(
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully set for message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
|
|
|
@ -25,6 +25,7 @@ type AttachmentPaths<'a> = Vec<&'a str>;
|
|||
type MaxTableWidth = Option<usize>;
|
||||
type Encrypt = bool;
|
||||
type Criteria = String;
|
||||
type Headers<'a> = Vec<&'a str>;
|
||||
|
||||
/// Message commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
@ -35,7 +36,7 @@ pub enum Cmd<'a> {
|
|||
Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt),
|
||||
List(MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Seq<'a>, Mbox<'a>),
|
||||
Read(Seq<'a>, TextMime<'a>, Raw),
|
||||
Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>),
|
||||
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
|
||||
Save(RawMsg<'a>),
|
||||
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
|
@ -121,7 +122,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
debug!("text mime: {}", mime);
|
||||
let raw = m.is_present("raw");
|
||||
debug!("raw: {}", raw);
|
||||
return Ok(Some(Cmd::Read(seq, mime, raw)));
|
||||
let headers: Vec<&str> = m.values_of("headers").unwrap_or_default().collect();
|
||||
debug!("headers: {:?}", headers);
|
||||
return Ok(Some(Cmd::Read(seq, mime, raw, headers)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("reply") {
|
||||
|
@ -318,7 +321,7 @@ fn page_arg<'a>() -> Arg<'a, 'a> {
|
|||
}
|
||||
|
||||
/// Message attachment argument.
|
||||
pub fn attachment_arg<'a>() -> Arg<'a, 'a> {
|
||||
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("attachments")
|
||||
.help("Adds attachment to the message")
|
||||
.short("a")
|
||||
|
@ -327,6 +330,16 @@ pub fn attachment_arg<'a>() -> Arg<'a, 'a> {
|
|||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Represents the message headers argument.
|
||||
pub fn headers_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("headers")
|
||||
.help("Shows additional headers with the message")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("STR")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Message encrypt argument.
|
||||
pub fn encrypt_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("encrypt")
|
||||
|
@ -399,7 +412,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
),
|
||||
SubCommand::with_name("write")
|
||||
.about("Writes a new message")
|
||||
.arg(attachment_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("send")
|
||||
.about("Sends a raw message")
|
||||
|
@ -424,19 +437,20 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
.help("Reads raw message")
|
||||
.long("raw")
|
||||
.short("r"),
|
||||
),
|
||||
)
|
||||
.arg(headers_arg()),
|
||||
SubCommand::with_name("reply")
|
||||
.aliases(&["rep", "r"])
|
||||
.about("Answers to a message")
|
||||
.arg(seq_arg())
|
||||
.arg(reply_all_arg())
|
||||
.arg(attachment_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "f"])
|
||||
.about("Forwards a message")
|
||||
.arg(seq_arg())
|
||||
.arg(attachment_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("copy")
|
||||
.aliases(&["cp", "c"])
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
use ammonia;
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
use convert_case::{Case, Casing};
|
||||
use html_escape;
|
||||
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
|
||||
use log::{debug, info, trace, warn};
|
||||
use log::{info, trace, warn};
|
||||
use regex::Regex;
|
||||
use std::{collections::HashSet, convert::TryInto, env::temp_dir, fmt::Debug, fs, path::PathBuf};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
convert::TryInto,
|
||||
env::temp_dir,
|
||||
fmt::Debug,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
|
@ -13,7 +21,7 @@ use crate::{
|
|||
config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM},
|
||||
msg::{
|
||||
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils,
|
||||
Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,
|
||||
Addr, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,
|
||||
},
|
||||
output::PrinterService,
|
||||
smtp::SmtpService,
|
||||
|
@ -24,7 +32,7 @@ use crate::{
|
|||
};
|
||||
|
||||
/// Representation of a message.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Msg {
|
||||
/// The sequence number of the message.
|
||||
///
|
||||
|
@ -41,11 +49,12 @@ pub struct Msg {
|
|||
pub bcc: Option<Addrs>,
|
||||
pub in_reply_to: Option<String>,
|
||||
pub message_id: Option<String>,
|
||||
pub headers: HashMap<String, String>,
|
||||
|
||||
/// The internal date of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
|
||||
pub date: Option<DateTime<FixedOffset>>,
|
||||
pub date: Option<DateTime<Local>>,
|
||||
pub parts: Parts,
|
||||
|
||||
pub encrypt: bool,
|
||||
|
@ -64,8 +73,9 @@ impl Msg {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Folds string body from all plain text parts into a single string body. If no plain text
|
||||
/// parts are found, HTML parts are used instead. The result is sanitized (all HTML markup is
|
||||
/// Folds string body from all plain text parts into a single
|
||||
/// string body. If no plain text parts are found, HTML parts are
|
||||
/// used instead. The result is sanitized (all HTML markup is
|
||||
/// removed).
|
||||
pub fn fold_text_plain_parts(&self) -> String {
|
||||
let (plain, html) = self.parts.iter().fold(
|
||||
|
@ -133,7 +143,8 @@ impl Msg {
|
|||
}
|
||||
}
|
||||
|
||||
/// Fold string body from all HTML parts into a single string body.
|
||||
/// Fold string body from all HTML parts into a single string
|
||||
/// body.
|
||||
fn fold_text_html_parts(&self) -> String {
|
||||
let text_parts = self
|
||||
.parts
|
||||
|
@ -151,8 +162,9 @@ impl Msg {
|
|||
text_parts
|
||||
}
|
||||
|
||||
/// Fold string body from all text parts into a single string body. The mime allows users to
|
||||
/// choose between plain text parts and html text parts.
|
||||
/// Fold string body from all text parts into a single string
|
||||
/// body. The mime allows users to choose between plain text parts
|
||||
/// and html text parts.
|
||||
pub fn fold_text_parts(&self, text_mime: &str) -> String {
|
||||
if text_mime == "html" {
|
||||
self.fold_text_html_parts()
|
||||
|
@ -164,22 +176,25 @@ impl Msg {
|
|||
pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result<Self> {
|
||||
let account_addr = account.address()?;
|
||||
|
||||
// Message-Id
|
||||
self.message_id = None;
|
||||
|
||||
// In-Reply-To
|
||||
self.in_reply_to = self.message_id.to_owned();
|
||||
|
||||
// Message-Id
|
||||
self.message_id = None;
|
||||
|
||||
// To
|
||||
let addrs = self
|
||||
.reply_to
|
||||
.as_deref()
|
||||
.or_else(|| self.from.as_deref())
|
||||
.map(|addrs| {
|
||||
addrs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|addr| addr != &account_addr)
|
||||
addrs.iter().cloned().filter(|addr| match addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(a) => match &account_addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(b) => a.addr != b.addr,
|
||||
},
|
||||
})
|
||||
});
|
||||
if all {
|
||||
self.to = addrs.map(|addrs| addrs.collect::<Vec<_>>().into());
|
||||
|
@ -189,18 +204,35 @@ impl Msg {
|
|||
.map(|addr| vec![addr].into());
|
||||
}
|
||||
|
||||
// Cc & Bcc
|
||||
if !all {
|
||||
self.cc = None;
|
||||
self.bcc = None;
|
||||
}
|
||||
// Cc
|
||||
self.cc = if all {
|
||||
self.cc.as_deref().map(|addrs| {
|
||||
addrs
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|addr| match addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(a) => match &account_addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(b) => a.addr != b.addr,
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Bcc
|
||||
self.bcc = None;
|
||||
|
||||
// Body
|
||||
let plain_content = {
|
||||
let date = self
|
||||
.date
|
||||
.as_ref()
|
||||
.map(|date| date.format("%d %b %Y, at %H:%M").to_string())
|
||||
.map(|date| date.format("%d %b %Y, at %H:%M (%z)").to_string())
|
||||
.unwrap_or_else(|| "unknown date".into());
|
||||
let sender = self
|
||||
.reply_to
|
||||
|
@ -339,15 +371,18 @@ impl Msg {
|
|||
loop {
|
||||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
let sent_msg = smtp.send_msg(account, &self)?;
|
||||
printer.print_str("Sending message…")?;
|
||||
let sent_msg = smtp.send(account, &self)?;
|
||||
let sent_folder = account
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
backend.add_msg(&sent_folder, &sent_msg.formatted(), "seen")?;
|
||||
printer
|
||||
.print_str(format!("Adding message to the {:?} folder…", sent_folder))?;
|
||||
backend.add_msg(&sent_folder, &sent_msg, "seen")?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
printer.print("Message successfully sent")?;
|
||||
printer.print_struct("Done!")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Edit) => {
|
||||
|
@ -355,7 +390,7 @@ impl Msg {
|
|||
continue;
|
||||
}
|
||||
Ok(PostEditChoice::LocalDraft) => {
|
||||
printer.print("Message successfully saved locally")?;
|
||||
printer.print_struct("Message successfully saved locally")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::RemoteDraft) => {
|
||||
|
@ -367,7 +402,8 @@ impl Msg {
|
|||
.unwrap_or(DEFAULT_DRAFT_FOLDER);
|
||||
backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
printer.print(format!("Message successfully saved to {}", draft_folder))?;
|
||||
printer
|
||||
.print_struct(format!("Message successfully saved to {}", draft_folder))?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Discard) => {
|
||||
|
@ -413,24 +449,19 @@ impl Msg {
|
|||
}
|
||||
|
||||
pub fn merge_with(&mut self, msg: Msg) {
|
||||
if msg.from.is_some() {
|
||||
self.from = msg.from;
|
||||
self.from = msg.from;
|
||||
self.reply_to = msg.reply_to;
|
||||
self.to = msg.to;
|
||||
self.cc = msg.cc;
|
||||
self.bcc = msg.bcc;
|
||||
self.subject = msg.subject;
|
||||
|
||||
if msg.message_id.is_some() {
|
||||
self.message_id = msg.message_id;
|
||||
}
|
||||
|
||||
if msg.to.is_some() {
|
||||
self.to = msg.to;
|
||||
}
|
||||
|
||||
if msg.cc.is_some() {
|
||||
self.cc = msg.cc;
|
||||
}
|
||||
|
||||
if msg.bcc.is_some() {
|
||||
self.bcc = msg.bcc;
|
||||
}
|
||||
|
||||
if !msg.subject.is_empty() {
|
||||
self.subject = msg.subject;
|
||||
if msg.in_reply_to.is_some() {
|
||||
self.in_reply_to = msg.in_reply_to;
|
||||
}
|
||||
|
||||
for part in msg.parts.0.into_iter() {
|
||||
|
@ -623,24 +654,18 @@ impl Msg {
|
|||
parsed_mail: mailparse::ParsedMail<'_>,
|
||||
config: &AccountConfig,
|
||||
) -> Result<Self> {
|
||||
info!("begin: building message from parsed mail");
|
||||
trace!(">> build message from parsed mail");
|
||||
trace!("parsed mail: {:?}", parsed_mail);
|
||||
|
||||
let mut msg = Msg::default();
|
||||
|
||||
debug!("parsing headers");
|
||||
for header in parsed_mail.get_headers() {
|
||||
trace!(">> parse header {:?}", header);
|
||||
|
||||
let key = header.get_key();
|
||||
debug!("header key: {:?}", key);
|
||||
trace!("header key: {:?}", key);
|
||||
|
||||
let val = header.get_value();
|
||||
let val = String::from_utf8(header.get_value_raw().to_vec())
|
||||
.map(|val| val.trim().to_string())
|
||||
.context(format!(
|
||||
"cannot decode value {:?} from header {:?}",
|
||||
key, val
|
||||
))?;
|
||||
debug!("header value: {:?}", val);
|
||||
trace!("header value: {:?}", val);
|
||||
|
||||
match key.to_lowercase().as_str() {
|
||||
"message-id" => msg.message_id = Some(val),
|
||||
|
@ -648,16 +673,15 @@ impl Msg {
|
|||
"subject" => {
|
||||
msg.subject = val;
|
||||
}
|
||||
"date" => {
|
||||
msg.date = DateTime::parse_from_rfc2822(
|
||||
val.split_at(val.find(" (").unwrap_or_else(|| val.len())).0,
|
||||
)
|
||||
.map_err(|err| {
|
||||
"date" => match mailparse::dateparse(&val) {
|
||||
Ok(timestamp) => {
|
||||
msg.date = Some(Utc.timestamp(timestamp, 0).with_timezone(&Local))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("cannot parse message date {:?}, skipping it", val);
|
||||
err
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
warn!("{}", err);
|
||||
}
|
||||
},
|
||||
"from" => {
|
||||
msg.from = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
|
@ -678,24 +702,133 @@ impl Msg {
|
|||
msg.bcc = from_slice_to_addrs(val)
|
||||
.context(format!("cannot parse header {:?}", key))?
|
||||
}
|
||||
_ => (),
|
||||
key => {
|
||||
msg.headers.insert(key.to_lowercase(), val);
|
||||
}
|
||||
}
|
||||
trace!("<< parse header");
|
||||
}
|
||||
|
||||
msg.parts = Parts::from_parsed_mail(config, &parsed_mail)
|
||||
.context("cannot parsed message mime parts")?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("end: building message from parsed mail");
|
||||
info!("<< build message from parsed mail");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Transforms a message into a readable string. A readable
|
||||
/// message is like a template, except that:
|
||||
/// - headers part is customizable (can be omitted if empty filter given in argument)
|
||||
/// - body type is customizable (plain or html)
|
||||
pub fn to_readable_string(
|
||||
&self,
|
||||
text_mime: &str,
|
||||
headers: Vec<&str>,
|
||||
config: &AccountConfig,
|
||||
) -> Result<String> {
|
||||
let mut all_headers = vec![];
|
||||
for h in config.read_headers.iter() {
|
||||
let h = h.to_lowercase();
|
||||
if !all_headers.contains(&h) {
|
||||
all_headers.push(h)
|
||||
}
|
||||
}
|
||||
for h in headers.iter() {
|
||||
let h = h.to_lowercase();
|
||||
if !all_headers.contains(&h) {
|
||||
all_headers.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
let mut readable_msg = String::new();
|
||||
for h in all_headers {
|
||||
match h.as_str() {
|
||||
"message-id" => match self.message_id {
|
||||
Some(ref message_id) if !message_id.is_empty() => {
|
||||
readable_msg.push_str(&format!("Message-Id: {}\n", message_id));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"in-reply-to" => match self.in_reply_to {
|
||||
Some(ref in_reply_to) if !in_reply_to.is_empty() => {
|
||||
readable_msg.push_str(&format!("In-Reply-To: {}\n", in_reply_to));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"subject" => {
|
||||
readable_msg.push_str(&format!("Subject: {}\n", self.subject));
|
||||
}
|
||||
"date" => {
|
||||
if let Some(ref date) = self.date {
|
||||
readable_msg.push_str(&format!("Date: {}\n", date.to_rfc2822()));
|
||||
}
|
||||
}
|
||||
"from" => match self.from {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("From: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"to" => match self.to {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("To: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"reply-to" => match self.reply_to {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("Reply-To: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"cc" => match self.cc {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("Cc: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"bcc" => match self.bcc {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("Bcc: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
key => match self.headers.get(key) {
|
||||
Some(ref val) if !val.is_empty() => {
|
||||
readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Train), val));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if !readable_msg.is_empty() {
|
||||
readable_msg.push_str("\n");
|
||||
}
|
||||
|
||||
readable_msg.push_str(&self.fold_text_parts(text_mime));
|
||||
Ok(readable_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<lettre::address::Envelope> for Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||
let from = match self.from.and_then(|addrs| addrs.extract_single_info()) {
|
||||
(&self).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<lettre::address::Envelope> for &Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||
let from = match self
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.clone().extract_single_info())
|
||||
{
|
||||
Some(addr) => addr.addr.parse().map(Some),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
@ -707,3 +840,234 @@ impl TryInto<lettre::address::Envelope> for Msg {
|
|||
Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mailparse::SingleInfo;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use crate::msg::Addr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_into_reply() {
|
||||
let config = AccountConfig {
|
||||
display_name: "Test".into(),
|
||||
email: "test-account@local".into(),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
|
||||
// Checks that:
|
||||
// - "message_id" moves to "in_reply_to"
|
||||
// - "subject" starts by "Re: "
|
||||
// - "to" is replaced by "from"
|
||||
// - "from" is replaced by the address from the account config
|
||||
|
||||
let msg = Msg {
|
||||
message_id: Some("msg-id".into()),
|
||||
subject: "subject".into(),
|
||||
from: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test-sender@local".into(),
|
||||
display_name: None,
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
..Msg::default()
|
||||
}
|
||||
.into_reply(false, &config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(msg.message_id, None);
|
||||
assert_eq!(msg.in_reply_to.unwrap(), "msg-id");
|
||||
assert_eq!(msg.subject, "Re: subject");
|
||||
assert_eq!(
|
||||
msg.from.unwrap().to_string(),
|
||||
"\"Test\" <test-account@local>"
|
||||
);
|
||||
assert_eq!(msg.to.unwrap().to_string(), "test-sender@local");
|
||||
|
||||
// Checks that:
|
||||
// - "subject" does not contains additional "Re: "
|
||||
// - "to" is replaced by reply_to
|
||||
// - "to" contains one address when "all" is false
|
||||
// - "cc" are empty when "all" is false
|
||||
|
||||
let msg = Msg {
|
||||
subject: "Re: subject".into(),
|
||||
from: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test-sender@local".into(),
|
||||
display_name: None,
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
reply_to: Some(
|
||||
vec![
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-to-reply@local".into(),
|
||||
display_name: Some("Sender".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-to-reply-2@local".into(),
|
||||
display_name: Some("Sender 2".into()),
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
cc: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test-cc@local".into(),
|
||||
display_name: None,
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
..Msg::default()
|
||||
}
|
||||
.into_reply(false, &config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(msg.subject, "Re: subject");
|
||||
assert_eq!(
|
||||
msg.to.unwrap().to_string(),
|
||||
"\"Sender\" <test-sender-to-reply@local>"
|
||||
);
|
||||
assert_eq!(msg.cc, None);
|
||||
|
||||
// Checks that:
|
||||
// - "to" contains all addresses except for the sender when "all" is true
|
||||
// - "cc" contains all addresses except for the sender when "all" is true
|
||||
|
||||
let msg = Msg {
|
||||
from: Some(
|
||||
vec![
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-1@local".into(),
|
||||
display_name: Some("Sender 1".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-2@local".into(),
|
||||
display_name: Some("Sender 2".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-account@local".into(),
|
||||
display_name: Some("Test".into()),
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
cc: Some(
|
||||
vec![
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-1@local".into(),
|
||||
display_name: Some("Sender 1".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-2@local".into(),
|
||||
display_name: Some("Sender 2".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-account@local".into(),
|
||||
display_name: None,
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
..Msg::default()
|
||||
}
|
||||
.into_reply(true, &config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
msg.to.unwrap().to_string(),
|
||||
"\"Sender 1\" <test-sender-1@local>, \"Sender 2\" <test-sender-2@local>"
|
||||
);
|
||||
assert_eq!(
|
||||
msg.cc.unwrap().to_string(),
|
||||
"\"Sender 1\" <test-sender-1@local>, \"Sender 2\" <test-sender-2@local>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_readable() {
|
||||
let config = AccountConfig::default();
|
||||
let msg = Msg {
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: String::from("hello, world!"),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
|
||||
// empty msg headers, empty headers, empty config
|
||||
assert_eq!(
|
||||
"hello, world!",
|
||||
msg.to_readable_string("plain", vec![], &config).unwrap()
|
||||
);
|
||||
// empty msg headers, basic headers
|
||||
assert_eq!(
|
||||
"hello, world!",
|
||||
msg.to_readable_string("plain", vec!["From", "DATE", "custom-hEader"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
// empty msg headers, multiple subject headers
|
||||
assert_eq!(
|
||||
"Subject: \n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["subject", "Subject", "SUBJECT"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let msg = Msg {
|
||||
headers: HashMap::from_iter([("custom-header".into(), "custom value".into())]),
|
||||
message_id: Some("<message-id>".into()),
|
||||
from: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test@local".into(),
|
||||
display_name: Some("Test".into()),
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
cc: Some(vec![].into()),
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: String::from("hello, world!"),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
|
||||
// header present in msg headers, empty config
|
||||
assert_eq!(
|
||||
"From: \"Test\" <test@local>\n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["from"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
// header present but empty in msg headers, empty config
|
||||
assert_eq!(
|
||||
"hello, world!",
|
||||
msg.to_readable_string("plain", vec!["cc"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
// multiple same custom headers present in msg headers, empty
|
||||
// config
|
||||
assert_eq!(
|
||||
"Custom-Header: custom value\n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["custom-header", "cuSTom-HeaDer"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let config = AccountConfig {
|
||||
read_headers: vec![
|
||||
"CusTOM-heaDER".into(),
|
||||
"Subject".into(),
|
||||
"from".into(),
|
||||
"cc".into(),
|
||||
],
|
||||
..AccountConfig::default()
|
||||
};
|
||||
// header present but empty in msg headers, empty config
|
||||
assert_eq!(
|
||||
"Custom-Header: custom value\nSubject: \nFrom: \"Test\" <test@local>\nMessage-Id: <message-id>\n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["cc", "message-ID"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ use log::{debug, info, trace};
|
|||
use mailparse::addrparse;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
convert::TryInto,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
|
@ -32,21 +31,29 @@ pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
) -> Result<()> {
|
||||
let attachments = backend.get_msg(mbox, seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
debug!(
|
||||
r#"{} attachment(s) found for message "{}""#,
|
||||
attachments_len, seq
|
||||
);
|
||||
|
||||
if attachments_len == 0 {
|
||||
return printer.print_struct(format!("No attachment found for message {:?}", seq));
|
||||
}
|
||||
|
||||
printer.print_str(format!(
|
||||
"Found {:?} attachment{} for message {:?}",
|
||||
attachments_len,
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
seq
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let file_path = config.get_download_file_path(&attachment.filename)?;
|
||||
debug!("downloading {}…", attachment.filename);
|
||||
printer.print_str(format!("Downloading {:?}…", file_path))?;
|
||||
fs::write(&file_path, &attachment.content)
|
||||
.context(format!("cannot download attachment {:?}", file_path))?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"{} attachment(s) successfully downloaded to {:?}",
|
||||
attachments_len, config.downloads_dir
|
||||
printer.print_struct(format!(
|
||||
"Attachment{} successfully downloaded to {:?}",
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
config.downloads_dir
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -59,7 +66,7 @@ pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
backend: Box<&mut B>,
|
||||
) -> Result<()> {
|
||||
backend.copy_msg(mbox_src, mbox_dst, seq)?;
|
||||
printer.print(format!(
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully copied to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
|
@ -73,7 +80,7 @@ pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.del_msg(mbox, seq)?;
|
||||
printer.print(format!(r#"Message(s) {} successfully deleted"#, seq))
|
||||
printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq))
|
||||
}
|
||||
|
||||
/// Forward the given message UID from the selected mailbox.
|
||||
|
@ -189,7 +196,7 @@ pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.move_msg(mbox_src, mbox_dst, seq)?;
|
||||
printer.print(format!(
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully moved to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
|
@ -200,19 +207,20 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
seq: &str,
|
||||
text_mime: &str,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let msg = backend.get_msg(mbox, seq)?;
|
||||
let msg = if raw {
|
||||
|
||||
printer.print_struct(if raw {
|
||||
// Emails don't always have valid utf8. Using "lossy" to display what we can.
|
||||
String::from_utf8_lossy(&msg.raw).into_owned()
|
||||
} else {
|
||||
msg.fold_text_parts(text_mime)
|
||||
};
|
||||
|
||||
printer.print(msg)
|
||||
msg.to_readable_string(text_mime, headers, config)?
|
||||
})
|
||||
}
|
||||
|
||||
/// Reply to the given message UID.
|
||||
|
@ -348,9 +356,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
.join("\r\n")
|
||||
};
|
||||
trace!("raw message: {:?}", raw_msg);
|
||||
let envelope: lettre::address::Envelope = Msg::from_tpl(&raw_msg)?.try_into()?;
|
||||
trace!("envelope: {:?}", envelope);
|
||||
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
|
||||
let msg = Msg::from_tpl(&raw_msg)?;
|
||||
smtp.send(&config, &msg)?;
|
||||
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use log::{debug, trace};
|
|||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
pub fn local_draft_path() -> PathBuf {
|
||||
let path = env::temp_dir().join("himalaya-draft.mail");
|
||||
let path = env::temp_dir().join("himalaya-draft.eml");
|
||||
trace!("local draft path: {:?}", path);
|
||||
path
|
||||
}
|
||||
|
|
|
@ -183,13 +183,13 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
.subcommand(
|
||||
SubCommand::with_name("save")
|
||||
.about("Saves a message based on the given template")
|
||||
.arg(&msg_args::attachment_arg())
|
||||
.arg(&msg_args::attachments_arg())
|
||||
.arg(Arg::with_name("template").raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("send")
|
||||
.about("Sends a message based on the given template")
|
||||
.arg(&msg_args::attachment_arg())
|
||||
.arg(&msg_args::attachments_arg())
|
||||
.arg(Arg::with_name("template").raw(true)),
|
||||
)]
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ pub fn new<'a, P: PrinterService>(
|
|||
printer: &'a mut P,
|
||||
) -> Result<()> {
|
||||
let tpl = Msg::default().to_tpl(opts, account)?;
|
||||
printer.print(tpl)
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a reply message template.
|
||||
|
@ -38,7 +38,7 @@ pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
.get_msg(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print(tpl)
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a forward message template.
|
||||
|
@ -54,7 +54,7 @@ pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
.get_msg(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print(tpl)
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Saves a message based on a template.
|
||||
|
@ -79,7 +79,7 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let raw_msg = msg.into_sendable_msg(config)?.formatted();
|
||||
backend.add_msg(mbox, &raw_msg, "seen")?;
|
||||
printer.print("Template successfully saved")
|
||||
printer.print_struct("Template successfully saved")
|
||||
}
|
||||
|
||||
/// Sends a message based on a template.
|
||||
|
@ -103,7 +103,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
.join("\n")
|
||||
};
|
||||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let sent_msg = smtp.send_msg(account, &msg)?;
|
||||
backend.add_msg(mbox, &sent_msg.formatted(), "seen")?;
|
||||
printer.print("Template successfully sent")
|
||||
let sent_msg = smtp.send(account, &msg)?;
|
||||
backend.add_msg(mbox, &sent_msg, "seen")?;
|
||||
printer.print_struct("Template successfully sent")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::debug;
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// TODO: move this in a more approriate place.
|
||||
pub fn run_cmd(cmd: &str) -> Result<String> {
|
||||
|
@ -14,3 +17,25 @@ pub fn run_cmd(cmd: &str) -> Result<String> {
|
|||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
}
|
||||
|
||||
pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("cannot spawn process from command {:?}", cmd))?;
|
||||
process
|
||||
.stdin
|
||||
.ok_or_else(|| anyhow!("cannot get stdin"))?
|
||||
.write_all(data)
|
||||
.with_context(|| "cannot write data to stdin")?;
|
||||
process
|
||||
.stdout
|
||||
.ok_or_else(|| anyhow!("cannot get stdout"))?
|
||||
.read_to_end(&mut res)
|
||||
.with_context(|| "cannot read data from stdout")?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
use anyhow::{Context, Result};
|
||||
use log::error;
|
||||
|
||||
use crate::output::WriteColor;
|
||||
|
||||
pub trait Print {
|
||||
fn print(&self, writter: &mut dyn WriteColor) -> Result<()>;
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()>;
|
||||
}
|
||||
|
||||
impl Print for &str {
|
||||
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
|
||||
writeln!(writter, "{}", self).with_context(|| {
|
||||
error!(r#"cannot write string to writter: "{}""#, self);
|
||||
"cannot write string to writter"
|
||||
})
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
writeln!(writer, "{}", self).context("cannot write string to writer")
|
||||
}
|
||||
}
|
||||
|
||||
impl Print for String {
|
||||
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
|
||||
self.as_str().print(writter)
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
self.as_str().print(writer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub trait WriteColor: io::Write + termcolor::WriteColor {}
|
|||
impl WriteColor for StandardStream {}
|
||||
|
||||
pub trait PrintTable {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct PrintTableOpts<'a> {
|
||||
|
|
|
@ -9,8 +9,9 @@ use termcolor::{ColorChoice, StandardStream};
|
|||
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
|
||||
|
||||
pub trait PrinterService {
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
|
||||
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
||||
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()>;
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
|
||||
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
|
@ -19,16 +20,23 @@ pub trait PrinterService {
|
|||
}
|
||||
|
||||
pub struct StdoutPrinter {
|
||||
pub writter: Box<dyn WriteColor>,
|
||||
pub writer: Box<dyn WriteColor>,
|
||||
pub fmt: OutputFmt,
|
||||
}
|
||||
|
||||
impl PrinterService for StdoutPrinter {
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
|
||||
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print(self.writter.as_mut()),
|
||||
OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data))
|
||||
.context("cannot write JSON to writter"),
|
||||
OutputFmt::Plain => data.print(self.writer.as_mut()),
|
||||
OutputFmt::Json => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print(self.writer.as_mut()),
|
||||
OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &OutputJson::new(data))
|
||||
.context("cannot write JSON to writer"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,9 +46,9 @@ impl PrinterService for StdoutPrinter {
|
|||
opts: PrintTableOpts,
|
||||
) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print_table(self.writter.as_mut(), opts),
|
||||
OutputFmt::Plain => data.print_table(self.writer.as_mut(), opts),
|
||||
OutputFmt::Json => {
|
||||
let json = &mut serde_json::Serializer::new(self.writter.as_mut());
|
||||
let json = &mut serde_json::Serializer::new(self.writer.as_mut());
|
||||
let ser = &mut <dyn erased_serde::Serializer>::erase(json);
|
||||
data.erased_serialize(ser).unwrap();
|
||||
Ok(())
|
||||
|
@ -55,7 +63,7 @@ impl PrinterService for StdoutPrinter {
|
|||
|
||||
impl From<OutputFmt> for StdoutPrinter {
|
||||
fn from(fmt: OutputFmt) -> Self {
|
||||
let writter = StandardStream::stdout(if atty::isnt(Stream::Stdin) {
|
||||
let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) {
|
||||
// Colors should be deactivated if the terminal is not a tty.
|
||||
ColorChoice::Never
|
||||
} else {
|
||||
|
@ -67,8 +75,8 @@ impl From<OutputFmt> for StdoutPrinter {
|
|||
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
|
||||
ColorChoice::Auto
|
||||
});
|
||||
let writter = Box::new(writter);
|
||||
Self { writter, fmt }
|
||||
let writer = Box::new(writer);
|
||||
Self { writer, fmt }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use lettre::{
|
||||
self,
|
||||
transport::smtp::{
|
||||
|
@ -7,13 +7,12 @@ use lettre::{
|
|||
},
|
||||
Transport,
|
||||
};
|
||||
use log::debug;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{config::AccountConfig, msg::Msg};
|
||||
use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd};
|
||||
|
||||
pub trait SmtpService {
|
||||
fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result<lettre::Message>;
|
||||
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
|
||||
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub struct LettreService<'a> {
|
||||
|
@ -21,7 +20,7 @@ pub struct LettreService<'a> {
|
|||
transport: Option<SmtpTransport>,
|
||||
}
|
||||
|
||||
impl<'a> LettreService<'a> {
|
||||
impl LettreService<'_> {
|
||||
fn transport(&mut self) -> Result<&SmtpTransport> {
|
||||
if let Some(ref transport) = self.transport {
|
||||
Ok(transport)
|
||||
|
@ -55,24 +54,29 @@ impl<'a> LettreService<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> SmtpService for LettreService<'a> {
|
||||
fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result<lettre::Message> {
|
||||
debug!("sending message…");
|
||||
let sendable_msg = msg.into_sendable_msg(account)?;
|
||||
self.transport()?.send(&sendable_msg)?;
|
||||
Ok(sendable_msg)
|
||||
}
|
||||
impl SmtpService for LettreService<'_> {
|
||||
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>> {
|
||||
let mut raw_msg = msg.into_sendable_msg(account)?.formatted();
|
||||
|
||||
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()> {
|
||||
debug!("sending raw message…");
|
||||
self.transport()?.send_raw(envelope, msg)?;
|
||||
Ok(())
|
||||
let envelope: lettre::address::Envelope =
|
||||
if let Some(cmd) = account.hooks.pre_send.as_deref() {
|
||||
for cmd in cmd.split('|') {
|
||||
raw_msg = pipe_cmd(cmd.trim(), &raw_msg)
|
||||
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?
|
||||
}
|
||||
let parsed_mail = mailparse::parse_mail(&raw_msg)?;
|
||||
Msg::from_parsed_mail(parsed_mail, account)?.try_into()
|
||||
} else {
|
||||
msg.try_into()
|
||||
}?;
|
||||
|
||||
self.transport()?.send_raw(&envelope, &raw_msg)?;
|
||||
Ok(raw_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
|
||||
fn from(account: &'a AccountConfig) -> Self {
|
||||
debug!("init SMTP service");
|
||||
Self {
|
||||
account,
|
||||
transport: None,
|
||||
|
|
|
@ -127,14 +127,14 @@ impl Cell {
|
|||
|
||||
/// Makes the cell printable.
|
||||
impl Print for Cell {
|
||||
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
// Applies colors to the cell
|
||||
writter
|
||||
writer
|
||||
.set_color(&self.style)
|
||||
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
|
||||
|
||||
// Writes the colorized cell to stdout
|
||||
write!(writter, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
|
||||
write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,8 +167,8 @@ where
|
|||
/// Defines the row template.
|
||||
fn row(&self) -> Row;
|
||||
|
||||
/// Writes the table to the writter.
|
||||
fn print(writter: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
|
||||
/// Writes the table to the writer.
|
||||
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
|
||||
let is_format_flowed = matches!(opts.format, Format::Flowed);
|
||||
let max_width = match opts.format {
|
||||
Format::Fixed(width) => opts.max_width.unwrap_or(*width),
|
||||
|
@ -202,7 +202,7 @@ where
|
|||
for row in table.iter_mut() {
|
||||
let mut glue = Cell::default();
|
||||
for (i, cell) in row.0.iter_mut().enumerate() {
|
||||
glue.print(writter)?;
|
||||
glue.print(writer)?;
|
||||
|
||||
let table_is_overflowing = table_width > max_width;
|
||||
if table_is_overflowing && !is_format_flowed && cell.is_shrinkable() {
|
||||
|
@ -256,10 +256,10 @@ where
|
|||
trace!("number of spaces added to value: {}", spaces_count);
|
||||
cell.value.push_str(&" ".repeat(spaces_count));
|
||||
}
|
||||
cell.print(writter)?;
|
||||
cell.print(writer)?;
|
||||
glue = Cell::new("│").ansi_256(8);
|
||||
}
|
||||
writeln!(writter)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -272,11 +272,11 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct StringWritter {
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWritter {
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
|
@ -289,7 +289,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWritter {
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -303,7 +303,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWritter {}
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
struct Item {
|
||||
id: u16,
|
||||
|
@ -338,16 +338,16 @@ mod tests {
|
|||
}
|
||||
|
||||
macro_rules! write_items {
|
||||
($writter:expr, $($item:expr),*) => {
|
||||
Table::print($writter, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap();
|
||||
($writer:expr, $($item:expr),*) => {
|
||||
Table::print($writer, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap();
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_smaller_than_head() {
|
||||
let mut writter = StringWritter::default();
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writter,
|
||||
&mut writer,
|
||||
Item::new(1, "a", "aa"),
|
||||
Item::new(2, "b", "bb"),
|
||||
Item::new(3, "c", "cc")
|
||||
|
@ -359,14 +359,14 @@ mod tests {
|
|||
"2 │b │bb \n",
|
||||
"3 │c │cc \n",
|
||||
];
|
||||
assert_eq!(expected, writter.content);
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_bigger_than_head() {
|
||||
let mut writter = StringWritter::default();
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writter,
|
||||
&mut writer,
|
||||
Item::new(1, "a", "aa"),
|
||||
Item::new(2222, "bbbbb", "bbbbb"),
|
||||
Item::new(3, "c", "cc")
|
||||
|
@ -378,11 +378,11 @@ mod tests {
|
|||
"2222 │bbbbb │bbbbb \n",
|
||||
"3 │c │cc \n",
|
||||
];
|
||||
assert_eq!(expected, writter.content);
|
||||
assert_eq!(expected, writer.content);
|
||||
|
||||
let mut writter = StringWritter::default();
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writter,
|
||||
&mut writer,
|
||||
Item::new(1, "a", "aa"),
|
||||
Item::new(2222, "bbbbb", "bbbbb"),
|
||||
Item::new(3, "cccccc", "cc")
|
||||
|
@ -394,14 +394,14 @@ mod tests {
|
|||
"2222 │bbbbb │bbbbb \n",
|
||||
"3 │cccccc │cc \n",
|
||||
];
|
||||
assert_eq!(expected, writter.content);
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_shrink() {
|
||||
let mut writter = StringWritter::default();
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writter,
|
||||
&mut writer,
|
||||
Item::new(1, "", "desc"),
|
||||
Item::new(2, "short", "desc"),
|
||||
Item::new(3, "loooooong", "desc"),
|
||||
|
@ -423,14 +423,14 @@ mod tests {
|
|||
"7 │😍😍😍😍… │desc \n",
|
||||
"8 │!😍😍😍… │desc \n",
|
||||
];
|
||||
assert_eq!(expected, writter.content);
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_shrink_width() {
|
||||
let mut writter = StringWritter::default();
|
||||
let mut writer = StringWriter::default();
|
||||
write_items![
|
||||
&mut writter,
|
||||
&mut writer,
|
||||
Item::new(1111, "shriiiiiiiink", "desc very looong"),
|
||||
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
|
||||
];
|
||||
|
@ -440,6 +440,6 @@ mod tests {
|
|||
"1111 │shri… │desc very looong \n",
|
||||
"2222 │shri… │desc very loooooooooong \n",
|
||||
];
|
||||
assert_eq!(expected, writter.content);
|
||||
assert_eq!(expected, writer.content);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,7 @@ fn test_maildir_backend() {
|
|||
|
||||
// configure accounts
|
||||
let account_config = AccountConfig {
|
||||
mailboxes: HashMap::from_iter([
|
||||
("inbox".into(), "INBOX".into()),
|
||||
("subdir".into(), "Subdir".into()),
|
||||
]),
|
||||
mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
let mdir_config = MaildirBackendConfig {
|
||||
|
|
Loading…
Reference in a new issue