mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-16 20:57:53 +08:00
release v0.5.2 (#282)
* doc: fix blur in list msg screenshots (#181) * fix a typo in mbox arg (#245) `targetted` to `targeted` 👌🏻 * make inbox, sent and drafts folder customizable (#246) * mbox: make inbox, sent and drafts folder customizable * msg: update send handler parameters order * vim: fix extracting message ids from list (#247) The current method doesn't work because the list uses a fancy line character (`│`) as the separator, not a regular pipe character (`|`). Matching for the first number in the line instead solves the problem and will continue to work regardless of what separator is used. * add new line after printing strings (#251) * init cargo workspace (#252) * init cargo workspaces * nix: fix assets path * doc: update rtp vim plugin * vim: add error message if loading vim plugin from vim/ * init sub crates (#253) * init sub crates * doc: update readme * doc: improve main readme * doc: add links, add missing crate task * doc: update emojis * update cargo lock * implement contact completion with completefunc (#250) This allows users to define a command for contact completion with `g:himalaya_complete_contact_cmd` and trigger it with `<C-x><C-u>` when writing an email. * fix clippy lints (#255) * revert cargo workspace feature * fix nix run (#274) * replace cargo2nix by naersk * add rust-analyzer and rustfmt to nix build inputs * remove wiki from git submodules, update changelog * fix missing range when fetch fails, add more logs (#276) * add missing fix in changelog * remove blank lines and spaces from plain parts (#280) * fix watch command (#271) * remove also tabs from text parts (#280) * pin native-tls minor version (#278) * improve msg sanitization (#280) * fix mbox vim plugin telescope preview (#249) * bump version v0.5.2 * update changelog Co-authored-by: Austin Traver <austintraver@gmail.com> Co-authored-by: Jason Cox <dev@jasoncarloscox.com> Co-authored-by: Gökmen Görgen <gkmngrgn@gmail.com> Co-authored-by: Ethiraric <ethiraric@gmail.com>
This commit is contained in:
@@ -8,6 +8,10 @@ use crate::{
|
||||
output::run_cmd,
|
||||
};
|
||||
|
||||
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
|
||||
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
|
||||
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
|
||||
|
||||
/// Represent a user account.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Account {
|
||||
@@ -16,6 +20,12 @@ pub struct Account {
|
||||
pub downloads_dir: PathBuf,
|
||||
pub sig: Option<String>,
|
||||
pub default_page_size: usize,
|
||||
/// Defines the inbox folder name for this account
|
||||
pub inbox_folder: String,
|
||||
/// Defines the sent folder name for this account
|
||||
pub sent_folder: String,
|
||||
/// Defines the draft folder name for this account
|
||||
pub draft_folder: String,
|
||||
pub watch_cmds: Vec<String>,
|
||||
pub default: bool,
|
||||
pub email: String,
|
||||
@@ -41,7 +51,7 @@ impl Account {
|
||||
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
|
||||
|
||||
if name.is_empty() {
|
||||
format!("{}", self.email)
|
||||
self.email.clone()
|
||||
} else if has_special_chars {
|
||||
// so the name has special characters => Wrap it with '"'
|
||||
format!("\"{}\" <{}>", name, self.email)
|
||||
@@ -102,7 +112,7 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
|
||||
.and_then(|dir| shellexpand::full(dir).ok())
|
||||
.map(|dir| PathBuf::from(dir.to_string()))
|
||||
})
|
||||
.unwrap_or_else(|| env::temp_dir());
|
||||
.unwrap_or_else(env::temp_dir);
|
||||
|
||||
let default_page_size = account
|
||||
.default_page_size
|
||||
@@ -134,6 +144,24 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
|
||||
downloads_dir,
|
||||
sig,
|
||||
default_page_size,
|
||||
inbox_folder: account
|
||||
.inbox_folder
|
||||
.as_deref()
|
||||
.or_else(|| config.inbox_folder.as_deref())
|
||||
.unwrap_or(DEFAULT_INBOX_FOLDER)
|
||||
.to_string(),
|
||||
sent_folder: account
|
||||
.sent_folder
|
||||
.as_deref()
|
||||
.or_else(|| config.sent_folder.as_deref())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER)
|
||||
.to_string(),
|
||||
draft_folder: account
|
||||
.draft_folder
|
||||
.as_deref()
|
||||
.or_else(|| config.draft_folder.as_deref())
|
||||
.unwrap_or(DEFAULT_DRAFT_FOLDER)
|
||||
.to_string(),
|
||||
watch_cmds: account
|
||||
.watch_cmds
|
||||
.as_ref()
|
||||
@@ -142,12 +170,14 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
|
||||
.to_owned(),
|
||||
default: account.default.unwrap_or(false),
|
||||
email: account.email.to_owned(),
|
||||
|
||||
imap_host: account.imap_host.to_owned(),
|
||||
imap_port: account.imap_port,
|
||||
imap_starttls: account.imap_starttls.unwrap_or_default(),
|
||||
imap_insecure: account.imap_insecure.unwrap_or_default(),
|
||||
imap_login: account.imap_login.to_owned(),
|
||||
imap_passwd_cmd: account.imap_passwd_cmd.to_owned(),
|
||||
|
||||
smtp_host: account.smtp_host.to_owned(),
|
||||
smtp_port: account.smtp_port,
|
||||
smtp_starttls: account.smtp_starttls.unwrap_or_default(),
|
||||
|
||||
+24
-27
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Context, Error, Result};
|
||||
use log::{debug, trace};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf, thread};
|
||||
use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf};
|
||||
use toml;
|
||||
|
||||
use crate::output::run_cmd;
|
||||
@@ -13,18 +13,27 @@ pub const DEFAULT_SIG_DELIM: &str = "-- \n";
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Config {
|
||||
/// Define the full display name of the user.
|
||||
/// Defines the full display name of the user.
|
||||
pub name: String,
|
||||
/// Define the downloads directory (eg. for attachments).
|
||||
/// Defines the downloads directory (eg. for attachments).
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
/// Override the default signature delimiter "`--\n `".
|
||||
/// Overrides the default signature delimiter "`--\n `".
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Define the signature.
|
||||
/// Defines the signature.
|
||||
pub signature: Option<String>,
|
||||
/// Define the default page size for listings.
|
||||
/// Defines the default page size for listings.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Defines the inbox folder name.
|
||||
pub inbox_folder: Option<String>,
|
||||
/// Defines the sent folder name.
|
||||
pub sent_folder: Option<String>,
|
||||
/// Defines the draft folder name.
|
||||
pub draft_folder: Option<String>,
|
||||
/// Defines the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Defines the watch commands.
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: ConfigAccountsMap,
|
||||
}
|
||||
@@ -41,15 +50,23 @@ pub struct ConfigAccountEntry {
|
||||
pub signature_delimiter: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Defines a specific inbox folder name for this account.
|
||||
pub inbox_folder: Option<String>,
|
||||
/// Defines a specific sent folder name for this account.
|
||||
pub sent_folder: Option<String>,
|
||||
/// Defines a specific draft folder name for this account.
|
||||
pub draft_folder: Option<String>,
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
pub default: Option<bool>,
|
||||
pub email: String,
|
||||
|
||||
pub imap_host: String,
|
||||
pub imap_port: u16,
|
||||
pub imap_starttls: Option<bool>,
|
||||
pub imap_insecure: Option<bool>,
|
||||
pub imap_login: String,
|
||||
pub imap_passwd_cmd: String,
|
||||
|
||||
pub smtp_host: String,
|
||||
pub smtp_port: u16,
|
||||
pub smtp_starttls: Option<bool>,
|
||||
@@ -118,28 +135,8 @@ impl Config {
|
||||
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
|
||||
.unwrap_or(default_cmd);
|
||||
|
||||
debug!("run command: {}", cmd);
|
||||
run_cmd(&cmd).context("cannot run notify cmd")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _exec_watch_cmds(&self, account: &ConfigAccountEntry) -> Result<()> {
|
||||
let cmds = account
|
||||
.watch_cmds
|
||||
.as_ref()
|
||||
.or_else(|| self.watch_cmds.as_ref())
|
||||
.map(|cmds| cmds.to_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
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);
|
||||
debug!("{:?}", res);
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{config::Config, domain::imap::ImapServiceInterface};
|
||||
use crate::{
|
||||
config::{Account, Config},
|
||||
domain::imap::ImapServiceInterface,
|
||||
};
|
||||
|
||||
/// Notify handler.
|
||||
pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
|
||||
@@ -12,13 +15,14 @@ pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
|
||||
config: &Config,
|
||||
imap: &mut ImapService,
|
||||
) -> Result<()> {
|
||||
imap.notify(&config, keepalive)
|
||||
imap.notify(config, keepalive)
|
||||
}
|
||||
|
||||
/// Watch handler.
|
||||
pub fn watch<'a, ImapService: ImapServiceInterface<'a>>(
|
||||
keepalive: u64,
|
||||
account: &Account,
|
||||
imap: &mut ImapService,
|
||||
) -> Result<()> {
|
||||
imap.watch(keepalive)
|
||||
imap.watch(account, keepalive)
|
||||
}
|
||||
|
||||
@@ -8,20 +8,21 @@ use native_tls::{TlsConnector, TlsStream};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::{TryFrom, TryInto},
|
||||
iter::FromIterator,
|
||||
net::TcpStream,
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{Account, Config},
|
||||
domain::{Envelope, Envelopes, Flags, Mbox, Mboxes, Msg, RawEnvelopes, RawMboxes},
|
||||
output::run_cmd,
|
||||
};
|
||||
|
||||
type ImapSession = imap::Session<TlsStream<TcpStream>>;
|
||||
|
||||
pub trait ImapServiceInterface<'a> {
|
||||
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>;
|
||||
fn watch(&mut self, keepalive: u64) -> Result<()>;
|
||||
fn watch(&mut self, account: &Account, keepalive: u64) -> Result<()>;
|
||||
fn fetch_mboxes(&'a mut self) -> Result<Mboxes>;
|
||||
fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>;
|
||||
fn fetch_envelopes_with(
|
||||
@@ -58,7 +59,7 @@ pub struct ImapService<'a> {
|
||||
|
||||
impl<'a> ImapService<'a> {
|
||||
fn sess(&mut self) -> Result<&mut ImapSession> {
|
||||
if let None = self.sess {
|
||||
if self.sess.is_none() {
|
||||
debug!("create TLS builder");
|
||||
debug!("insecure: {}", self.account.imap_insecure);
|
||||
let builder = TlsConnector::builder()
|
||||
@@ -122,12 +123,17 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
}
|
||||
|
||||
fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> {
|
||||
debug!("fetch envelopes");
|
||||
debug!("page size: {:?}", page_size);
|
||||
debug!("page: {:?}", page);
|
||||
|
||||
let mbox = self.mbox.to_owned();
|
||||
let last_seq = self
|
||||
.sess()?
|
||||
.select(&mbox.name)
|
||||
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?
|
||||
.exists as i64;
|
||||
debug!("last sequence number: {:?}", last_seq);
|
||||
|
||||
if last_seq == 0 {
|
||||
return Ok(Envelopes::default());
|
||||
@@ -142,13 +148,14 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
} else {
|
||||
String::from("1:*")
|
||||
};
|
||||
debug!("range: {:?}", range);
|
||||
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(r#"cannot fetch messages within range "{}""#)?;
|
||||
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(format!(r#"cannot fetch messages within range "{}""#, range))?;
|
||||
self._raw_msgs_cache = Some(fetches);
|
||||
Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?)
|
||||
Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn fetch_envelopes_with(
|
||||
@@ -186,7 +193,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
|
||||
.context(r#"cannot fetch messages within range "{}""#)?;
|
||||
self._raw_msgs_cache = Some(fetches);
|
||||
Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?)
|
||||
Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// Find a message by sequence number.
|
||||
@@ -201,9 +208,9 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
.context(r#"cannot fetch messages "{}""#)?;
|
||||
let fetch = fetches
|
||||
.first()
|
||||
.ok_or(anyhow!(r#"cannot find message "{}"#, seq))?;
|
||||
.ok_or_else(|| anyhow!(r#"cannot find message "{}"#, seq))?;
|
||||
|
||||
Ok(Msg::try_from(fetch)?)
|
||||
Msg::try_from(fetch)
|
||||
}
|
||||
|
||||
fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>> {
|
||||
@@ -217,14 +224,14 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
.context(r#"cannot fetch raw messages "{}""#)?;
|
||||
let fetch = fetches
|
||||
.first()
|
||||
.ok_or(anyhow!(r#"cannot find raw message "{}"#, seq))?;
|
||||
.ok_or_else(|| anyhow!(r#"cannot find raw message "{}"#, seq))?;
|
||||
|
||||
Ok(fetch.body().map(Vec::from).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()> {
|
||||
self.sess()?
|
||||
.append(&mbox.name, &msg)
|
||||
.append(&mbox.name, msg)
|
||||
.flags(flags.0)
|
||||
.finish()
|
||||
.context(format!(r#"cannot append message to "{}""#, mbox.name))?;
|
||||
@@ -242,16 +249,21 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
}
|
||||
|
||||
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()> {
|
||||
debug!("notify");
|
||||
|
||||
let mbox = self.mbox.to_owned();
|
||||
|
||||
debug!("examine mailbox: {}", mbox.name);
|
||||
debug!("examine mailbox {:?}", mbox);
|
||||
self.sess()?
|
||||
.examine(&mbox.name)
|
||||
.context(format!("cannot examine mailbox `{}`", &self.mbox.name))?;
|
||||
.context(format!("cannot examine mailbox {}", self.mbox.name))?;
|
||||
|
||||
debug!("init messages hashset");
|
||||
let mut msgs_set: HashSet<u32> =
|
||||
HashSet::from_iter(self.search_new_msgs()?.iter().cloned());
|
||||
let mut msgs_set: HashSet<u32> = self
|
||||
.search_new_msgs()?
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
trace!("messages hashset: {:?}", msgs_set);
|
||||
|
||||
loop {
|
||||
@@ -271,7 +283,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
let uids: Vec<u32> = self
|
||||
.search_new_msgs()?
|
||||
.into_iter()
|
||||
.filter(|uid| msgs_set.get(&uid).is_none())
|
||||
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
|
||||
.collect();
|
||||
debug!("found {} new messages not in hashset", uids.len());
|
||||
trace!("messages hashet: {:?}", msgs_set);
|
||||
@@ -309,7 +321,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn watch(&mut self, keepalive: u64) -> Result<()> {
|
||||
fn watch(&mut self, account: &Account, keepalive: u64) -> Result<()> {
|
||||
debug!("examine mailbox: {}", &self.mbox.name);
|
||||
let mbox = self.mbox.to_owned();
|
||||
|
||||
@@ -330,8 +342,17 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
||||
})
|
||||
})
|
||||
.context("cannot start the idle mode")?;
|
||||
// FIXME
|
||||
// ctx.config.exec_watch_cmds(&ctx.account)?;
|
||||
|
||||
let cmds = account.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);
|
||||
debug!("{:?}", res);
|
||||
})
|
||||
});
|
||||
|
||||
debug!("end loop");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,12 @@ pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
.long("mailbox")
|
||||
.help("Specifies the source mailbox")
|
||||
.value_name("SOURCE")
|
||||
.default_value("INBOX")
|
||||
}
|
||||
|
||||
/// Defines the target mailbox argument.
|
||||
pub fn target_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mbox-target")
|
||||
.help("Specifies the targetted mailbox")
|
||||
.help("Specifies the targeted mailbox")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
@@ -104,7 +103,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(Some("INBOX"), app.value_of("mbox-source"));
|
||||
assert_eq!(None, app.value_of("mbox-source"));
|
||||
|
||||
let app = get_matches_from!["-m", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
|
||||
|
||||
@@ -28,7 +28,7 @@ mod tests {
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
config::{Account, Config},
|
||||
domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg},
|
||||
output::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
@@ -117,7 +117,7 @@ mod tests {
|
||||
fn notify(&mut self, _: &Config, _: u64) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn watch(&mut self, _: u64) -> Result<()> {
|
||||
fn watch(&mut self, _: &Account, _: u64) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
|
||||
|
||||
@@ -32,7 +32,7 @@ impl<'a> Deref for Mboxes<'a> {
|
||||
impl<'a> PrintTable for Mboxes<'a> {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, &self, opts)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
|
||||
fn try_from(fetch: &'a RawEnvelope) -> Result<Envelope> {
|
||||
let envelope = fetch
|
||||
.envelope()
|
||||
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?;
|
||||
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
|
||||
|
||||
// Get the sequence number
|
||||
let id = fetch.message;
|
||||
@@ -57,7 +57,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
|
||||
fetch.message
|
||||
))
|
||||
})
|
||||
.unwrap_or(Ok(String::default()))?
|
||||
.unwrap_or_else(|| Ok(String::default()))?
|
||||
.into();
|
||||
|
||||
// Get the sender
|
||||
@@ -66,7 +66,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.get(0))
|
||||
.or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0)))
|
||||
.ok_or(anyhow!("cannot get sender of message {}", fetch.message))?;
|
||||
.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 {}",
|
||||
@@ -76,10 +76,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
|
||||
let mbox = sender
|
||||
.mailbox
|
||||
.as_ref()
|
||||
.ok_or(anyhow!(
|
||||
"cannot get sender's mailbox of message {}",
|
||||
fetch.message
|
||||
))
|
||||
.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 {}",
|
||||
@@ -89,10 +86,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
|
||||
let host = sender
|
||||
.host
|
||||
.as_ref()
|
||||
.ok_or(anyhow!(
|
||||
"cannot get sender's host of message {}",
|
||||
fetch.message
|
||||
))
|
||||
.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 {}",
|
||||
@@ -133,11 +127,7 @@ impl<'a> Table for Envelope<'a> {
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = self
|
||||
.date
|
||||
.as_ref()
|
||||
.map(|date| date.as_str())
|
||||
.unwrap_or_default();
|
||||
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())
|
||||
|
||||
@@ -39,7 +39,7 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
|
||||
impl<'a> PrintTable for Envelopes<'a> {
|
||||
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writter)?;
|
||||
Table::print(writter, &self, opts)?;
|
||||
Table::print(writter, self, opts)?;
|
||||
writeln!(writter)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -126,16 +126,14 @@ impl<'a> From<Vec<&'a str>> for Flags {
|
||||
let mut map: HashSet<Flag<'static>> = HashSet::new();
|
||||
|
||||
for f in flags {
|
||||
match f {
|
||||
"Answered" | _ if f.eq_ignore_ascii_case("answered") => map.insert(Flag::Answered),
|
||||
"Deleted" | _ if f.eq_ignore_ascii_case("deleted") => map.insert(Flag::Deleted),
|
||||
"Draft" | _ if f.eq_ignore_ascii_case("draft") => map.insert(Flag::Draft),
|
||||
"Flagged" | _ if f.eq_ignore_ascii_case("flagged") => map.insert(Flag::Flagged),
|
||||
"MayCreate" | _ if f.eq_ignore_ascii_case("maycreate") => {
|
||||
map.insert(Flag::MayCreate)
|
||||
}
|
||||
"Recent" | _ if f.eq_ignore_ascii_case("recent") => map.insert(Flag::Recent),
|
||||
"Seen" | _ if f.eq_ignore_ascii_case("seen") => map.insert(Flag::Seen),
|
||||
match f.to_lowercase().as_str() {
|
||||
"answered" => map.insert(Flag::Answered),
|
||||
"deleted" => map.insert(Flag::Deleted),
|
||||
"draft" => map.insert(Flag::Draft),
|
||||
"flagged" => map.insert(Flag::Flagged),
|
||||
"maycreate" => map.insert(Flag::MayCreate),
|
||||
"recent" => map.insert(Flag::Recent),
|
||||
"seen" => map.insert(Flag::Seen),
|
||||
custom => map.insert(Flag::Custom(Cow::Owned(custom.into()))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,11 +200,11 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("template") {
|
||||
return Ok(Some(Command::Tpl(tpl_arg::matches(&m)?)));
|
||||
return Ok(Some(Command::Tpl(tpl_arg::matches(m)?)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("flag") {
|
||||
return Ok(Some(Command::Flag(flag_arg::matches(&m)?)));
|
||||
return Ok(Some(Command::Flag(flag_arg::matches(m)?)));
|
||||
}
|
||||
|
||||
debug!("default list command matched");
|
||||
|
||||
@@ -100,21 +100,43 @@ impl Msg {
|
||||
.tags(HashSet::default())
|
||||
.clean(&html)
|
||||
.to_string();
|
||||
// Replace ` ` by regular space
|
||||
let sanitized_html = Regex::new(r" ")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, " ")
|
||||
.to_string();
|
||||
// Merge new line chars
|
||||
let sanitized_html = Regex::new(r"(\r?\n ?){2,}")
|
||||
let sanitized_html = Regex::new(r"(\r?\n\s*){2,}")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, "\n\n")
|
||||
.to_string();
|
||||
// Replace tabulations and &npsp; by spaces
|
||||
let sanitized_html = Regex::new(r"(\t| )")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, " ")
|
||||
.to_string();
|
||||
// Merge spaces
|
||||
let sanitized_html = Regex::new(r" {2,}")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, " ")
|
||||
.to_string();
|
||||
// Decode HTML entities
|
||||
let sanitized_html = html_escape::decode_html_entities(&sanitized_html).to_string();
|
||||
|
||||
sanitized_html
|
||||
} else {
|
||||
plain
|
||||
// Merge new line chars
|
||||
let sanitized_plain = Regex::new(r"(\r?\n\s*){2,}")
|
||||
.unwrap()
|
||||
.replace_all(&plain, "\n\n")
|
||||
.to_string();
|
||||
// Replace tabulations by spaces
|
||||
let sanitized_plain = Regex::new(r"\t")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_plain, " ")
|
||||
.to_string();
|
||||
// Merge spaces
|
||||
let sanitized_plain = Regex::new(r" {2,}")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_plain, " ")
|
||||
.to_string();
|
||||
|
||||
sanitized_plain
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,14 +216,18 @@ impl Msg {
|
||||
.date
|
||||
.as_ref()
|
||||
.map(|date| date.format("%d %b %Y, at %H:%M").to_string())
|
||||
.unwrap_or("unknown date".into());
|
||||
.unwrap_or_else(|| "unknown date".into());
|
||||
let sender = self
|
||||
.reply_to
|
||||
.as_ref()
|
||||
.or(self.from.as_ref())
|
||||
.or_else(|| self.from.as_ref())
|
||||
.and_then(|addrs| addrs.first())
|
||||
.map(|addr| addr.name.to_owned().unwrap_or(addr.email.to_string()))
|
||||
.unwrap_or("unknown sender".into());
|
||||
.map(|addr| {
|
||||
addr.name
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| addr.email.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "unknown sender".into());
|
||||
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
|
||||
|
||||
let mut glue = "";
|
||||
@@ -210,8 +236,8 @@ impl Msg {
|
||||
break;
|
||||
}
|
||||
content.push_str(glue);
|
||||
content.push_str(">");
|
||||
content.push_str(if line.starts_with(">") { "" } else { " " });
|
||||
content.push('>');
|
||||
content.push_str(if line.starts_with('>') { "" } else { " " });
|
||||
content.push_str(line);
|
||||
glue = "\n";
|
||||
}
|
||||
@@ -239,7 +265,7 @@ impl Msg {
|
||||
self.in_reply_to = None;
|
||||
|
||||
// From
|
||||
self.from = Some(vec![account_addr.to_owned()]);
|
||||
self.from = Some(vec![account_addr]);
|
||||
|
||||
// To
|
||||
self.to = Some(vec![]);
|
||||
@@ -270,7 +296,7 @@ impl Msg {
|
||||
content.push_str(&addr.to_string());
|
||||
glue = ", ";
|
||||
}
|
||||
content.push_str("\n");
|
||||
content.push('\n');
|
||||
}
|
||||
if let Some(addrs) = prev_to.as_ref() {
|
||||
content.push_str("To: ");
|
||||
@@ -280,9 +306,9 @@ impl Msg {
|
||||
content.push_str(&addr.to_string());
|
||||
glue = ", ";
|
||||
}
|
||||
content.push_str("\n");
|
||||
content.push('\n');
|
||||
}
|
||||
content.push_str("\n");
|
||||
content.push('\n');
|
||||
content.push_str(&self.fold_text_parts("plain"));
|
||||
self.parts
|
||||
.replace_text_plain_parts_with(TextPlainPart { content });
|
||||
@@ -337,7 +363,7 @@ impl Msg {
|
||||
loop {
|
||||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
let mbox = Mbox::new("Sent");
|
||||
let mbox = Mbox::new(&account.sent_folder);
|
||||
let sent_msg = smtp.send_msg(&self)?;
|
||||
let flags = Flags::try_from(vec![Flag::Seen])?;
|
||||
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
|
||||
@@ -354,12 +380,15 @@ impl Msg {
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::RemoteDraft) => {
|
||||
let mbox = Mbox::new("Drafts");
|
||||
let mbox = Mbox::new(&account.draft_folder);
|
||||
let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?;
|
||||
let tpl = self.to_tpl(TplOverride::default(), account);
|
||||
imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
printer.print("Message successfully saved to Drafts")?;
|
||||
printer.print(format!(
|
||||
"Message successfully saved to {}",
|
||||
account.draft_folder
|
||||
))?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Discard) => {
|
||||
@@ -383,7 +412,7 @@ impl Msg {
|
||||
let path = PathBuf::from(path.to_string());
|
||||
let filename: String = path
|
||||
.file_name()
|
||||
.ok_or(anyhow!("cannot get file name of attachment {:?}", path))?
|
||||
.ok_or_else(|| anyhow!("cannot get file name of attachment {:?}", path))?
|
||||
.to_string_lossy()
|
||||
.into();
|
||||
let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?;
|
||||
@@ -424,17 +453,11 @@ impl Msg {
|
||||
match part {
|
||||
Part::Binary(_) => self.parts.push(part),
|
||||
Part::TextPlain(_) => {
|
||||
self.parts.retain(|p| match p {
|
||||
Part::TextPlain(_) => false,
|
||||
_ => true,
|
||||
});
|
||||
self.parts.retain(|p| !matches!(p, Part::TextPlain(_)));
|
||||
self.parts.push(part);
|
||||
}
|
||||
Part::TextHtml(_) => {
|
||||
self.parts.retain(|p| match p {
|
||||
Part::TextHtml(_) => false,
|
||||
_ => true,
|
||||
});
|
||||
self.parts.retain(|p| !matches!(p, Part::TextHtml(_)));
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
@@ -504,7 +527,7 @@ impl Msg {
|
||||
));
|
||||
|
||||
// Headers <=> body separator
|
||||
tpl.push_str("\n");
|
||||
tpl.push('\n');
|
||||
|
||||
// Body
|
||||
if let Some(body) = opts.body {
|
||||
@@ -522,7 +545,7 @@ impl Msg {
|
||||
tpl.push_str(sig);
|
||||
}
|
||||
|
||||
tpl.push_str("\n");
|
||||
tpl.push('\n');
|
||||
|
||||
trace!("template: {:#?}", tpl);
|
||||
tpl
|
||||
@@ -539,49 +562,45 @@ impl Msg {
|
||||
let val = String::from_utf8(header.get_value_raw().to_vec())
|
||||
.map(|val| val.trim().to_string())?;
|
||||
|
||||
match key.as_str() {
|
||||
"Message-Id" | _ if key.eq_ignore_ascii_case("message-id") => {
|
||||
msg.message_id = Some(val.to_owned())
|
||||
}
|
||||
"From" | _ if key.eq_ignore_ascii_case("from") => {
|
||||
match key.to_lowercase().as_str() {
|
||||
"message-id" => msg.message_id = Some(val.to_owned()),
|
||||
"from" => {
|
||||
msg.from = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"To" | _ if key.eq_ignore_ascii_case("to") => {
|
||||
"to" => {
|
||||
msg.to = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"Reply-To" | _ if key.eq_ignore_ascii_case("reply-to") => {
|
||||
"reply-to" => {
|
||||
msg.reply_to = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"In-Reply-To" | _ if key.eq_ignore_ascii_case("in-reply-to") => {
|
||||
msg.in_reply_to = Some(val.to_owned())
|
||||
}
|
||||
"Cc" | _ if key.eq_ignore_ascii_case("cc") => {
|
||||
"in-reply-to" => msg.in_reply_to = Some(val.to_owned()),
|
||||
"cc" => {
|
||||
msg.cc = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"Bcc" | _ if key.eq_ignore_ascii_case("bcc") => {
|
||||
"bcc" => {
|
||||
msg.bcc = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"Subject" | _ if key.eq_ignore_ascii_case("subject") => {
|
||||
"subject" => {
|
||||
msg.subject = val;
|
||||
}
|
||||
_ => (),
|
||||
@@ -693,7 +712,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
||||
fn try_from(fetch: &'a imap::types::Fetch) -> Result<Msg> {
|
||||
let envelope = fetch
|
||||
.envelope()
|
||||
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?;
|
||||
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
|
||||
|
||||
// Get the sequence number
|
||||
let id = fetch.message;
|
||||
@@ -711,13 +730,13 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
||||
fetch.message
|
||||
))
|
||||
})
|
||||
.unwrap_or(Ok(String::default()))?;
|
||||
.unwrap_or_else(|| Ok(String::default()))?;
|
||||
|
||||
// Get the sender(s) address(es)
|
||||
let from = match envelope
|
||||
.sender
|
||||
.as_ref()
|
||||
.or_else(|| envelope.from.as_ref())
|
||||
.as_deref()
|
||||
.or_else(|| envelope.from.as_deref())
|
||||
.map(parse_addrs)
|
||||
{
|
||||
Some(addrs) => Some(addrs?),
|
||||
@@ -770,7 +789,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
||||
&mailparse::parse_mail(
|
||||
fetch
|
||||
.body()
|
||||
.ok_or(anyhow!("cannot get body of message {}", id))?,
|
||||
.ok_or_else(|| anyhow!("cannot get body of message {}", id))?,
|
||||
)
|
||||
.context(format!("cannot parse body of message {}", id))?,
|
||||
);
|
||||
@@ -779,13 +798,13 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
||||
id,
|
||||
flags,
|
||||
subject,
|
||||
message_id,
|
||||
from,
|
||||
reply_to,
|
||||
in_reply_to,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
in_reply_to,
|
||||
message_id,
|
||||
date,
|
||||
parts,
|
||||
})
|
||||
@@ -799,20 +818,20 @@ pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
|
||||
.map(|name| {
|
||||
rfc2047_decoder::decode(&name.to_vec())
|
||||
.context("cannot decode address name")
|
||||
.map(|name| Some(name))
|
||||
.map(Some)
|
||||
})
|
||||
.unwrap_or(Ok(None))?;
|
||||
let mbox = addr
|
||||
.mailbox
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("cannot get address mailbox"))
|
||||
.ok_or_else(|| anyhow!("cannot get address mailbox"))
|
||||
.and_then(|mbox| {
|
||||
rfc2047_decoder::decode(&mbox.to_vec()).context("cannot decode address mailbox")
|
||||
})?;
|
||||
let host = addr
|
||||
.host
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("cannot get address host"))
|
||||
.ok_or_else(|| anyhow!("cannot get address host"))
|
||||
.and_then(|host| {
|
||||
rfc2047_decoder::decode(&host.to_vec()).context("cannot decode address host")
|
||||
})?;
|
||||
@@ -820,7 +839,7 @@ pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
|
||||
Ok(Addr::new(name, lettre::Address::new(mbox, host)?))
|
||||
}
|
||||
|
||||
pub fn parse_addrs(addrs: &Vec<imap_proto::Address>) -> Result<Vec<Addr>> {
|
||||
pub fn parse_addrs(addrs: &[imap_proto::Address]) -> Result<Vec<Addr>> {
|
||||
let mut parsed_addrs = vec![];
|
||||
for addr in addrs {
|
||||
parsed_addrs
|
||||
@@ -830,7 +849,7 @@ pub fn parse_addrs(addrs: &Vec<imap_proto::Address>) -> Result<Vec<Addr>> {
|
||||
}
|
||||
|
||||
pub fn parse_some_addrs(addrs: &Option<Vec<imap_proto::Address>>) -> Result<Option<Vec<Addr>>> {
|
||||
Ok(match addrs.as_ref().map(parse_addrs) {
|
||||
Ok(match addrs.as_deref().map(parse_addrs) {
|
||||
Some(addrs) => Some(addrs?),
|
||||
None => None,
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::{
|
||||
mbox::Mbox,
|
||||
msg::{Flags, Msg, Part, TextPlainPart},
|
||||
smtp::SmtpServiceInterface,
|
||||
Parts,
|
||||
},
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
};
|
||||
@@ -32,7 +33,7 @@ pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterfac
|
||||
printer: &mut Printer,
|
||||
imap: &mut ImapService,
|
||||
) -> Result<()> {
|
||||
let attachments = imap.find_msg(&seq)?.attachments();
|
||||
let attachments = imap.find_msg(seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
debug!(
|
||||
r#"{} attachment(s) found for message "{}""#,
|
||||
@@ -60,7 +61,7 @@ pub fn copy<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
||||
imap: &mut ImapService,
|
||||
) -> Result<()> {
|
||||
let mbox = Mbox::new(mbox);
|
||||
let msg = imap.find_raw_msg(&seq)?;
|
||||
let msg = imap.find_raw_msg(seq)?;
|
||||
let flags = Flags::try_from(vec![Flag::Seen])?;
|
||||
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
|
||||
printer.print(format!(
|
||||
@@ -135,7 +136,7 @@ pub fn mailto<
|
||||
) -> Result<()> {
|
||||
let to: Vec<lettre::message::Mailbox> = url
|
||||
.path()
|
||||
.split(";")
|
||||
.split(';')
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let mut cc = Vec::new();
|
||||
@@ -161,16 +162,18 @@ pub fn mailto<
|
||||
}
|
||||
}
|
||||
|
||||
let mut msg = Msg::default();
|
||||
let msg = Msg {
|
||||
from: Some(vec![account.address().parse()?]),
|
||||
to: if to.is_empty() { None } else { Some(to) },
|
||||
cc: if cc.is_empty() { None } else { Some(cc) },
|
||||
bcc: if bcc.is_empty() { None } else { Some(bcc) },
|
||||
subject: subject.into(),
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: body.into(),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
|
||||
msg.from = Some(vec![account.address().parse()?]);
|
||||
msg.to = if to.is_empty() { None } else { Some(to) };
|
||||
msg.cc = if cc.is_empty() { None } else { Some(cc) };
|
||||
msg.bcc = if bcc.is_empty() { None } else { Some(bcc) };
|
||||
msg.subject = subject.into();
|
||||
msg.parts.push(Part::TextPlain(TextPlainPart {
|
||||
content: body.into(),
|
||||
}));
|
||||
msg.edit_with_editor(account, printer, imap, smtp)
|
||||
}
|
||||
|
||||
@@ -185,7 +188,7 @@ pub fn move_<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>
|
||||
) -> Result<()> {
|
||||
// Copy the message to targetted mailbox
|
||||
let mbox = Mbox::new(mbox);
|
||||
let msg = imap.find_raw_msg(&seq)?;
|
||||
let msg = imap.find_raw_msg(seq)?;
|
||||
let flags = Flags::try_from(vec![Flag::Seen])?;
|
||||
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
|
||||
|
||||
@@ -210,9 +213,9 @@ pub fn read<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
||||
) -> Result<()> {
|
||||
let msg = if raw {
|
||||
// Emails don't always have valid utf8. Using "lossy" to display what we can.
|
||||
String::from_utf8_lossy(&imap.find_raw_msg(&seq)?).into_owned()
|
||||
String::from_utf8_lossy(&imap.find_raw_msg(seq)?).into_owned()
|
||||
} else {
|
||||
imap.find_msg(&seq)?.fold_text_parts(text_mime)
|
||||
imap.find_msg(seq)?.fold_text_parts(text_mime)
|
||||
};
|
||||
|
||||
printer.print(msg)
|
||||
@@ -254,8 +257,7 @@ pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(|ln| ln.ok())
|
||||
.map(|ln| ln.to_string())
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
@@ -290,6 +292,7 @@ pub fn send<
|
||||
SmtpService: SmtpServiceInterface,
|
||||
>(
|
||||
raw_msg: &str,
|
||||
account: &Account,
|
||||
printer: &mut Printer,
|
||||
imap: &mut ImapService,
|
||||
smtp: &mut SmtpService,
|
||||
@@ -300,19 +303,18 @@ pub fn send<
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(|ln| ln.ok())
|
||||
.map(|ln| ln.to_string())
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
|
||||
let msg = Msg::from_tpl(&raw_msg.to_string())?;
|
||||
let msg = Msg::from_tpl(&raw_msg)?;
|
||||
let envelope: lettre::address::Envelope = msg.try_into()?;
|
||||
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
|
||||
debug!("message sent!");
|
||||
|
||||
// Save message to sent folder
|
||||
let mbox = Mbox::new("Sent");
|
||||
let mbox = Mbox::new(&account.sent_folder);
|
||||
let flags = Flags::try_from(vec![Flag::Seen])?;
|
||||
imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags)
|
||||
}
|
||||
|
||||
@@ -39,24 +39,12 @@ pub struct Parts(pub Vec<Part>);
|
||||
|
||||
impl Parts {
|
||||
pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) {
|
||||
self.retain(|part| {
|
||||
if let Part::TextPlain(_) = part {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
self.retain(|part| !matches!(part, Part::TextPlain(_)));
|
||||
self.push(Part::TextPlain(part));
|
||||
}
|
||||
|
||||
pub fn replace_text_html_parts_with(&mut self, part: TextHtmlPart) {
|
||||
self.retain(|part| {
|
||||
if let Part::TextHtml(_) = part {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
self.retain(|part| !matches!(part, Part::TextHtml(_)));
|
||||
self.push(Part::TextHtml(part));
|
||||
}
|
||||
}
|
||||
@@ -92,7 +80,7 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
|
||||
.params
|
||||
.get("filename")
|
||||
.map(String::from)
|
||||
.unwrap_or(String::from("noname"));
|
||||
.unwrap_or_else(|| String::from("noname"));
|
||||
let content = part.get_body_raw().unwrap_or_default();
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
parts.push(Part::Binary(BinaryPart {
|
||||
@@ -103,16 +91,14 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
|
||||
}
|
||||
// TODO: manage other use cases
|
||||
_ => {
|
||||
part.get_headers()
|
||||
.get_first_value("content-type")
|
||||
.map(|ctype| {
|
||||
let content = part.get_body().unwrap_or_default();
|
||||
if ctype.starts_with("text/plain") {
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else if ctype.starts_with("text/html") {
|
||||
parts.push(Part::TextHtml(TextHtmlPart { content }))
|
||||
}
|
||||
});
|
||||
if let Some(ctype) = part.get_headers().get_first_value("content-type") {
|
||||
let content = part.get_body().unwrap_or_default();
|
||||
if ctype.starts_with("text/plain") {
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else if ctype.starts_with("text/html") {
|
||||
parts.push(Part::TextHtml(TextHtmlPart { content }))
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
} else {
|
||||
|
||||
+5
-6
@@ -1,6 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use clap;
|
||||
use env_logger;
|
||||
use output::StdoutPrinter;
|
||||
use std::{convert::TryFrom, env};
|
||||
use url::Url;
|
||||
@@ -36,6 +34,7 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||
.subcommands(msg_arg::subcmds())
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
fn main() -> Result<()> {
|
||||
// Init env logger
|
||||
env_logger::init_from_env(
|
||||
@@ -45,9 +44,9 @@ fn main() -> Result<()> {
|
||||
// Check mailto command BEFORE app initialization.
|
||||
let raw_args: Vec<String> = env::args().collect();
|
||||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
let mbox = Mbox::new("INBOX");
|
||||
let config = Config::try_from(None)?;
|
||||
let account = Account::try_from((&config, None))?;
|
||||
let mbox = Mbox::new(&account.inbox_folder);
|
||||
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
|
||||
let url = Url::parse(&raw_args[1])?;
|
||||
let mut imap = ImapService::from((&account, &mbox));
|
||||
@@ -68,9 +67,9 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// Init entities and services.
|
||||
let mbox = Mbox::new(m.value_of("mbox-source").unwrap());
|
||||
let config = Config::try_from(m.value_of("config"))?;
|
||||
let account = Account::try_from((&config, m.value_of("account")))?;
|
||||
let mbox = Mbox::new(m.value_of("mbox-source").unwrap_or(&account.inbox_folder));
|
||||
let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
|
||||
let mut imap = ImapService::from((&account, &mbox));
|
||||
let mut smtp = SmtpService::from(&account);
|
||||
@@ -81,7 +80,7 @@ fn main() -> Result<()> {
|
||||
return imap_handler::notify(keepalive, &config, &mut imap);
|
||||
}
|
||||
Some(imap_arg::Command::Watch(keepalive)) => {
|
||||
return imap_handler::watch(keepalive, &mut imap);
|
||||
return imap_handler::watch(keepalive, &account, &mut imap);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -150,7 +149,7 @@ fn main() -> Result<()> {
|
||||
);
|
||||
}
|
||||
Some(msg_arg::Command::Send(raw_msg)) => {
|
||||
return msg_handler::send(raw_msg, &mut printer, &mut imap, &mut smtp);
|
||||
return msg_handler::send(raw_msg, &account, &mut printer, &mut imap, &mut smtp);
|
||||
}
|
||||
Some(msg_arg::Command::Write(atts)) => {
|
||||
return msg_handler::write(atts, &account, &mut printer, &mut imap, &mut smtp);
|
||||
|
||||
@@ -36,9 +36,9 @@ impl TryFrom<Option<&str>> for OutputFmt {
|
||||
|
||||
impl Display for OutputFmt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let fmt = match self {
|
||||
&OutputFmt::Json => "JSON",
|
||||
&OutputFmt::Plain => "Plain",
|
||||
let fmt = match *self {
|
||||
OutputFmt::Json => "JSON",
|
||||
OutputFmt::Plain => "Plain",
|
||||
};
|
||||
write!(f, "{}", fmt)
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ pub trait Print {
|
||||
|
||||
impl Print for &str {
|
||||
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
|
||||
write!(writter, "{}", self).with_context(|| {
|
||||
writeln!(writter, "{}", self).with_context(|| {
|
||||
error!(r#"cannot write string to writter: "{}""#, self);
|
||||
"cannot write string to writter"
|
||||
})
|
||||
|
||||
+1
-3
@@ -227,13 +227,11 @@ where
|
||||
trace!("number of spaces added to shrinked value: {}", spaces_count);
|
||||
value.push_str(&" ".repeat(spaces_count));
|
||||
cell.value = value;
|
||||
cell.print(writter)?;
|
||||
} else {
|
||||
trace!("cell is not overflowing");
|
||||
let spaces_count = cell_width - cell.unicode_width() + 1;
|
||||
trace!("number of spaces added to value: {}", spaces_count);
|
||||
cell.value.push_str(&" ".repeat(spaces_count));
|
||||
cell.print(writter)?;
|
||||
}
|
||||
} else {
|
||||
trace!("table is not overflowing or cell is not shrinkable");
|
||||
@@ -242,8 +240,8 @@ where
|
||||
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
|
||||
trace!("number of spaces added to value: {}", spaces_count);
|
||||
cell.value.push_str(&" ".repeat(spaces_count));
|
||||
cell.print(writter)?;
|
||||
}
|
||||
cell.print(writter)?;
|
||||
glue = Cell::new("│").ansi_256(8);
|
||||
}
|
||||
writeln!(writter)?;
|
||||
|
||||
Reference in New Issue
Block a user