release v0.5.6 (#301)

* make use of mailparse::MailAddr

* move addr logic to a dedicated file

* update changelog

* add suffix to downoalded attachments with same name (#204)

* implement sort command (#34)

* introduce backends structure (#296)

* implement backend structure poc

* improve config namings

* improve account namings and structure

* rename imap vars to backend

* maildir backend (#299)

* refactor config system, preparing maildir backend

* rename deserializable by deserialized

* wrap backend in a Box

* reword backend trait methods

* merge list envelopes functions

* remove find_raw_msg from backend trait

* remove expunge fn from backend trait

* rename add_msg from backend trait

* init maildir integration tests, start impl maildir backend fns

* implement remaining methods maildir backend, refactor trait

* improve backend trait, add copy and move fns

* remove usage of Mbox in handlers

* reorganize backends folder structure

* move mbox out of domain folder

* rename mbox entities

* improve mbox structure

* remove unused files, move smtp module

* improve envelope, impl get_envelopes for maildir

* link maildir mail entry id to envelope id

* use erased-serde to make backend get_mboxes return a trait object

* remove unused mbox files

* rename Output trait

* make get_envelopes return a trait object

* remove unused impl for imap envelope

* update backend return signature with Box

* replace impl from imap::Fetch to mailparse::ParsedMail

* split flags by backends

* remove unused flags from msg

* remove remaining flags from domain

* impl maildir copy and move, improve maildir e2e tests

* set up imap backend e2e tests

* move domain/msg to msg

* repair broken tests

* fix maildir envelopes encoding issues

* add date column to maildir envelopes

* implement maildir list pagination

* improve maildir subdir path management

* add pgp and maildir features to readme

* update changelog

* bump version v0.5.6
This commit is contained in:
Clément DOUIN 2022-02-22 16:54:39 +01:00 committed by GitHub
parent 585fa77af5
commit 158bc86cfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 3834 additions and 2696 deletions

View file

@ -14,8 +14,17 @@ jobs:
uses: actions/checkout@v2
- name: Start GreenMail testing server
run: |
docker run --rm -d -e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' -p 3025:3025 -p 3110:3110 -p 3143:3143 -p 3465:3465 -p 3993:3993 -p 3995:3995 greenmail/standalone:1.6.2
docker run \
--rm \
-d \
-e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' \
-p 3025:3025 \
-p 3110:3110 \
-p 3143:3143 \
-p 3465:3465 \
-p 3993:3993 \
-p 3995:3995 \
greenmail/standalone:1.6.2
- name: Install rust
uses: actions-rs/toolchain@v1
with:

View file

@ -7,17 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.6] - 2022-02-22
### Added
- Sort command [#34]
- Maildir support [#43]
### Fixed
- Suffix to downloaded attachments with same name [#204]
## [0.5.5] - 2022-02-08
### Added
- [Contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) [#256]
- Notify query config option [#289]
- End-to-end encryption *(EXPERIMENTAL)* [#54]
- End-to-end encryption [#54]
### Fixed
- Multiple recipients issue [#288]
- Cannot parse address [#227]
## [0.5.4] - 2022-02-05
@ -292,7 +304,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.5...HEAD
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.6...HEAD
[0.5.6]: https://github.com/soywod/himalaya/compare/v0.5.5...v0.5.6
[0.5.5]: https://github.com/soywod/himalaya/compare/v0.5.4...v0.5.5
[0.5.4]: https://github.com/soywod/himalaya/compare/v0.5.3...v0.5.4
[0.5.3]: https://github.com/soywod/himalaya/compare/v0.5.2...v0.5.3
@ -346,6 +359,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#39]: https://github.com/soywod/himalaya/issues/39
[#40]: https://github.com/soywod/himalaya/issues/40
[#41]: https://github.com/soywod/himalaya/issues/41
[#43]: https://github.com/soywod/himalaya/issues/43
[#47]: https://github.com/soywod/himalaya/issues/47
[#48]: https://github.com/soywod/himalaya/issues/48
[#50]: https://github.com/soywod/himalaya/issues/50
@ -400,9 +414,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#193]: https://github.com/soywod/himalaya/issues/193
[#196]: https://github.com/soywod/himalaya/issues/196
[#199]: https://github.com/soywod/himalaya/issues/199
[#204]: https://github.com/soywod/himalaya/issues/204
[#205]: https://github.com/soywod/himalaya/issues/205
[#215]: https://github.com/soywod/himalaya/issues/215
[#220]: https://github.com/soywod/himalaya/issues/220
[#227]: https://github.com/soywod/himalaya/issues/227
[#228]: https://github.com/soywod/himalaya/issues/228
[#229]: https://github.com/soywod/himalaya/issues/229
[#249]: https://github.com/soywod/himalaya/issues/249

33
Cargo.lock generated
View file

@ -226,6 +226,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56047058e1ab118075ca22f9ecd737bcc961aa3566a3019cb71388afa280bd8a"
dependencies = [
"serde",
]
[[package]]
name = "fastrand"
version = "1.5.0"
@ -322,6 +331,16 @@ dependencies = [
"slab",
]
[[package]]
name = "gethostname"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4addc164932852d066774c405dbbdb7914742d2b39e39e1a7ca949c856d054d1"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@ -361,7 +380,7 @@ dependencies = [
[[package]]
name = "himalaya"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"ammonia",
"anyhow",
@ -369,11 +388,13 @@ dependencies = [
"chrono",
"clap",
"env_logger",
"erased-serde",
"html-escape",
"imap",
"imap-proto",
"lettre",
"log",
"maildir",
"mailparse",
"native-tls",
"regex",
@ -563,6 +584,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "maildir"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c47481eb056f735997fe5248a94fe8d03816388858c990a52eb271c21b33ff3"
dependencies = [
"gethostname",
"mailparse",
]
[[package]]
name = "mailparse"
version = "0.13.6"

View file

@ -1,9 +1,9 @@
[package]
name = "himalaya"
description = "Command-line interface for email management"
version = "0.5.5"
version = "0.5.6"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2018"
edition = "2021"
license-file = "LICENSE"
readme = "README.md"
categories = ["command-line-interface", "command-line-utilities", "email"]
@ -23,11 +23,13 @@ atty = "0.2.14"
chrono = "0.4.19"
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
env_logger = "0.8.3"
erased-serde = "0.3.18"
html-escape = "0.2.9"
imap = "3.0.0-alpha.4"
imap-proto = "0.14.3"
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
log = "0.4.14"
maildir = "0.6.0"
mailparse = "0.13.6"
native-tls = "0.2.8"
regex = "1.5.4"

View file

@ -2,15 +2,23 @@
Command-line interface for email management
*The project is under active development. Do not use in production before the `v1.0.0`.*
*The project is under active development. Do not use in production
before the `v1.0.0`.*
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
## Motivation
Bringing emails to the terminal is a *pain*. First, because they are sensitive data. Secondly, the existing TUIs ([Mutt](http://www.mutt.org/), [NeoMutt](https://neomutt.org/), [Alpine](https://alpine.x10host.com/), [aerc](https://aerc-mail.org/)…) are really hard to configure. They require time and patience.
Bringing emails to the terminal is a *pain*. First, because they are
sensitive data. Secondly, the existing TUIs
([Mutt](http://www.mutt.org/), [NeoMutt](https://neomutt.org/),
[Alpine](https://alpine.x10host.com/),
[aerc](https://aerc-mail.org/)…) are really hard to configure. They
require time and patience.
The aim of Himalaya is to extract the email logic into a simple (yet solid) CLI API that can be used directly from the terminal, from scripts, from UIs… Possibilities are endless!
The aim of Himalaya is to extract the email logic into a simple (yet
solid) CLI API that can be used directly from the terminal, from
scripts, from UIs… Possibilities are endless!
## Installation
@ -21,7 +29,9 @@ The aim of Himalaya is to extract the email logic into a simple (yet solid) CLI
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh
```
*See the [wiki](https://github.com/soywod/himalaya/wiki/Installation:from-binary) for other installation methods.*
*See the
[wiki](https://github.com/soywod/himalaya/wiki/Installation:from-binary)
for other installation methods.*
## Configuration
@ -50,7 +60,9 @@ smtp-login = "your.email@gmail.com"
smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
```
*See the [wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file) for all the options.*
*See the
[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file)
for all the options.*
## Features
@ -59,13 +71,17 @@ smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
- Email composition based on `$EDITOR`
- Email manipulation (copy/move/delete)
- Multi-accounting
- IMAP and Maildir support (POP and Notmuch are coming soon)
- PGP end-to-end encryption
- IDLE mode for real-time notifications
- Vim plugin
- Completions for bash/zsh/fish
- JSON output
- …
*See the [wiki](https://github.com/soywod/himalaya/wiki/Usage:msg:list) for all the features.*
*See the
[wiki](https://github.com/soywod/himalaya/wiki/Usage:msg:list) for all
the features.*
## Sponsoring
@ -79,8 +95,11 @@ smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
- [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage
- [isync](https://isync.sourceforge.io/), an email synchronizer for
offline usage
- [NeoMutt](https://neomutt.org/), an email terminal user interface
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface
- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other
email terminal user interface
- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool
over NeoMutt and isync
- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib

41
src/backends/backend.rs Normal file
View file

@ -0,0 +1,41 @@
//! Backend module.
//!
//! This module exposes the backend trait, which can be used to create
//! custom backend implementations.
use anyhow::Result;
use crate::{
mbox::Mboxes,
msg::{Envelopes, Msg},
};
pub trait Backend<'a> {
fn connect(&mut self) -> Result<()> {
Ok(())
}
fn add_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>>;
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_envelopes(
&mut self,
mbox: &str,
sort: &str,
filter: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>>;
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>>;
fn get_msg(&mut self, mbox: &str, id: &str) -> Result<Msg>;
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
fn del_msg(&mut self, mbox: &str, ids: &str) -> Result<()>;
fn add_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn set_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn del_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn disconnect(&mut self) -> Result<()> {
Ok(())
}
}

View file

@ -0,0 +1,369 @@
//! IMAP backend module.
//!
//! This module contains the definition of the IMAP backend.
use anyhow::{anyhow, Context, Result};
use log::{debug, log_enabled, trace, Level};
use native_tls::{TlsConnector, TlsStream};
use std::{
collections::HashSet,
convert::{TryFrom, TryInto},
net::TcpStream,
thread,
};
use crate::{
backends::{
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
},
config::{AccountConfig, ImapBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
output::run_cmd,
};
use super::ImapFlags;
type ImapSess = imap::Session<TlsStream<TcpStream>>;
pub struct ImapBackend<'a> {
account_config: &'a AccountConfig,
imap_config: &'a ImapBackendConfig,
sess: Option<ImapSess>,
}
impl<'a> ImapBackend<'a> {
pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self {
Self {
account_config,
imap_config,
sess: None,
}
}
fn sess(&mut self) -> Result<&mut ImapSess> {
if self.sess.is_none() {
debug!("create TLS builder");
debug!("insecure: {}", self.imap_config.imap_insecure);
let builder = TlsConnector::builder()
.danger_accept_invalid_certs(self.imap_config.imap_insecure)
.danger_accept_invalid_hostnames(self.imap_config.imap_insecure)
.build()
.context("cannot create TLS connector")?;
debug!("create client");
debug!("host: {}", self.imap_config.imap_host);
debug!("port: {}", self.imap_config.imap_port);
debug!("starttls: {}", self.imap_config.imap_starttls);
let mut client_builder =
imap::ClientBuilder::new(&self.imap_config.imap_host, self.imap_config.imap_port);
if self.imap_config.imap_starttls {
client_builder.starttls();
}
let client = client_builder
.connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?))
.context("cannot connect to IMAP server")?;
debug!("create session");
debug!("login: {}", self.imap_config.imap_login);
debug!("passwd cmd: {}", self.imap_config.imap_passwd_cmd);
let mut sess = client
.login(
&self.imap_config.imap_login,
&self.imap_config.imap_passwd()?,
)
.map_err(|res| res.0)
.context("cannot login to IMAP server")?;
sess.debug = log_enabled!(Level::Trace);
self.sess = Some(sess);
}
match self.sess {
Some(ref mut sess) => Ok(sess),
None => Err(anyhow!("cannot get IMAP session")),
}
}
fn search_new_msgs(&mut self, query: &str) -> Result<Vec<u32>> {
let uids: Vec<u32> = self
.sess()?
.uid_search(query)
.context("cannot search new messages")?
.into_iter()
.collect();
debug!("found {} new messages", uids.len());
trace!("uids: {:?}", uids);
Ok(uids)
}
pub fn notify(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
debug!("notify");
debug!("examine mailbox {:?}", mbox);
self.sess()?
.examine(mbox)
.context(format!("cannot examine mailbox {}", mbox))?;
debug!("init messages hashset");
let mut msgs_set: HashSet<u32> = self
.search_new_msgs(&self.account_config.notify_query)?
.iter()
.cloned()
.collect::<HashSet<_>>();
trace!("messages hashset: {:?}", msgs_set);
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let uids: Vec<u32> = self
.search_new_msgs(&self.account_config.notify_query)?
.into_iter()
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
.collect();
debug!("found {} new messages not in hashset", uids.len());
trace!("messages hashet: {:?}", msgs_set);
if !uids.is_empty() {
let uids = uids
.iter()
.map(|uid| uid.to_string())
.collect::<Vec<_>>()
.join(",");
let fetches = self
.sess()?
.uid_fetch(uids, "(UID ENVELOPE)")
.context("cannot fetch new messages enveloppe")?;
for fetch in fetches.iter() {
let msg = ImapEnvelope::try_from(fetch)?;
let uid = fetch.uid.ok_or_else(|| {
anyhow!("cannot retrieve message {}'s UID", fetch.message)
})?;
let from = msg.sender.to_owned().into();
self.account_config.run_notify_cmd(&msg.subject, &from)?;
debug!("notify message: {}", uid);
trace!("message: {:?}", msg);
debug!("insert message {} in hashset", uid);
msgs_set.insert(uid);
trace!("messages hashset: {:?}", msgs_set);
}
}
debug!("end loop");
}
}
pub fn watch(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
debug!("examine mailbox: {}", mbox);
self.sess()?
.examine(mbox)
.context(format!("cannot examine mailbox `{}`", mbox))?;
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let cmds = self.account_config.watch_cmds.clone();
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});
debug!("end loop");
}
}
}
impl<'a> Backend<'a> for ImapBackend<'a> {
fn add_mbox(&mut self, mbox: &str) -> Result<()> {
self.sess()?
.create(mbox)
.context(format!("cannot create imap mailbox {:?}", mbox))
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
let mboxes: ImapMboxes = self
.sess()?
.list(Some(""), Some("*"))
.context("cannot list mailboxes")?
.into();
Ok(Box::new(mboxes))
}
fn del_mbox(&mut self, mbox: &str) -> Result<()> {
self.sess()?
.delete(mbox)
.context(format!("cannot delete imap mailbox {:?}", mbox))
}
fn get_envelopes(
&mut self,
mbox: &str,
sort: &str,
filter: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists;
if last_seq == 0 {
return Ok(Box::new(ImapEnvelopes::default()));
}
let sort: SortCriteria = sort.try_into()?;
let charset = imap::extensions::sort::SortCharset::Utf8;
let begin = page * page_size;
let end = begin + (page_size - 1);
let seqs: Vec<String> = self
.sess()?
.sort(&sort, charset, filter)
.context(format!(
"cannot search in {:?} with query {:?}",
mbox, filter
))?
.iter()
.map(|seq| seq.to_string())
.collect();
if seqs.is_empty() {
return Ok(Box::new(ImapEnvelopes::default()));
}
let range = seqs[begin..end.min(seqs.len())].join(",");
let fetches = self
.sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(format!("cannot fetch messages within range {:?}", range))?;
let envelopes: ImapEnvelopes = fetches.try_into()?;
Ok(Box::new(envelopes))
}
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
let flags: ImapFlags = flags.into();
self.sess()?
.append(mbox, msg)
.flags(<ImapFlags as Into<Vec<imap::types::Flag<'a>>>>::into(flags))
.finish()
.context(format!("cannot append message to {:?}", mbox))?;
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists;
Ok(Box::new(last_seq))
}
fn get_msg(&mut self, mbox: &str, seq: &str) -> Result<Msg> {
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
let fetches = self
.sess()?
.fetch(seq, "(FLAGS INTERNALDATE BODY[])")
.context(format!("cannot fetch messages {:?}", seq))?;
let fetch = fetches
.first()
.ok_or_else(|| anyhow!("cannot find message {:?}", seq))?;
let msg_raw = fetch.body().unwrap_or_default().to_owned();
let mut msg = Msg::from_parsed_mail(
mailparse::parse_mail(&msg_raw).context("cannot parse message")?,
self.account_config,
)?;
msg.raw = msg_raw;
Ok(msg)
}
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
let msg = self.get_msg(&mbox_src, seq)?.raw;
println!("raw: {:?}", String::from_utf8(msg.to_vec()).unwrap());
self.add_msg(&mbox_dst, &msg, "seen")?;
Ok(())
}
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
let msg = self.get_msg(mbox_src, seq)?.raw;
self.add_flags(mbox_src, seq, "seen deleted")?;
self.add_msg(&mbox_dst, &msg, "seen")?;
Ok(())
}
fn del_msg(&mut self, mbox: &str, seq: &str) -> Result<()> {
self.add_flags(mbox, seq, "deleted")
}
fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("+FLAGS ({})", flags))
.context(format!("cannot add flags {:?}", &flags))?;
self.sess()?
.expunge()
.context(format!("cannot expunge mailbox {:?}", mbox))?;
Ok(())
}
fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("FLAGS ({})", flags))
.context(format!("cannot set flags {:?}", &flags))?;
Ok(())
}
fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("-FLAGS ({})", flags))
.context(format!("cannot remove flags {:?}", &flags))?;
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
if let Some(ref mut sess) = self.sess {
debug!("logout from IMAP server");
sess.logout().context("cannot logout from IMAP server")?;
}
Ok(())
}
}

View file

@ -1,42 +1,115 @@
//! IMAP envelope module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the envelope.
use anyhow::{anyhow, Context, Error, Result};
use serde::Serialize;
use std::{borrow::Cow, convert::TryFrom};
use std::{convert::TryFrom, ops::Deref};
use crate::{
domain::msg::{Flag, Flags},
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
pub type RawEnvelope = imap::types::Fetch;
use super::{ImapFlag, ImapFlags};
/// Representation of an envelope. An envelope gathers basic information related to a message. It
/// is mostly used for listings.
#[derive(Debug, Default, Serialize)]
pub struct Envelope<'a> {
/// The sequence number of the message.
/// Represents a list of IMAP envelopes.
#[derive(Debug, Default, serde::Serialize)]
pub struct ImapEnvelopes(pub Vec<ImapEnvelope>);
impl Deref for ImapEnvelopes {
type Target = Vec<ImapEnvelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for ImapEnvelopes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
// impl Envelopes for ImapEnvelopes {
// //
// }
/// Represents the IMAP envelope. The envelope is just a message
/// subset, and is mostly used for listings.
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct ImapEnvelope {
/// Represents the sequence number of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
pub id: u32,
/// The flags attached to the message.
pub flags: Flags,
/// Represents the flags attached to the message.
pub flags: ImapFlags,
/// The subject of the message.
pub subject: Cow<'a, str>,
/// Represents the subject of the message.
pub subject: String,
/// The sender of the message.
/// Represents the first sender of the message.
pub sender: String,
/// The internal date of the message.
/// Represents the internal date of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
pub date: Option<String>,
}
impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
impl Table for ImapEnvelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("SENDER").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
fn row(&self) -> Row {
let id = self.id.to_string();
let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&ImapFlag::Seen);
let subject = &self.subject;
let sender = &self.sender;
let date = self.date.as_deref().unwrap_or_default();
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
}
}
/// Represents a list of raw envelopes returned by the `imap` crate.
pub type RawImapEnvelopes = imap::types::ZeroCopy<Vec<RawImapEnvelope>>;
impl TryFrom<RawImapEnvelopes> for ImapEnvelopes {
type Error = Error;
fn try_from(fetch: &'a RawEnvelope) -> Result<Envelope> {
fn try_from(raw_envelopes: RawImapEnvelopes) -> Result<Self, Self::Error> {
let mut envelopes = vec![];
for raw_envelope in raw_envelopes.iter().rev() {
envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?);
}
Ok(Self(envelopes))
}
}
/// Represents the raw envelope returned by the `imap` crate.
pub type RawImapEnvelope = imap::types::Fetch;
impl TryFrom<&RawImapEnvelope> for ImapEnvelope {
type Error = Error;
fn try_from(fetch: &RawImapEnvelope) -> Result<ImapEnvelope> {
let envelope = fetch
.envelope()
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
@ -45,10 +118,10 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
let id = fetch.message;
// Get the flags
let flags = Flags::try_from(fetch.flags())?;
let flags = ImapFlags::try_from(fetch.flags())?;
// Get the subject
let subject: Cow<str> = envelope
let subject = envelope
.subject
.as_ref()
.map(|subj| {
@ -57,8 +130,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
fetch.message
))
})
.unwrap_or_else(|| Ok(String::default()))?
.into();
.unwrap_or_else(|| Ok(String::default()))?;
// Get the sender
let sender = envelope
@ -110,29 +182,3 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
})
}
}
impl<'a> Table for Envelope<'a> {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("SENDER").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
fn row(&self) -> Row {
let id = self.id.to_string();
let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&Flag::Seen);
let subject = &self.subject;
let sender = &self.sender;
let date = self.date.as_deref().unwrap_or_default();
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
}
}

View file

@ -0,0 +1,147 @@
use anyhow::{anyhow, Error, Result};
use std::{convert::TryFrom, fmt, ops::Deref};
/// Represents the imap flag variants.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub enum ImapFlag {
Seen,
Answered,
Flagged,
Deleted,
Draft,
Recent,
MayCreate,
Custom(String),
}
impl From<&str> for ImapFlag {
fn from(flag_str: &str) -> Self {
match flag_str {
"seen" => ImapFlag::Seen,
"answered" => ImapFlag::Answered,
"flagged" => ImapFlag::Flagged,
"deleted" => ImapFlag::Deleted,
"draft" => ImapFlag::Draft,
"recent" => ImapFlag::Recent,
"maycreate" | "may-create" => ImapFlag::MayCreate,
flag_str => ImapFlag::Custom(flag_str.into()),
}
}
}
impl TryFrom<&imap::types::Flag<'_>> for ImapFlag {
type Error = Error;
fn try_from(flag: &imap::types::Flag<'_>) -> Result<Self, Self::Error> {
Ok(match flag {
imap::types::Flag::Seen => ImapFlag::Seen,
imap::types::Flag::Answered => ImapFlag::Answered,
imap::types::Flag::Flagged => ImapFlag::Flagged,
imap::types::Flag::Deleted => ImapFlag::Deleted,
imap::types::Flag::Draft => ImapFlag::Draft,
imap::types::Flag::Recent => ImapFlag::Recent,
imap::types::Flag::MayCreate => ImapFlag::MayCreate,
imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()),
_ => return Err(anyhow!("cannot parse imap flag")),
})
}
}
/// Represents the imap flags.
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ImapFlags(pub Vec<ImapFlag>);
impl ImapFlags {
/// Builds a symbols string
pub fn to_symbols_string(&self) -> String {
let mut flags = String::new();
flags.push_str(if self.contains(&ImapFlag::Seen) {
" "
} else {
""
});
flags.push_str(if self.contains(&ImapFlag::Answered) {
""
} else {
" "
});
flags.push_str(if self.contains(&ImapFlag::Flagged) {
""
} else {
" "
});
flags
}
}
impl Deref for ImapFlags {
type Target = Vec<ImapFlag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for ImapFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut glue = "";
for flag in &self.0 {
write!(f, "{}", glue)?;
match flag {
ImapFlag::Seen => write!(f, "\\Seen")?,
ImapFlag::Answered => write!(f, "\\Answered")?,
ImapFlag::Flagged => write!(f, "\\Flagged")?,
ImapFlag::Deleted => write!(f, "\\Deleted")?,
ImapFlag::Draft => write!(f, "\\Draft")?,
ImapFlag::Recent => write!(f, "\\Recent")?,
ImapFlag::MayCreate => write!(f, "\\MayCreate")?,
ImapFlag::Custom(custom) => write!(f, "{}", custom)?,
}
glue = " ";
}
Ok(())
}
}
impl<'a> Into<Vec<imap::types::Flag<'a>>> for ImapFlags {
fn into(self) -> Vec<imap::types::Flag<'a>> {
self.0
.into_iter()
.map(|flag| match flag {
ImapFlag::Seen => imap::types::Flag::Seen,
ImapFlag::Answered => imap::types::Flag::Answered,
ImapFlag::Flagged => imap::types::Flag::Flagged,
ImapFlag::Deleted => imap::types::Flag::Deleted,
ImapFlag::Draft => imap::types::Flag::Draft,
ImapFlag::Recent => imap::types::Flag::Recent,
ImapFlag::MayCreate => imap::types::Flag::MayCreate,
ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()),
})
.collect()
}
}
impl From<&str> for ImapFlags {
fn from(flags_str: &str) -> Self {
ImapFlags(
flags_str
.split_whitespace()
.map(|flag_str| flag_str.trim().into())
.collect(),
)
}
}
impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags {
type Error = Error;
fn try_from(flags: &[imap::types::Flag<'_>]) -> Result<Self, Self::Error> {
let mut f = vec![];
for flag in flags {
f.push(flag.try_into()?);
}
Ok(Self(f))
}
}

View file

@ -0,0 +1,15 @@
//! Module related to IMAP handling.
//!
//! This module gathers all IMAP handlers triggered by the CLI.
use anyhow::Result;
use crate::backends::ImapBackend;
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
imap.notify(keepalive, mbox)
}
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
imap.watch(keepalive, mbox)
}

View file

@ -0,0 +1,148 @@
//! IMAP mailbox module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the mailbox.
use anyhow::Result;
use std::fmt::{self, Display};
use std::ops::Deref;
use crate::mbox::Mboxes;
use crate::{
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
use super::ImapMboxAttrs;
/// Represents a list of IMAP mailboxes.
#[derive(Debug, Default, serde::Serialize)]
pub struct ImapMboxes(pub Vec<ImapMbox>);
impl Deref for ImapMboxes {
type Target = Vec<ImapMbox>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for ImapMboxes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
impl Mboxes for ImapMboxes {
//
}
/// Represents the IMAP mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct ImapMbox {
/// Represents the mailbox hierarchie delimiter.
pub delim: String,
/// Represents the mailbox name.
pub name: String,
/// Represents the mailbox attributes.
pub attrs: ImapMboxAttrs,
}
impl ImapMbox {
pub fn new(name: &str) -> Self {
Self {
name: name.into(),
..Self::default()
}
}
}
impl Display for ImapMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for ImapMbox {
fn head() -> Row {
Row::new()
.cell(Cell::new("DELIM").bold().underline().white())
.cell(Cell::new("NAME").bold().underline().white())
.cell(
Cell::new("ATTRIBUTES")
.shrinkable()
.bold()
.underline()
.white(),
)
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.delim).white())
.cell(Cell::new(&self.name).green())
.cell(Cell::new(&self.attrs.to_string()).shrinkable().blue())
}
}
#[cfg(test)]
mod tests {
use crate::backends::ImapMboxAttr;
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(ImapMbox::default(), ImapMbox::new(""));
assert_eq!(
ImapMbox {
name: "INBOX".into(),
..ImapMbox::default()
},
ImapMbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = ImapMbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = ImapMbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = ImapMbox {
delim: ".".into(),
name: "Sent".into(),
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
};
assert_eq!("Sent", full_mbox.to_string());
}
}
/// Represents a list of raw mailboxes returned by the `imap` crate.
pub type RawImapMboxes = imap::types::ZeroCopy<Vec<RawImapMbox>>;
impl<'a> From<RawImapMboxes> for ImapMboxes {
fn from(raw_mboxes: RawImapMboxes) -> Self {
Self(raw_mboxes.iter().map(ImapMbox::from).collect())
}
}
/// Represents the raw mailbox returned by the `imap` crate.
pub type RawImapMbox = imap::types::Name;
impl<'a> From<&'a RawImapMbox> for ImapMbox {
fn from(raw_mbox: &'a RawImapMbox) -> Self {
Self {
delim: raw_mbox.delimiter().unwrap_or_default().into(),
name: raw_mbox.name().into(),
attrs: raw_mbox.attributes().into(),
}
}
}

View file

@ -0,0 +1,119 @@
//! IMAP mailbox attribute module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the mailbox attribute.
/// Represents the raw mailbox attribute returned by the `imap` crate.
pub use imap::types::NameAttribute as RawImapMboxAttr;
use std::{
fmt::{self, Display},
ops::Deref,
};
/// Represents the attributes of the mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct ImapMboxAttrs(pub Vec<ImapMboxAttr>);
impl Deref for ImapMboxAttrs {
type Target = Vec<ImapMboxAttr>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for ImapMboxAttrs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut glue = "";
for attr in self.iter() {
write!(f, "{}{}", glue, attr)?;
glue = ", ";
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
pub enum ImapMboxAttr {
NoInferiors,
NoSelect,
Marked,
Unmarked,
Custom(String),
}
/// Makes the attribute displayable.
impl Display for ImapMboxAttr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"),
ImapMboxAttr::NoSelect => write!(f, "NoSelect"),
ImapMboxAttr::Marked => write!(f, "Marked"),
ImapMboxAttr::Unmarked => write!(f, "Unmarked"),
ImapMboxAttr::Custom(custom) => write!(f, "{}", custom),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_display_attrs() {
macro_rules! attrs_from {
($($attr:expr),*) => {
ImapMboxAttrs(vec![$($attr,)*]).to_string()
};
}
let empty_attr = attrs_from![];
let single_attr = attrs_from![ImapMboxAttr::NoInferiors];
let multiple_attrs = attrs_from![
ImapMboxAttr::Custom("AttrCustom".into()),
ImapMboxAttr::NoInferiors
];
assert_eq!("", empty_attr);
assert_eq!("NoInferiors", single_attr);
assert!(multiple_attrs.contains("NoInferiors"));
assert!(multiple_attrs.contains("AttrCustom"));
assert!(multiple_attrs.contains(","));
}
#[test]
fn it_should_display_attr() {
macro_rules! attr_from {
($attr:ident) => {
ImapMboxAttr::$attr.to_string()
};
($custom:literal) => {
ImapMboxAttr::Custom($custom.into()).to_string()
};
}
assert_eq!("NoInferiors", attr_from![NoInferiors]);
assert_eq!("NoSelect", attr_from![NoSelect]);
assert_eq!("Marked", attr_from![Marked]);
assert_eq!("Unmarked", attr_from![Unmarked]);
assert_eq!("CustomAttr", attr_from!["CustomAttr"]);
}
}
impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs {
fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self {
Self(raw_attrs.iter().map(ImapMboxAttr::from).collect())
}
}
impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr {
fn from(attr: &'a RawImapMboxAttr<'a>) -> Self {
match attr {
RawImapMboxAttr::NoInferiors => Self::NoInferiors,
RawImapMboxAttr::NoSelect => Self::NoSelect,
RawImapMboxAttr::Marked => Self::Marked,
RawImapMboxAttr::Unmarked => Self::Unmarked,
RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()),
}
}
}

View file

@ -0,0 +1,61 @@
//! Message sort criteria module.
//!
//! This module regroups everything related to deserialization of
//! message sort criteria.
use anyhow::{anyhow, Error, Result};
use std::{convert::TryFrom, ops::Deref};
/// Represents the message sort criteria. It is just a wrapper around
/// the `imap::extensions::sort::SortCriterion`.
pub struct SortCriteria<'a>(Vec<imap::extensions::sort::SortCriterion<'a>>);
impl<'a> Deref for SortCriteria<'a> {
type Target = Vec<imap::extensions::sort::SortCriterion<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> TryFrom<&'a str> for SortCriteria<'a> {
type Error = Error;
fn try_from(criteria_str: &'a str) -> Result<Self, Self::Error> {
let mut criteria = vec![];
for criterion_str in criteria_str.split(" ") {
criteria.push(match criterion_str.trim() {
"arrival:asc" | "arrival" => Ok(imap::extensions::sort::SortCriterion::Arrival),
"arrival:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Arrival,
)),
"cc:asc" | "cc" => Ok(imap::extensions::sort::SortCriterion::Cc),
"cc:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Cc,
)),
"date:asc" | "date" => Ok(imap::extensions::sort::SortCriterion::Date),
"date:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Date,
)),
"from:asc" | "from" => Ok(imap::extensions::sort::SortCriterion::From),
"from:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::From,
)),
"size:asc" | "size" => Ok(imap::extensions::sort::SortCriterion::Size),
"size:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Size,
)),
"subject:asc" | "subject" => Ok(imap::extensions::sort::SortCriterion::Subject),
"subject:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Subject,
)),
"to:asc" | "to" => Ok(imap::extensions::sort::SortCriterion::To),
"to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::To,
)),
_ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)),
}?);
}
Ok(Self(criteria))
}
}

View file

@ -0,0 +1,185 @@
use anyhow::{anyhow, Context, Result};
use std::{convert::TryInto, fs, path::PathBuf};
use crate::{
backends::{Backend, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
config::{AccountConfig, MaildirBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
};
pub struct MaildirBackend<'a> {
mdir: maildir::Maildir,
account_config: &'a AccountConfig,
}
impl<'a> MaildirBackend<'a> {
pub fn new(
account_config: &'a AccountConfig,
maildir_config: &'a MaildirBackendConfig,
) -> Self {
Self {
account_config,
mdir: maildir_config.maildir_dir.clone().into(),
}
}
fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result<PathBuf> {
if mdir_path.is_dir() {
Ok(mdir_path)
} else {
Err(anyhow!(
"cannot read maildir from directory {:?}",
mdir_path
))
}
}
fn get_mdir_from_name(&self, mdir: &str) -> Result<maildir::Maildir> {
if mdir == self.account_config.inbox_folder {
self.validate_mdir_path(self.mdir.path().to_owned())
.map(maildir::Maildir::from)
} else {
self.validate_mdir_path(mdir.into())
.or_else(|_| {
let path = self.mdir.path().join(format!(".{}", mdir));
self.validate_mdir_path(path)
})
.map(maildir::Maildir::from)
}
}
}
impl<'a> Backend<'a> for MaildirBackend<'a> {
fn add_mbox(&mut self, mdir: &str) -> Result<()> {
fs::create_dir(self.mdir.path().join(format!(".{}", mdir)))
.context(format!("cannot create maildir subfolder {:?}", mdir))
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
let mboxes: MaildirMboxes = self.mdir.list_subdirs().try_into()?;
Ok(Box::new(mboxes))
}
fn del_mbox(&mut self, mdir: &str) -> Result<()> {
fs::remove_dir_all(self.mdir.path().join(format!(".{}", mdir)))
.context(format!("cannot delete maildir subfolder {:?}", mdir))
}
fn get_envelopes(
&mut self,
mdir: &str,
_sort: &str,
filter: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
let mdir = self.get_mdir_from_name(mdir)?;
let mail_entries = match filter {
"new" => mdir.list_new(),
_ => mdir.list_cur(),
};
let mut envelopes: MaildirEnvelopes = mail_entries
.try_into()
.context("cannot parse maildir envelopes from {:?}")?;
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
let page_begin = page * page_size;
if page_begin > envelopes.len() {
return Err(anyhow!(format!(
"cannot list maildir envelopes at page {:?} (out of bounds)",
page_begin + 1,
)));
}
let page_end = envelopes.len().min(page_begin + page_size);
envelopes.0 = envelopes[page_begin..page_end].to_owned();
Ok(Box::new(envelopes))
}
fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags.try_into()?;
let id = mdir
.store_cur_with_flags(msg, &flags.to_string())
.context(format!(
"cannot add message to the \"cur\" folder of maildir {:?}",
mdir.path()
))?;
Ok(Box::new(id))
}
fn get_msg(&mut self, mdir: &str, id: &str) -> Result<Msg> {
let mdir = self.get_mdir_from_name(mdir)?;
let mut mail_entry = mdir
.find(id)
.ok_or_else(|| anyhow!("cannot find maildir message {:?} in {:?}", id, mdir.path()))?;
let parsed_mail = mail_entry.parsed().context(format!(
"cannot parse maildir message {:?} in {:?}",
id,
mdir.path()
))?;
Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!(
"cannot parse maildir message {:?} from {:?}",
id,
mdir.path()
))
}
fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> {
let mdir_src = self.get_mdir_from_name(mdir_src)?;
let mdir_dst = self.get_mdir_from_name(mdir_dst)?;
mdir_src.copy_to(id, &mdir_dst).context(format!(
"cannot copy message {:?} from maildir {:?} to maildir {:?}",
id,
mdir_src.path(),
mdir_dst.path()
))
}
fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> {
let mdir_src = self.get_mdir_from_name(mdir_src)?;
let mdir_dst = self.get_mdir_from_name(mdir_dst)?;
mdir_src.move_to(id, &mdir_dst).context(format!(
"cannot move message {:?} from maildir {:?} to maildir {:?}",
id,
mdir_src.path(),
mdir_dst.path()
))
}
fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
mdir.delete(id).context(format!(
"cannot delete message {:?} from maildir {:?}",
id,
mdir.path()
))
}
fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags_str.try_into()?;
mdir.add_flags(id, &flags.to_string()).context(format!(
"cannot add flags {:?} to maildir message {:?}",
flags_str, id
))
}
fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags_str.try_into()?;
mdir.set_flags(id, &flags.to_string()).context(format!(
"cannot set flags {:?} to maildir message {:?}",
flags_str, id
))
}
fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags_str.try_into()?;
mdir.remove_flags(id, &flags.to_string()).context(format!(
"cannot remove flags {:?} from maildir message {:?}",
flags_str, id
))
}
}

View file

@ -0,0 +1,187 @@
//! Maildir mailbox module.
//!
//! This module provides Maildir types and conversion utilities
//! related to the envelope
use anyhow::{anyhow, Context, Error, Result};
use chrono::DateTime;
use log::{debug, info, trace};
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use crate::{
backends::{MaildirFlag, MaildirFlags},
msg::{from_slice_to_addrs, Addr},
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of envelopes.
#[derive(Debug, Default, serde::Serialize)]
pub struct MaildirEnvelopes(pub Vec<MaildirEnvelope>);
impl Deref for MaildirEnvelopes {
type Target = Vec<MaildirEnvelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for MaildirEnvelopes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl PrintTable for MaildirEnvelopes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
// impl Envelopes for MaildirEnvelopes {
// //
// }
/// Represents the envelope. The envelope is just a message subset,
/// and is mostly used for listings.
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct MaildirEnvelope {
/// Represents the id of the message.
pub id: String,
/// Represents the flags of the message.
pub flags: MaildirFlags,
/// Represents the subject of the message.
pub subject: String,
/// Represents the first sender of the message.
pub sender: String,
/// Represents the date of the message.
pub date: String,
}
impl Table for MaildirEnvelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("IDENTIFIER").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("SENDER").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
fn row(&self) -> Row {
let id = self.id.to_string();
let unseen = !self.flags.contains(&MaildirFlag::Seen);
let flags = self.flags.to_symbols_string();
let subject = &self.subject;
let sender = &self.sender;
let date = &self.date;
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
}
}
/// Represents a list of raw envelopees returned by the `maildir` crate.
pub type RawMaildirEnvelopes = maildir::MailEntries;
impl<'a> TryFrom<RawMaildirEnvelopes> for MaildirEnvelopes {
type Error = Error;
fn try_from(mail_entries: RawMaildirEnvelopes) -> Result<Self, Self::Error> {
let mut envelopes = vec![];
for entry in mail_entries {
let envelope: MaildirEnvelope = entry
.context("cannot decode maildir mail entry")?
.try_into()
.context("cannot parse maildir mail entry")?;
envelopes.push(envelope);
}
Ok(MaildirEnvelopes(envelopes))
}
}
/// Represents the raw envelope returned by the `maildir` crate.
pub type RawMaildirEnvelope = maildir::MailEntry;
impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
type Error = Error;
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
info!("begin: try building envelope from maildir parsed mail");
let mut envelope = Self {
id: mail_entry.id().into(),
flags: (&mail_entry)
.try_into()
.context("cannot parse maildir flags")?,
..Self::default()
};
let parsed_mail = mail_entry
.parsed()
.context("cannot parse maildir mail entry")?;
debug!("begin: parse headers");
for h in parsed_mail.get_headers() {
let k = h.get_key();
debug!("header key: {:?}", k);
let v = rfc2047_decoder::decode(h.get_value_raw())
.context(format!("cannot decode value from header {:?}", k))?;
debug!("header value: {:?}", v);
match k.to_lowercase().as_str() {
"date" => {
envelope.date =
DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0)
.context(format!("cannot parse maildir message date {:?}", v))?
.naive_local()
.to_string();
}
"subject" => {
envelope.subject = v.into();
}
"from" => {
envelope.sender = from_slice_to_addrs(v)
.context(format!("cannot parse header {:?}", k))?
.and_then(|senders| {
if senders.is_empty() {
None
} else {
Some(senders)
}
})
.map(|senders| match &senders[0] {
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
}
Addr::Group(mailparse::GroupInfo { group_name, .. }) => {
group_name.to_owned()
}
})
.ok_or_else(|| anyhow!("cannot find sender"))?;
}
_ => (),
}
}
debug!("end: parse headers");
trace!("envelope: {:?}", envelope);
info!("end: try building envelope from maildir parsed mail");
Ok(envelope)
}
}

View file

@ -0,0 +1,129 @@
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
ops::Deref,
};
/// Represents the maildir flag variants.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub enum MaildirFlag {
Passed,
Replied,
Seen,
Trashed,
Draft,
Flagged,
Custom(char),
}
/// Represents the maildir flags.
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
pub struct MaildirFlags(pub Vec<MaildirFlag>);
impl MaildirFlags {
/// Builds a symbols string
pub fn to_symbols_string(&self) -> String {
let mut flags = String::new();
flags.push_str(if self.contains(&MaildirFlag::Seen) {
" "
} else {
""
});
flags.push_str(if self.contains(&MaildirFlag::Replied) {
""
} else {
" "
});
flags.push_str(if self.contains(&MaildirFlag::Passed) {
""
} else {
" "
});
flags.push_str(if self.contains(&MaildirFlag::Flagged) {
""
} else {
" "
});
flags
}
}
impl Deref for MaildirFlags {
type Target = Vec<MaildirFlag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ToString for MaildirFlags {
fn to_string(&self) -> String {
self.0
.iter()
.map(|flag| {
let flag_char: char = flag.into();
flag_char
})
.collect()
}
}
impl TryFrom<&str> for MaildirFlags {
type Error = Error;
fn try_from(flags_str: &str) -> Result<Self, Self::Error> {
let mut flags = vec![];
for flag_str in flags_str.split_whitespace() {
flags.push(flag_str.trim().try_into()?);
}
Ok(MaildirFlags(flags))
}
}
impl From<&maildir::MailEntry> for MaildirFlags {
fn from(mail_entry: &maildir::MailEntry) -> Self {
let mut flags = vec![];
for c in mail_entry.flags().chars() {
flags.push(match c {
'P' => MaildirFlag::Passed,
'R' => MaildirFlag::Replied,
'S' => MaildirFlag::Seen,
'T' => MaildirFlag::Trashed,
'D' => MaildirFlag::Draft,
'F' => MaildirFlag::Flagged,
custom => MaildirFlag::Custom(custom),
})
}
Self(flags)
}
}
impl Into<char> for &MaildirFlag {
fn into(self) -> char {
match self {
MaildirFlag::Passed => 'P',
MaildirFlag::Replied => 'R',
MaildirFlag::Seen => 'S',
MaildirFlag::Trashed => 'T',
MaildirFlag::Draft => 'D',
MaildirFlag::Flagged => 'F',
MaildirFlag::Custom(custom) => *custom,
}
}
}
impl TryFrom<&str> for MaildirFlag {
type Error = Error;
fn try_from(flag_str: &str) -> Result<Self, Self::Error> {
match flag_str {
"passed" => Ok(MaildirFlag::Passed),
"replied" => Ok(MaildirFlag::Replied),
"seen" => Ok(MaildirFlag::Seen),
"trashed" => Ok(MaildirFlag::Trashed),
"draft" => Ok(MaildirFlag::Draft),
"flagged" => Ok(MaildirFlag::Flagged),
flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)),
}
}
}

View file

@ -0,0 +1,141 @@
//! Maildir mailbox module.
//!
//! This module provides Maildir types and conversion utilities
//! related to the mailbox
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
ffi::OsStr,
fmt::{self, Display},
ops::Deref,
};
use crate::{
mbox::Mboxes,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of Maildir mailboxes.
#[derive(Debug, Default, serde::Serialize)]
pub struct MaildirMboxes(pub Vec<MaildirMbox>);
impl Deref for MaildirMboxes {
type Target = Vec<MaildirMbox>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for MaildirMboxes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
impl Mboxes for MaildirMboxes {
//
}
/// Represents the mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct MaildirMbox {
/// Represents the mailbox name.
pub name: String,
}
impl MaildirMbox {
pub fn new(name: &str) -> Self {
Self { name: name.into() }
}
}
impl Display for MaildirMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for MaildirMbox {
fn head() -> Row {
Row::new().cell(Cell::new("SUBDIR").bold().underline().white())
}
fn row(&self) -> Row {
Row::new().cell(Cell::new(&self.name).green())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(MaildirMbox::default(), MaildirMbox::new(""));
assert_eq!(
MaildirMbox {
name: "INBOX".into(),
..MaildirMbox::default()
},
MaildirMbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = MaildirMbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = MaildirMbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = MaildirMbox {
name: "Sent".into(),
};
assert_eq!("Sent", full_mbox.to_string());
}
}
/// Represents a list of raw mailboxes returned by the `maildir` crate.
pub type RawMaildirMboxes = maildir::MaildirEntries;
impl TryFrom<RawMaildirMboxes> for MaildirMboxes {
type Error = Error;
fn try_from(mail_entries: RawMaildirMboxes) -> Result<Self, Self::Error> {
let mut mboxes = vec![];
for entry in mail_entries {
mboxes.push(entry?.try_into()?);
}
Ok(MaildirMboxes(mboxes))
}
}
/// Represents the raw mailbox returned by the `maildir` crate.
pub type RawMaildirMbox = maildir::Maildir;
impl TryFrom<RawMaildirMbox> for MaildirMbox {
type Error = Error;
fn try_from(mail_entry: RawMaildirMbox) -> Result<Self, Self::Error> {
let subdir_name = mail_entry.path().file_name();
Ok(Self {
name: subdir_name
.and_then(OsStr::to_str)
.and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) })
.ok_or_else(|| {
anyhow!(
"cannot parse maildir subdirectory name from path {:?}",
subdir_name,
)
})?
.into(),
})
}
}

View file

@ -0,0 +1,13 @@
//! This module provides arguments related to the user account config.
use clap::Arg;
/// Represents the user account name argument.
/// This argument allows the user to select a different account than the default one.
pub fn name_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("account")
.long("account")
.short("a")
.help("Selects a specific account")
.value_name("NAME")
}

View file

@ -0,0 +1,402 @@
use anyhow::{anyhow, Context, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, info, trace};
use mailparse::MailAddr;
use std::{env, ffi::OsStr, fs, path::PathBuf};
use crate::{config::*, output::run_cmd};
/// Represents the user account.
#[derive(Debug, Default, Clone)]
pub struct AccountConfig {
/// Represents the name of the user account.
pub name: String,
/// Makes this account the default one.
pub default: bool,
/// Represents the display name of the user account.
pub display_name: String,
/// Represents the email address of the user account.
pub email: String,
/// Represents the downloads directory (mostly for attachments).
pub downloads_dir: PathBuf,
/// Represents the signature of the user.
pub sig: Option<String>,
/// Represents the default page size for listings.
pub default_page_size: usize,
/// Represents the inbox folder name for this account.
pub inbox_folder: String,
/// Represents the sent folder name for this account.
pub sent_folder: String,
/// Represents the draft folder name for this account.
pub draft_folder: String,
/// Represents the notify command.
pub notify_cmd: Option<String>,
/// Overrides the default IMAP query "NEW" used to fetch new messages
pub notify_query: String,
/// Represents the watch commands.
pub watch_cmds: Vec<String>,
/// Represents the SMTP host.
pub smtp_host: String,
/// Represents the SMTP port.
pub smtp_port: u16,
/// Enables StartTLS.
pub smtp_starttls: bool,
/// Trusts any certificate.
pub smtp_insecure: bool,
/// Represents the SMTP login.
pub smtp_login: String,
/// Represents the SMTP password command.
pub smtp_passwd_cmd: String,
/// Represents the command used to encrypt a message.
pub pgp_encrypt_cmd: Option<String>,
/// Represents the command used to decrypt a message.
pub pgp_decrypt_cmd: Option<String>,
}
impl<'a> AccountConfig {
/// tries to create an account from a config and an optional account name.
pub fn from_config_and_opt_account_name(
config: &'a DeserializedConfig,
account_name: Option<&str>,
) -> Result<(AccountConfig, BackendConfig)> {
info!("begin: parsing account and backend configs from config and account name");
debug!("account name: {:?}", account_name.unwrap_or("default"));
let (name, account) = match account_name.map(|name| name.trim()) {
Some("default") | Some("") | None => config
.accounts
.iter()
.find(|(_, account)| match account {
DeserializedAccountConfig::Imap(account) => account.default.unwrap_or_default(),
DeserializedAccountConfig::Maildir(account) => {
account.default.unwrap_or_default()
}
})
.map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => config
.accounts
.get(name)
.map(|account| (name.to_owned(), account))
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)),
}?;
let base_account = account.to_base();
let downloads_dir = base_account
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or_else(|| {
config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
})
.unwrap_or_else(env::temp_dir);
let default_page_size = base_account
.default_page_size
.as_ref()
.or_else(|| config.default_page_size.as_ref())
.unwrap_or(&DEFAULT_PAGE_SIZE)
.to_owned();
let default_sig_delim = DEFAULT_SIG_DELIM.to_string();
let sig_delim = base_account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let sig = base_account
.signature
.as_ref()
.or_else(|| config.signature.as_ref());
let sig = sig
.and_then(|sig| shellexpand::full(sig).ok())
.map(String::from)
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| sig.map(|sig| sig.to_owned()))
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
let account_config = AccountConfig {
name,
display_name: base_account
.name
.as_ref()
.unwrap_or(&config.name)
.to_owned(),
downloads_dir,
sig,
default_page_size,
inbox_folder: base_account
.inbox_folder
.as_deref()
.or_else(|| config.inbox_folder.as_deref())
.unwrap_or(DEFAULT_INBOX_FOLDER)
.to_string(),
sent_folder: base_account
.sent_folder
.as_deref()
.or_else(|| config.sent_folder.as_deref())
.unwrap_or(DEFAULT_SENT_FOLDER)
.to_string(),
draft_folder: base_account
.draft_folder
.as_deref()
.or_else(|| config.draft_folder.as_deref())
.unwrap_or(DEFAULT_DRAFT_FOLDER)
.to_string(),
notify_cmd: base_account.notify_cmd.clone(),
notify_query: base_account
.notify_query
.as_ref()
.or_else(|| config.notify_query.as_ref())
.unwrap_or(&String::from("NEW"))
.to_owned(),
watch_cmds: base_account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.to_owned(),
default: base_account.default.unwrap_or_default(),
email: base_account.email.to_owned(),
smtp_host: base_account.smtp_host.to_owned(),
smtp_port: base_account.smtp_port,
smtp_starttls: base_account.smtp_starttls.unwrap_or_default(),
smtp_insecure: base_account.smtp_insecure.unwrap_or_default(),
smtp_login: base_account.smtp_login.to_owned(),
smtp_passwd_cmd: base_account.smtp_passwd_cmd.to_owned(),
pgp_encrypt_cmd: base_account.pgp_encrypt_cmd.to_owned(),
pgp_decrypt_cmd: base_account.pgp_decrypt_cmd.to_owned(),
};
trace!("account config: {:?}", account_config);
let backend_config = match account {
DeserializedAccountConfig::Imap(config) => BackendConfig::Imap(ImapBackendConfig {
imap_host: config.imap_host.clone(),
imap_port: config.imap_port.clone(),
imap_starttls: config.imap_starttls.unwrap_or_default(),
imap_insecure: config.imap_insecure.unwrap_or_default(),
imap_login: config.imap_login.clone(),
imap_passwd_cmd: config.imap_passwd_cmd.clone(),
}),
DeserializedAccountConfig::Maildir(config) => {
BackendConfig::Maildir(MaildirBackendConfig {
maildir_dir: config.maildir_dir.clone(),
})
}
};
trace!("backend config: {:?}", backend_config);
info!("end: parsing account and backend configs from config and account name");
Ok((account_config, backend_config))
}
/// 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 addr = if self.display_name.is_empty() {
self.email.clone()
} else if has_special_chars {
// Wraps the name with double quotes if it contains any special character.
format!("\"{}\" <{}>", self.display_name, self.email)
} else {
format!("{} <{}>", self.display_name, self.email)
};
Ok(mailparse::addrparse(&addr)
.context(format!(
"cannot parse account address {:?}",
self.display_name
))?
.first()
.ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))?
.clone())
}
/// Builds the user account SMTP credentials.
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
}
/// Encrypts a file.
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
run_cmd(&encrypt_file_cmd).map(Some).context(format!(
"cannot run pgp encrypt command {:?}",
encrypt_file_cmd
))
} else {
Ok(None)
}
}
/// Decrypts a file.
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
run_cmd(&decrypt_file_cmd).map(Some).context(format!(
"cannot run pgp decrypt command {:?}",
decrypt_file_cmd
))
} else {
Ok(None)
}
}
/// Gets the download path from a file name.
pub fn get_download_file_path<S: AsRef<str>>(&self, file_name: S) -> Result<PathBuf> {
let file_path = self.downloads_dir.join(file_name.as_ref());
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
.context(format!(
"cannot get download file path of {:?}",
file_name.as_ref()
))
}
/// Gets the unique download path from a file name by adding suffixes in case of name conflicts.
pub fn get_unique_download_file_path(
&self,
original_file_path: &PathBuf,
is_file: impl Fn(&PathBuf, u8) -> bool,
) -> Result<PathBuf> {
let mut count = 0;
let file_ext = original_file_path
.extension()
.and_then(OsStr::to_str)
.map(|fext| String::from(".") + fext)
.unwrap_or_default();
let mut file_path = original_file_path.clone();
while is_file(&file_path, count) {
count += 1;
file_path.set_file_name(OsStr::new(
&original_file_path
.file_stem()
.and_then(OsStr::to_str)
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
.ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?,
));
}
Ok(file_path)
}
/// Runs the notify command.
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
let subject = subject.as_ref();
let sender = sender.as_ref();
let default_cmd = format!(r#"notify-send "New message from {}" "{}""#, sender, subject);
let cmd = self
.notify_cmd
.as_ref()
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
debug!("run command: {}", cmd);
run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
}
/// Represents all existing kind of account (backend).
#[derive(Debug, Clone)]
pub enum BackendConfig {
Imap(ImapBackendConfig),
Maildir(MaildirBackendConfig),
}
/// Represents the IMAP backend.
#[derive(Debug, Default, Clone)]
pub struct ImapBackendConfig {
/// Represents the IMAP host.
pub imap_host: String,
/// Represents the IMAP port.
pub imap_port: u16,
/// Enables StartTLS.
pub imap_starttls: bool,
/// Trusts any certificate.
pub imap_insecure: bool,
/// Represents the IMAP login.
pub imap_login: String,
/// Represents the IMAP password command.
pub imap_passwd_cmd: String,
}
impl ImapBackendConfig {
/// Gets the IMAP password of the user account.
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(passwd)
}
}
/// Represents the Maildir backend.
#[derive(Debug, Default, Clone)]
pub struct MaildirBackendConfig {
/// Represents the Maildir directory path.
pub maildir_dir: PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_get_unique_download_file_path() {
let account = AccountConfig::default();
let path = PathBuf::from("downloads/file.ext");
// When file path is unique
assert!(matches!(
account.get_unique_download_file_path(&path, |_, _| false),
Ok(path) if path == PathBuf::from("downloads/file.ext")
));
// When 1 file path already exist
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 1),
Ok(path) if path == PathBuf::from("downloads/file_1.ext")
));
// When 5 file paths already exist
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file_5.ext")
));
// When file path has no extension
let path = PathBuf::from("downloads/file");
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file_5")
));
// When file path has 2 extensions
let path = PathBuf::from("downloads/file.ext.ext2");
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file.ext_5.ext2")
));
}
}

View file

@ -1,230 +0,0 @@
use anyhow::{anyhow, Context, Error, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, trace};
use std::{convert::TryFrom, env, fs, path::PathBuf};
use crate::{
config::{Config, DEFAULT_PAGE_SIZE, DEFAULT_SIG_DELIM},
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 {
pub name: String,
pub from: String,
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,
/// Defines the IMAP query used to fetch new messages.
pub notify_query: String,
pub watch_cmds: Vec<String>,
pub default: bool,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_starttls: bool,
pub imap_insecure: bool,
pub imap_login: String,
pub imap_passwd_cmd: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_starttls: bool,
pub smtp_insecure: bool,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
pub pgp_encrypt_cmd: Option<String>,
pub pgp_decrypt_cmd: Option<String>,
}
impl Account {
pub fn address(&self) -> String {
let name = &self.from;
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
if name.is_empty() {
self.email.clone()
} else if has_special_chars {
// so the name has special characters => Wrap it with '"'
format!("\"{}\" <{}>", name, self.email)
} else {
format!("{} <{}>", name, self.email)
}
}
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(passwd)
}
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
}
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
run_cmd(&encrypt_file_cmd).map(Some).context(format!(
"cannot run pgp encrypt command {:?}",
encrypt_file_cmd
))
} else {
Ok(None)
}
}
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
run_cmd(&decrypt_file_cmd).map(Some).context(format!(
"cannot run pgp decrypt command {:?}",
decrypt_file_cmd
))
} else {
Ok(None)
}
}
}
impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
type Error = Error;
fn try_from((config, account_name): (&'a Config, Option<&str>)) -> Result<Self, Self::Error> {
debug!("init account `{}`", account_name.unwrap_or("default"));
let (name, account) = match account_name.map(|name| name.trim()) {
Some("default") | Some("") | None => config
.accounts
.iter()
.find(|(_, account)| account.default.unwrap_or(false))
.map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => config
.accounts
.get(name)
.map(|account| (name.to_owned(), account))
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)),
}?;
let downloads_dir = account
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or_else(|| {
config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
})
.unwrap_or_else(env::temp_dir);
let default_page_size = account
.default_page_size
.as_ref()
.or_else(|| config.default_page_size.as_ref())
.unwrap_or(&DEFAULT_PAGE_SIZE)
.to_owned();
let default_sig_delim = DEFAULT_SIG_DELIM.to_string();
let sig_delim = account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let sig = account
.signature
.as_ref()
.or_else(|| config.signature.as_ref());
let sig = sig
.and_then(|sig| shellexpand::full(sig).ok())
.map(String::from)
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| sig.map(|sig| sig.to_owned()))
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
let account = Account {
name,
from: account.name.as_ref().unwrap_or(&config.name).to_owned(),
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(),
notify_query: account
.notify_query
.as_ref()
.or_else(|| config.notify_query.as_ref())
.unwrap_or(&String::from("NEW"))
.to_owned(),
watch_cmds: account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.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(),
smtp_insecure: account.smtp_insecure.unwrap_or_default(),
smtp_login: account.smtp_login.to_owned(),
smtp_passwd_cmd: account.smtp_passwd_cmd.to_owned(),
pgp_encrypt_cmd: account.pgp_encrypt_cmd.to_owned(),
pgp_decrypt_cmd: account.pgp_decrypt_cmd.to_owned(),
};
trace!("account: {:?}", account);
Ok(account)
}
}

View file

@ -1,21 +0,0 @@
//! Module related to config CLI.
//!
//! This module provides arguments related to config.
use clap::Arg;
/// Config arguments.
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
vec![
Arg::with_name("config")
.long("config")
.short("c")
.help("Forces a specific config path")
.value_name("PATH"),
Arg::with_name("account")
.long("account")
.short("a")
.help("Selects a specific account")
.value_name("NAME"),
]
}

13
src/config/config_args.rs Normal file
View file

@ -0,0 +1,13 @@
//! This module provides arguments related to the user config.
use clap::Arg;
/// Represents the config path argument.
/// This argument allows the user to customize the config file path.
pub fn path_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("config")
.long("config")
.short("c")
.help("Forces a specific config path")
.value_name("PATH")
}

View file

@ -1,162 +0,0 @@
use anyhow::{Context, Error, Result};
use log::{debug, trace};
use serde::Deserialize;
use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf};
use toml;
use crate::output::run_cmd;
pub const DEFAULT_PAGE_SIZE: usize = 10;
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
/// Represent the user config.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
/// Defines the full display name of the user.
pub name: String,
/// Defines the downloads directory (eg. for attachments).
pub downloads_dir: Option<PathBuf>,
/// Overrides the default signature delimiter "`--\n `".
pub signature_delimiter: Option<String>,
/// Defines the signature.
pub signature: Option<String>,
/// 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>,
/// Customizes the IMAP query used to fetch new messages.
pub notify_query: Option<String>,
/// Defines the watch commands.
pub watch_cmds: Option<Vec<String>>,
#[serde(flatten)]
pub accounts: ConfigAccountsMap,
}
/// Represent the accounts section of the config.
pub type ConfigAccountsMap = HashMap<String, ConfigAccountEntry>;
/// Represent an account in the accounts section.
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigAccountEntry {
pub name: Option<String>,
pub downloads_dir: Option<PathBuf>,
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>,
/// Customizes the IMAP query used to fetch new messages.
pub notify_query: 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>,
pub smtp_insecure: Option<bool>,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
pub pgp_encrypt_cmd: Option<String>,
pub pgp_decrypt_cmd: Option<String>,
}
impl Config {
fn path_from_xdg() -> Result<PathBuf> {
let path = env::var("XDG_CONFIG_HOME").context("cannot find `XDG_CONFIG_HOME` env var")?;
let mut path = PathBuf::from(path);
path.push("himalaya");
path.push("config.toml");
Ok(path)
}
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let mut path: PathBuf = env::var(home_var)
.context(format!("cannot find `{}` env var", home_var))?
.into();
path.push(".config");
path.push("himalaya");
path.push("config.toml");
Ok(path)
}
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let mut path: PathBuf = env::var(home_var)
.context(format!("cannot find `{}` env var", home_var))?
.into();
path.push(".himalayarc");
Ok(path)
}
pub fn path() -> Result<PathBuf> {
let path = Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")?;
Ok(path)
}
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
let subject = subject.as_ref();
let sender = sender.as_ref();
let default_cmd = format!(r#"notify-send "New message from {}" "{}""#, sender, subject);
let cmd = self
.notify_cmd
.as_ref()
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
debug!("run command: {}", cmd);
run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
}
impl TryFrom<Option<&str>> for Config {
type Error = Error;
fn try_from(path: Option<&str>) -> Result<Self, Self::Error> {
debug!("init config from `{:?}`", path);
let path = path.map(|s| s.into()).unwrap_or(Config::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config = toml::from_str(&content).context("cannot parse config file")?;
trace!("{:#?}", config);
Ok(config)
}
}

View file

@ -0,0 +1,124 @@
use serde::Deserialize;
use std::path::PathBuf;
pub trait ToDeserializedBaseAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig;
}
/// Represents all existing kind of account config.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum DeserializedAccountConfig {
Imap(DeserializedImapAccountConfig),
Maildir(DeserializedMaildirAccountConfig),
}
impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig {
match self {
Self::Imap(config) => config.to_base(),
Self::Maildir(config) => config.to_base(),
}
}
}
macro_rules! make_account_config {
($AccountConfig:ident, $($element: ident: $ty: ty),*) => {
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct $AccountConfig {
/// Overrides the display name of the user for this account.
pub name: Option<String>,
/// Overrides the downloads directory (mostly for attachments).
pub downloads_dir: Option<PathBuf>,
/// Overrides the signature for this account.
pub signature: Option<String>,
/// Overrides the signature delimiter for this account.
pub signature_delimiter: Option<String>,
/// Overrides the default page size for this account.
pub default_page_size: Option<usize>,
/// Overrides the inbox folder name for this account.
pub inbox_folder: Option<String>,
/// Overrides the sent folder name for this account.
pub sent_folder: Option<String>,
/// Overrides the draft folder name for this account.
pub draft_folder: Option<String>,
/// Overrides the notify command for this account.
pub notify_cmd: Option<String>,
/// Overrides the IMAP query used to fetch new messages for this account.
pub notify_query: Option<String>,
/// Overrides the watch commands for this account.
pub watch_cmds: Option<Vec<String>>,
/// Makes this account the default one.
pub default: Option<bool>,
/// Represents the account email address.
pub email: String,
/// Represents the SMTP host.
pub smtp_host: String,
/// Represents the SMTP port.
pub smtp_port: u16,
/// Enables StartTLS.
pub smtp_starttls: Option<bool>,
/// Trusts any certificate.
pub smtp_insecure: Option<bool>,
/// Represents the SMTP login.
pub smtp_login: String,
/// Represents the SMTP password command.
pub smtp_passwd_cmd: String,
/// Represents the command used to encrypt a message.
pub pgp_encrypt_cmd: Option<String>,
/// Represents the command used to decrypt a message.
pub pgp_decrypt_cmd: Option<String>,
$(pub $element: $ty),*
}
impl ToDeserializedBaseAccountConfig for $AccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig {
DeserializedBaseAccountConfig {
name: self.name.clone(),
downloads_dir: self.downloads_dir.clone(),
signature: self.signature.clone(),
signature_delimiter: self.signature_delimiter.clone(),
default_page_size: self.default_page_size.clone(),
inbox_folder: self.inbox_folder.clone(),
sent_folder: self.sent_folder.clone(),
draft_folder: self.draft_folder.clone(),
notify_cmd: self.notify_cmd.clone(),
notify_query: self.notify_query.clone(),
watch_cmds: self.watch_cmds.clone(),
default: self.default.clone(),
email: self.email.clone(),
smtp_host: self.smtp_host.clone(),
smtp_port: self.smtp_port.clone(),
smtp_starttls: self.smtp_starttls.clone(),
smtp_insecure: self.smtp_insecure.clone(),
smtp_login: self.smtp_login.clone(),
smtp_passwd_cmd: self.smtp_passwd_cmd.clone(),
pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(),
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
}
}
}
}
}
make_account_config!(DeserializedBaseAccountConfig,);
make_account_config!(
DeserializedImapAccountConfig,
imap_host: String,
imap_port: u16,
imap_starttls: Option<bool>,
imap_insecure: Option<bool>,
imap_login: String,
imap_passwd_cmd: String
);
make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: PathBuf);

View file

@ -0,0 +1,103 @@
use anyhow::{Context, Result};
use log::{debug, info, trace};
use serde::Deserialize;
use std::{collections::HashMap, env, fs, path::PathBuf};
use toml;
use crate::config::DeserializedAccountConfig;
pub const DEFAULT_PAGE_SIZE: usize = 10;
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
/// Represents the user config file.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig {
/// Represents the display name of the user.
pub name: String,
/// Represents the downloads directory (mostly for attachments).
pub downloads_dir: Option<PathBuf>,
/// Represents the signature of the user.
pub signature: Option<String>,
/// Overrides the default signature delimiter "`--\n `".
pub signature_delimiter: Option<String>,
/// Represents the default page size for listings.
pub default_page_size: Option<usize>,
/// Overrides the default inbox folder name "INBOX".
pub inbox_folder: Option<String>,
/// Overrides the default sent folder name "Sent".
pub sent_folder: Option<String>,
/// Overrides the default draft folder name "Drafts".
pub draft_folder: Option<String>,
/// Represents the notify command.
pub notify_cmd: Option<String>,
/// Overrides the default IMAP query "NEW" used to fetch new messages
pub notify_query: Option<String>,
/// Represents the watch commands.
pub watch_cmds: Option<Vec<String>>,
/// Represents all the user accounts.
#[serde(flatten)]
pub accounts: HashMap<String, DeserializedAccountConfig>,
}
impl DeserializedConfig {
/// Tries to create a config from an optional path.
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
info!("begin: trying to parse config from path");
debug!("path: {:?}", path);
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config = toml::from_str(&content).context("cannot parse config file")?;
info!("end: trying to parse config from path");
trace!("config: {:?}", config);
Ok(config)
}
/// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable.
fn path_from_xdg() -> Result<PathBuf> {
let path =
env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?;
let path = PathBuf::from(path).join("himalaya").join("config.toml");
Ok(path)
}
/// Tries to get the XDG config file path from HOME environment variable.
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
let path = PathBuf::from(path)
.join(".config")
.join("himalaya")
.join("config.toml");
Ok(path)
}
/// Tries to get the .himalayarc config file path from HOME environment variable.
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
let path = PathBuf::from(path).join(".himalayarc");
Ok(path)
}
/// Tries to get the config file path.
pub fn path() -> Result<PathBuf> {
Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")
}
}

View file

@ -1,9 +1,11 @@
//! Module related to the user's configuration.
//! This barrel module provides everything related to the user configuration.
pub mod config_arg;
pub mod config_args;
pub mod deserialized_config;
pub use deserialized_config::*;
pub mod account_entity;
pub use account_entity::*;
pub mod config_entity;
pub use config_entity::*;
pub mod account_args;
pub mod account_config;
pub use account_config::*;
pub mod deserialized_account_config;
pub use deserialized_account_config::*;

View file

@ -1,27 +0,0 @@
//! Module related to IMAP handling.
//!
//! This module gathers all IMAP handlers triggered by the CLI.
use anyhow::Result;
use crate::{
config::{Account, Config},
domain::imap::ImapServiceInterface,
};
pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
keepalive: u64,
config: &Config,
account: &Account,
imap: &mut ImapService,
) -> Result<()> {
imap.notify(config, account, keepalive)
}
pub fn watch<'a, ImapService: ImapServiceInterface<'a>>(
keepalive: u64,
account: &Account,
imap: &mut ImapService,
) -> Result<()> {
imap.watch(account, keepalive)
}

View file

@ -1,416 +0,0 @@
//! Module related to IMAP servicing.
//!
//! This module exposes a service that can interact with IMAP servers.
use anyhow::{anyhow, Context, Result};
use log::{debug, log_enabled, trace, Level};
use native_tls::{TlsConnector, TlsStream};
use std::{collections::HashSet, convert::TryFrom, 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, account: &Account, 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(
&'a mut self,
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Envelopes>;
fn find_msg(&mut self, account: &Account, seq: &str) -> Result<Msg>;
fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>>;
fn append_msg(&mut self, mbox: &Mbox, account: &Account, msg: Msg) -> Result<()>;
fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()>;
fn expunge(&mut self) -> Result<()>;
fn logout(&mut self) -> Result<()>;
/// Add flags to all messages within the given sequence range.
fn add_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()>;
/// Replace flags of all messages within the given sequence range.
fn set_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()>;
/// Remove flags from all messages within the given sequence range.
fn remove_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()>;
}
pub struct ImapService<'a> {
account: &'a Account,
mbox: &'a Mbox<'a>,
sess: Option<ImapSession>,
/// Holds raw mailboxes fetched by the `imap` crate in order to extend mailboxes lifetime
/// outside of handlers. Without that, it would be impossible for handlers to return a `Mbox`
/// struct or a `Mboxes` struct due to the `ZeroCopy` constraint.
_raw_mboxes_cache: Option<RawMboxes>,
_raw_msgs_cache: Option<RawEnvelopes>,
}
impl<'a> ImapService<'a> {
fn sess(&mut self) -> Result<&mut ImapSession> {
if self.sess.is_none() {
debug!("create TLS builder");
debug!("insecure: {}", self.account.imap_insecure);
let builder = TlsConnector::builder()
.danger_accept_invalid_certs(self.account.imap_insecure)
.danger_accept_invalid_hostnames(self.account.imap_insecure)
.build()
.context("cannot create TLS connector")?;
debug!("create client");
debug!("host: {}", self.account.imap_host);
debug!("port: {}", self.account.imap_port);
debug!("starttls: {}", self.account.imap_starttls);
let mut client_builder =
imap::ClientBuilder::new(&self.account.imap_host, self.account.imap_port);
if self.account.imap_starttls {
client_builder.starttls();
}
let client = client_builder
.connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?))
.context("cannot connect to IMAP server")?;
debug!("create session");
debug!("login: {}", self.account.imap_login);
debug!("passwd cmd: {}", self.account.imap_passwd_cmd);
let mut sess = client
.login(&self.account.imap_login, &self.account.imap_passwd()?)
.map_err(|res| res.0)
.context("cannot login to IMAP server")?;
sess.debug = log_enabled!(Level::Trace);
self.sess = Some(sess);
}
match self.sess {
Some(ref mut sess) => Ok(sess),
None => Err(anyhow!("cannot get IMAP session")),
}
}
fn search_new_msgs(&mut self, account: &Account) -> Result<Vec<u32>> {
let uids: Vec<u32> = self
.sess()?
.uid_search(&account.notify_query)
.context("cannot search new messages")?
.into_iter()
.collect();
debug!("found {} new messages", uids.len());
trace!("uids: {:?}", uids);
Ok(uids)
}
}
impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
fn fetch_mboxes(&'a mut self) -> Result<Mboxes> {
let raw_mboxes = self
.sess()?
.list(Some(""), Some("*"))
.context("cannot list mailboxes")?;
self._raw_mboxes_cache = Some(raw_mboxes);
Ok(Mboxes::from(self._raw_mboxes_cache.as_ref().unwrap()))
}
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());
}
// TODO: add tests, improve error management when empty page
let range = if *page_size > 0 {
let cursor = (page * page_size) as i64;
let begin = 1.max(last_seq - cursor);
let end = begin - begin.min(*page_size as i64) + 1;
format!("{}:{}", end, begin)
} else {
String::from("1:*")
};
debug!("range: {}", range);
let fetches = self
.sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(format!(r#"cannot fetch messages within range "{}""#, range))?;
self._raw_msgs_cache = Some(fetches);
Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())
}
fn fetch_envelopes_with(
&'a mut self,
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Envelopes> {
let mbox = self.mbox.to_owned();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
let begin = page * page_size;
let end = begin + (page_size - 1);
let seqs: Vec<String> = self
.sess()?
.search(query)
.context(format!(
r#"cannot search in "{}" with query: "{}""#,
self.mbox.name, query
))?
.iter()
.map(|seq| seq.to_string())
.collect();
if seqs.is_empty() {
return Ok(Envelopes::default());
}
// FIXME: panic if begin > end
let range = seqs[begin..end.min(seqs.len())].join(",");
let fetches = self
.sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(r#"cannot fetch messages within range "{}""#)?;
self._raw_msgs_cache = Some(fetches);
Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())
}
/// Find a message by sequence number.
fn find_msg(&mut self, account: &Account, seq: &str) -> Result<Msg> {
let mbox = self.mbox.to_owned();
self.sess()?
.select(&mbox.name)
.context(format!("cannot select mailbox {}", self.mbox.name))?;
let fetches = self
.sess()?
.fetch(seq, "(ENVELOPE FLAGS INTERNALDATE BODY[])")
.context(r#"cannot fetch messages "{}""#)?;
let fetch = fetches
.first()
.ok_or_else(|| anyhow!(r#"cannot find message "{}"#, seq))?;
Msg::try_from((account, fetch))
}
fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>> {
let mbox = self.mbox.to_owned();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
let fetches = self
.sess()?
.fetch(seq, "BODY[]")
.context(r#"cannot fetch raw messages "{}""#)?;
let fetch = fetches
.first()
.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)
.flags(flags.0)
.finish()
.context(format!(r#"cannot append message to "{}""#, mbox.name))?;
Ok(())
}
fn append_msg(&mut self, mbox: &Mbox, account: &Account, msg: Msg) -> Result<()> {
let msg_raw = msg.into_sendable_msg(account)?.formatted();
self.sess()?
.append(&mbox.name, &msg_raw)
.flags(msg.flags.0)
.finish()
.context(format!(r#"cannot append message to "{}""#, mbox.name))?;
Ok(())
}
fn notify(&mut self, config: &Config, account: &Account, keepalive: u64) -> Result<()> {
debug!("notify");
let mbox = self.mbox.to_owned();
debug!("examine mailbox {:?}", mbox);
self.sess()?
.examine(&mbox.name)
.context(format!("cannot examine mailbox {}", self.mbox.name))?;
debug!("init messages hashset");
let mut msgs_set: HashSet<u32> = self
.search_new_msgs(account)?
.iter()
.cloned()
.collect::<HashSet<_>>();
trace!("messages hashset: {:?}", msgs_set);
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let uids: Vec<u32> = self
.search_new_msgs(account)?
.into_iter()
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
.collect();
debug!("found {} new messages not in hashset", uids.len());
trace!("messages hashet: {:?}", msgs_set);
if !uids.is_empty() {
let uids = uids
.iter()
.map(|uid| uid.to_string())
.collect::<Vec<_>>()
.join(",");
let fetches = self
.sess()?
.uid_fetch(uids, "(UID ENVELOPE)")
.context("cannot fetch new messages enveloppe")?;
for fetch in fetches.iter() {
let msg = Envelope::try_from(fetch)?;
let uid = fetch.uid.ok_or_else(|| {
anyhow!("cannot retrieve message {}'s UID", fetch.message)
})?;
let from = msg.sender.to_owned().into();
config.run_notify_cmd(&msg.subject, &from)?;
debug!("notify message: {}", uid);
trace!("message: {:?}", msg);
debug!("insert message {} in hashset", uid);
msgs_set.insert(uid);
trace!("messages hashset: {:?}", msgs_set);
}
}
debug!("end loop");
}
}
fn watch(&mut self, account: &Account, keepalive: u64) -> Result<()> {
debug!("examine mailbox: {}", &self.mbox.name);
let mbox = self.mbox.to_owned();
self.sess()?
.examine(&mbox.name)
.context(format!("cannot examine mailbox `{}`", &self.mbox.name))?;
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
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");
}
}
fn logout(&mut self) -> Result<()> {
if let Some(ref mut sess) = self.sess {
debug!("logout from IMAP server");
sess.logout().context("cannot logout from IMAP server")?;
}
Ok(())
}
fn add_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox;
let flags: String = flags.to_string();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()?
.store(seq_range, format!("+FLAGS ({})", flags))
.context(format!(r#"cannot add flags "{}""#, &flags))?;
Ok(())
}
fn set_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox;
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()?
.store(seq_range, format!("FLAGS ({})", flags))
.context(format!(r#"cannot set flags "{}""#, &flags))?;
Ok(())
}
fn remove_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox;
let flags = flags.to_string();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()?
.store(seq_range, format!("-FLAGS ({})", flags))
.context(format!(r#"cannot remove flags "{}""#, &flags))?;
Ok(())
}
fn expunge(&mut self) -> Result<()> {
self.sess()?
.expunge()
.context(format!(r#"cannot expunge mailbox "{}""#, self.mbox.name))?;
Ok(())
}
}
impl<'a> From<(&'a Account, &'a Mbox<'a>)> for ImapService<'a> {
fn from((account, mbox): (&'a Account, &'a Mbox)) -> Self {
Self {
account,
mbox,
sess: None,
_raw_mboxes_cache: None,
_raw_msgs_cache: None,
}
}
}

View file

@ -1,7 +0,0 @@
//! Module related to IMAP.
pub mod imap_arg;
pub mod imap_handler;
pub mod imap_service;
pub use imap_service::*;

View file

@ -1,70 +0,0 @@
//! Mailbox attribute entity module.
//!
//! This module contains the definition of the mailbox attribute and its traits implementations.
pub use imap::types::NameAttribute as AttrRemote;
use serde::Serialize;
use std::{
borrow::Cow,
fmt::{self, Display},
};
/// Wraps an `imap::types::NameAttribute`.
/// See https://serde.rs/remote-derive.html.
#[derive(Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(remote = "AttrRemote")]
pub enum AttrWrap<'a> {
NoInferiors,
NoSelect,
Marked,
Unmarked,
Custom(Cow<'a, str>),
}
/// Represents the mailbox attribute.
/// See https://serde.rs/remote-derive.html.
#[derive(Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Attr<'a>(#[serde(with = "AttrWrap")] pub AttrRemote<'a>);
/// Makes the attribute displayable.
impl<'a> Display for Attr<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
AttrRemote::NoInferiors => write!(f, "NoInferiors"),
AttrRemote::NoSelect => write!(f, "NoSelect"),
AttrRemote::Marked => write!(f, "Marked"),
AttrRemote::Unmarked => write!(f, "Unmarked"),
AttrRemote::Custom(cow) => write!(f, "{}", cow),
}
}
}
/// Converts an `imap::types::NameAttribute` into an attribute.
impl<'a> From<AttrRemote<'a>> for Attr<'a> {
fn from(attr: AttrRemote<'a>) -> Self {
Self(attr)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_display_attr() {
macro_rules! attr_from {
($attr:ident) => {
Attr(AttrRemote::$attr).to_string()
};
($custom:literal) => {
Attr(AttrRemote::Custom($custom.into())).to_string()
};
}
assert_eq!("NoInferiors", attr_from![NoInferiors]);
assert_eq!("NoSelect", attr_from![NoSelect]);
assert_eq!("Marked", attr_from![Marked]);
assert_eq!("Unmarked", attr_from![Unmarked]);
assert_eq!("CustomAttr", attr_from!["CustomAttr"]);
}
}

View file

@ -1,70 +0,0 @@
//! Mailbox attributes entity module.
//!
//! This module contains the definition of the mailbox attributes and its traits implementations.
use serde::Serialize;
use std::{
fmt::{self, Display},
ops::Deref,
};
use crate::domain::{Attr, AttrRemote};
/// Represents the attributes of the mailbox.
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct Attrs<'a>(Vec<Attr<'a>>);
/// Converts a vector of `imap::types::NameAttribute` into attributes.
impl<'a> From<Vec<AttrRemote<'a>>> for Attrs<'a> {
fn from(attrs: Vec<AttrRemote<'a>>) -> Self {
Self(attrs.into_iter().map(Attr::from).collect())
}
}
/// Derefs the attributes to its inner hashset.
impl<'a> Deref for Attrs<'a> {
type Target = Vec<Attr<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Makes the attributes displayable.
impl<'a> Display for Attrs<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut glue = "";
for attr in self.iter() {
write!(f, "{}{}", glue, attr)?;
glue = ", ";
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_display_attrs() {
macro_rules! attrs_from {
($($attr:expr),*) => {
Attrs::from(vec![$($attr,)*]).to_string()
};
}
let empty_attr = attrs_from![];
let single_attr = attrs_from![AttrRemote::NoInferiors];
let multiple_attrs = attrs_from![
AttrRemote::Custom("AttrCustom".into()),
AttrRemote::NoInferiors
];
assert_eq!("", empty_attr);
assert_eq!("NoInferiors", single_attr);
assert!(multiple_attrs.contains("NoInferiors"));
assert!(multiple_attrs.contains("AttrCustom"));
assert!(multiple_attrs.contains(","));
}
}

View file

@ -1,116 +0,0 @@
//! Mailbox entity module.
//!
//! This module contains the definition of the mailbox and its traits implementations.
use serde::Serialize;
use std::{
borrow::Cow,
fmt::{self, Display},
};
use crate::{
domain::Attrs,
ui::{Cell, Row, Table},
};
/// Represents a raw mailbox returned by the `imap` crate.
pub type RawMbox = imap::types::Name;
/// Represents a mailbox.
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct Mbox<'a> {
/// Represents the mailbox hierarchie delimiter.
pub delim: Cow<'a, str>,
/// Represents the mailbox name.
pub name: Cow<'a, str>,
/// Represents the mailbox attributes.
pub attrs: Attrs<'a>,
}
impl<'a> Mbox<'a> {
/// Creates a new mailbox with just a name.
pub fn new(name: &'a str) -> Self {
Self {
name: name.into(),
..Self::default()
}
}
}
/// Makes the mailbox displayable.
impl<'a> Display for Mbox<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
/// Makes the mailbox tableable.
impl<'a> Table for Mbox<'a> {
fn head() -> Row {
Row::new()
.cell(Cell::new("DELIM").bold().underline().white())
.cell(Cell::new("NAME").bold().underline().white())
.cell(
Cell::new("ATTRIBUTES")
.shrinkable()
.bold()
.underline()
.white(),
)
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.delim).white())
.cell(Cell::new(&self.name).green())
.cell(Cell::new(&self.attrs.to_string()).shrinkable().blue())
}
}
/// Converts an `imap::types::Name` into a mailbox.
impl<'a> From<&'a imap::types::Name> for Mbox<'a> {
fn from(raw_mbox: &'a imap::types::Name) -> Self {
Self {
delim: raw_mbox.delimiter().unwrap_or_default().into(),
name: raw_mbox.name().into(),
attrs: Attrs::from(raw_mbox.attributes().to_vec()),
}
}
}
#[cfg(test)]
mod tests {
use super::super::AttrRemote;
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(Mbox::default(), Mbox::new(""));
assert_eq!(
Mbox {
delim: Cow::default(),
name: "INBOX".into(),
attrs: Attrs::default()
},
Mbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = Mbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = Mbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = Mbox {
delim: ".".into(),
name: "Sent".into(),
attrs: Attrs::from(vec![AttrRemote::NoSelect]),
};
assert_eq!("Sent", full_mbox.to_string());
}
}

View file

@ -1,46 +0,0 @@
//! Mailboxes entity module.
//!
//! This module contains the definition of the mailboxes and its traits implementations.
use anyhow::Result;
use serde::Serialize;
use std::ops::Deref;
use crate::{
domain::{Mbox, RawMbox},
output::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
};
/// Represents a list of raw mailboxes returned by the `imap` crate.
pub(crate) type RawMboxes = imap::types::ZeroCopy<Vec<RawMbox>>;
/// Represents a list of mailboxes.
#[derive(Debug, Default, Serialize)]
pub struct Mboxes<'a>(pub Vec<Mbox<'a>>);
/// Derefs the mailboxes to its inner vector.
impl<'a> Deref for Mboxes<'a> {
type Target = Vec<Mbox<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Makes the mailboxes printable.
impl<'a> PrintTable for Mboxes<'a> {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
/// Converts a list of `imap::types::Name` into mailboxes.
impl<'a> From<&'a RawMboxes> for Mboxes<'a> {
fn from(raw_mboxes: &'a RawMboxes) -> Mboxes<'a> {
Self(raw_mboxes.iter().map(Mbox::from).collect())
}
}

View file

@ -1,18 +0,0 @@
//! Mailbox module.
//!
//! This module contains everything related to mailbox.
pub mod mbox_arg;
pub mod mbox_handler;
pub mod attr_entity;
pub use attr_entity::*;
pub mod attrs_entity;
pub use attrs_entity::*;
pub mod mbox_entity;
pub use mbox_entity::*;
pub mod mboxes_entity;
pub use mboxes_entity::*;

View file

@ -1,13 +0,0 @@
//! Domain-specific modules.
pub mod imap;
pub use self::imap::*;
pub mod mbox;
pub use mbox::*;
pub mod msg;
pub use msg::*;
pub mod smtp;
pub use smtp::*;

View file

@ -1,46 +0,0 @@
use anyhow::{Error, Result};
use serde::Serialize;
use std::{convert::TryFrom, ops::Deref};
use crate::{
domain::{msg::Envelope, RawEnvelope},
output::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
};
pub type RawEnvelopes = imap::types::ZeroCopy<Vec<RawEnvelope>>;
/// Representation of a list of envelopes.
#[derive(Debug, Default, Serialize)]
pub struct Envelopes<'a>(pub Vec<Envelope<'a>>);
impl<'a> Deref for Envelopes<'a> {
type Target = Vec<Envelope<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
type Error = Error;
fn try_from(fetches: &'a RawEnvelopes) -> Result<Self> {
let mut envelopes = vec![];
for fetch in fetches.iter().rev() {
envelopes.push(Envelope::try_from(fetch)?);
}
Ok(Self(envelopes))
}
}
impl<'a> PrintTable for Envelopes<'a> {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}

View file

@ -1,31 +0,0 @@
pub use imap::types::Flag;
use serde::ser::{Serialize, Serializer};
/// Represents a serializable `imap::types::Flag`.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SerializableFlag<'a>(pub &'a Flag<'a>);
/// Implements the serialize trait for `imap::types::Flag`.
/// Remote serialization cannot be used because of the [#[non_exhaustive]] directive of
/// `imap::types::Flag`.
///
/// [#[non_exhaustive]]: https://github.com/serde-rs/serde/issues/1991
impl<'a> Serialize for SerializableFlag<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(match self.0 {
Flag::Seen => "Seen",
Flag::Answered => "Answered",
Flag::Flagged => "Flagged",
Flag::Deleted => "Deleted",
Flag::Draft => "Draft",
Flag::Recent => "Recent",
Flag::MayCreate => "MayCreate",
Flag::Custom(cow) => cow,
// TODO: find a way to return an error
_ => "Unknown",
})
}
}

View file

@ -1,58 +0,0 @@
//! Message flag handling module.
//!
//! This module gathers all flag actions triggered by the CLI.
use anyhow::Result;
use crate::{
domain::{Flags, ImapServiceInterface},
output::PrinterService,
};
/// Adds flags to all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn add<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str,
flags: Vec<&'a str>,
printer: &'a mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.add_flags(seq_range, &flags)?;
printer.print(format!(
r#"Flag(s) "{}" successfully added to message(s) "{}""#,
flags, seq_range
))
}
/// Removes flags from all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn remove<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str,
flags: Vec<&'a str>,
printer: &'a mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.remove_flags(seq_range, &flags)?;
printer.print(format!(
r#"Flag(s) "{}" successfully removed from message(s) "{}""#,
flags, seq_range
))
}
/// Replaces flags of all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn set<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str,
flags: Vec<&'a str>,
printer: &'a mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.set_flags(seq_range, &flags)?;
printer.print(format!(
r#"Flag(s) "{}" successfully set for message(s) "{}""#,
flags, seq_range
))
}

View file

@ -1,197 +0,0 @@
use anyhow::{anyhow, Error, Result};
use serde::ser::{Serialize, SerializeSeq, Serializer};
use std::{
borrow::Cow,
collections::HashSet,
convert::{TryFrom, TryInto},
fmt::{self, Display},
ops::{Deref, DerefMut},
};
use crate::domain::msg::{Flag, SerializableFlag};
/// Represents the flags of the message.
/// A hashset is used to avoid duplicates.
#[derive(Debug, Clone, Default)]
pub struct Flags(pub HashSet<Flag<'static>>);
impl Flags {
/// Builds a symbols string based on flags contained in the hashset.
pub fn to_symbols_string(&self) -> String {
let mut flags = String::new();
flags.push_str(if self.contains(&Flag::Seen) {
" "
} else {
""
});
flags.push_str(if self.contains(&Flag::Answered) {
""
} else {
" "
});
flags.push_str(if self.contains(&Flag::Flagged) {
""
} else {
" "
});
flags
}
}
impl Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut glue = "";
for flag in &self.0 {
write!(f, "{}", glue)?;
match flag {
Flag::Seen => write!(f, "\\Seen")?,
Flag::Answered => write!(f, "\\Answered")?,
Flag::Flagged => write!(f, "\\Flagged")?,
Flag::Deleted => write!(f, "\\Deleted")?,
Flag::Draft => write!(f, "\\Draft")?,
Flag::Recent => write!(f, "\\Recent")?,
Flag::MayCreate => write!(f, "\\MayCreate")?,
Flag::Custom(cow) => write!(f, "{}", cow)?,
_ => (),
}
glue = " ";
}
Ok(())
}
}
impl<'a> TryFrom<Vec<Flag<'a>>> for Flags {
type Error = Error;
fn try_from(flags: Vec<Flag<'a>>) -> Result<Flags> {
let mut set: HashSet<Flag<'static>> = HashSet::new();
for flag in flags {
set.insert(match flag {
Flag::Seen => Flag::Seen,
Flag::Answered => Flag::Answered,
Flag::Flagged => Flag::Flagged,
Flag::Deleted => Flag::Deleted,
Flag::Draft => Flag::Draft,
Flag::Recent => Flag::Recent,
Flag::MayCreate => Flag::MayCreate,
Flag::Custom(cow) => Flag::Custom(Cow::Owned(cow.to_string())),
flag => return Err(anyhow!(r#"cannot parse flag "{}""#, flag)),
});
}
Ok(Self(set))
}
}
impl<'a> TryFrom<&'a [Flag<'a>]> for Flags {
type Error = Error;
fn try_from(flags: &'a [Flag<'a>]) -> Result<Flags> {
flags.to_vec().try_into()
}
}
impl Deref for Flags {
type Target = HashSet<Flag<'static>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Flags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Serialize for Flags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for flag in &self.0 {
seq.serialize_element(&SerializableFlag(flag))?;
}
seq.end()
}
}
impl<'a> From<Vec<&'a str>> for Flags {
fn from(flags: Vec<&'a str>) -> Self {
let mut map: HashSet<Flag<'static>> = HashSet::new();
for f in flags {
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()))),
};
}
Self(map)
}
}
// FIXME
//#[cfg(test)]
//mod tests {
// use crate::domain::msg::flag::entity::Flags;
// use imap::types::Flag;
// use std::collections::HashSet;
// #[test]
// fn test_get_signs() {
// let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
// assert_eq!(flags.to_symbols_string(), " ↵ ".to_string());
// }
// #[test]
// fn test_from_string() {
// let flags = Flags::from("Seen Answered");
// let expected = Flags::from(vec![Flag::Seen, Flag::Answered]);
// assert_eq!(flags, expected);
// }
// #[test]
// fn test_to_string() {
// let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
// // since we can't influence the order in the HashSet, we're gonna convert it into a vec,
// // sort it according to the names and compare it aftwards.
// let flag_string = flags.to_string();
// let mut flag_vec: Vec<String> = flag_string
// .split_ascii_whitespace()
// .map(|word| word.to_string())
// .collect();
// flag_vec.sort();
// assert_eq!(
// flag_vec,
// vec!["\\Answered".to_string(), "\\Seen".to_string()]
// );
// }
// #[test]
// fn test_from_vec() {
// let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
// let mut expected = HashSet::new();
// expected.insert(Flag::Seen);
// expected.insert(Flag::Answered);
// assert_eq!(flags.0, expected);
// }
//}

View file

@ -1,50 +0,0 @@
//! This module holds everything which is related to a **Msg**/**Mail**. Here are
//! structs which **represent the data** in Msgs/Mails.
/// Includes the following subcommands:
/// - `list`
/// - `search`
/// - `write`
/// - `send`
/// - `save`
/// - `read`
/// - `attachments`
/// - `reply`
/// - `forward`
/// - `copy`
/// - `move`
/// - `delete`
/// - `template`
///
/// Execute `himalaya help <cmd>` where `<cmd>` is one entry of this list above
/// to get more information about them.
pub mod msg_arg;
pub mod msg_handler;
pub mod msg_utils;
pub mod flag_arg;
pub mod flag_handler;
pub mod flag_entity;
pub use flag_entity::*;
pub mod flags_entity;
pub use flags_entity::*;
pub mod envelope_entity;
pub use envelope_entity::*;
pub mod envelopes_entity;
pub use envelopes_entity::*;
pub mod tpl_arg;
pub use tpl_arg::TplOverride;
pub mod tpl_handler;
pub mod msg_entity;
pub use msg_entity::*;
pub mod parts_entity;
pub use parts_entity::*;

View file

@ -1,365 +0,0 @@
//! Module related to message handling.
//!
//! This module gathers all message commands.
use anyhow::{Context, Result};
use atty::Stream;
use imap::types::Flag;
use log::{debug, info, trace};
use std::{
borrow::Cow,
convert::{TryFrom, TryInto},
fs,
io::{self, BufRead},
};
use url::Url;
use crate::{
config::Account,
domain::{
imap::ImapServiceInterface,
mbox::Mbox,
msg::{Flags, Msg, Part, TextPlainPart},
smtp::SmtpServiceInterface,
Parts,
},
output::{PrintTableOpts, PrinterService},
};
/// Download all message attachments to the user account downloads directory.
pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
) -> Result<()> {
let attachments = imap.find_msg(account, seq)?.attachments();
let attachments_len = attachments.len();
debug!(
r#"{} attachment(s) found for message "{}""#,
attachments_len, seq
);
for attachment in attachments {
let filepath = account.downloads_dir.join(&attachment.filename);
debug!("downloading {}…", attachment.filename);
fs::write(&filepath, &attachment.content)
.context(format!("cannot download attachment {:?}", filepath))?;
}
printer.print(format!(
"{} attachment(s) successfully downloaded to {:?}",
attachments_len, account.downloads_dir
))
}
/// Copy a message from a mailbox to another.
pub fn copy<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str,
mbox: &str,
printer: &mut Printer,
imap: &mut ImapService,
) -> Result<()> {
let mbox = Mbox::new(mbox);
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!(
r#"Message {} successfully copied to folder "{}""#,
seq, mbox
))
}
/// Delete messages matching the given sequence range.
pub fn delete<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str,
printer: &mut Printer,
imap: &mut ImapService,
) -> Result<()> {
let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?;
imap.add_flags(seq, &flags)?;
imap.expunge()?;
printer.print(format!(r#"Message(s) {} successfully deleted"#, seq))
}
/// Forward the given message UID from the selected mailbox.
pub fn forward<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
seq: &str,
attachments_paths: Vec<&str>,
encrypt: bool,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
imap.find_msg(account, seq)?
.into_forward(account)?
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(account, printer, imap, smtp)
}
/// List paginated messages from the selected mailbox.
pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
account: &Account,
printer: &mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size);
let msgs = imap.fetch_envelopes(&page_size, &page)?;
trace!("messages: {:#?}", msgs);
printer.print_table(msgs, PrintTableOpts { max_width })
}
/// Parses and edits a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
url: &Url,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
info!("entering mailto command handler");
let to: Vec<lettre::message::Mailbox> = url
.path()
.split(';')
.filter_map(|s| s.parse().ok())
.collect();
let mut cc = Vec::new();
let mut bcc = Vec::new();
let mut subject = Cow::default();
let mut body = Cow::default();
for (key, val) in url.query_pairs() {
match key.as_bytes() {
b"cc" => {
cc.push(val.parse()?);
}
b"bcc" => {
bcc.push(val.parse()?);
}
b"subject" => {
subject = val;
}
b"body" => {
body = val;
}
_ => (),
}
}
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()
};
trace!("message: {:?}", msg);
msg.edit_with_editor(account, printer, imap, smtp)
}
/// Move a message from a mailbox to another.
pub fn move_<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
// The sequence number of the message to move
seq: &str,
// The mailbox to move the message in
mbox: &str,
printer: &mut Printer,
imap: &mut ImapService,
) -> Result<()> {
// Copy the message to targetted mailbox
let mbox = Mbox::new(mbox);
let msg = imap.find_raw_msg(seq)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
// Delete the original message
let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?;
imap.add_flags(seq, &flags)?;
imap.expunge()?;
printer.print(format!(
r#"Message {} successfully moved to folder "{}""#,
seq, mbox
))
}
/// Read a message by its sequence number.
pub fn read<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str,
text_mime: &str,
raw: bool,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
) -> 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()
} else {
imap.find_msg(account, seq)?.fold_text_parts(text_mime)
};
printer.print(msg)
}
/// Reply to the given message UID.
pub fn reply<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
seq: &str,
all: bool,
attachments_paths: Vec<&str>,
encrypt: bool,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
imap.find_msg(account, seq)?
.into_reply(all, account)?
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(account, printer, imap, smtp)?;
let flags = Flags::try_from(vec![Flag::Answered])?;
imap.add_flags(seq, &flags)
}
/// Saves a raw message to the targetted mailbox.
pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
mbox: &Mbox,
raw_msg: &str,
printer: &mut Printer,
imap: &mut ImapService,
) -> Result<()> {
info!("entering save message handler");
debug!("mailbox: {}", mbox);
let flags = Flags::try_from(vec![Flag::Seen])?;
debug!("flags: {}", flags);
let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json();
debug!("is json: {}", is_json);
let raw_msg = if is_tty || is_json {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
imap.append_raw_msg_with_flags(mbox, raw_msg.as_bytes(), flags)
}
/// Paginate messages from the selected mailbox matching the specified query.
pub fn search<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
account: &Account,
printer: &mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size);
let msgs = imap.fetch_envelopes_with(&query, &page_size, &page)?;
trace!("messages: {:#?}", msgs);
printer.print_table(msgs, PrintTableOpts { max_width })
}
/// Send a raw message.
pub fn send<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
raw_msg: &str,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
info!("entering send message handler");
let mbox = Mbox::new(&account.sent_folder);
debug!("mailbox: {}", mbox);
let flags = Flags::try_from(vec![Flag::Seen])?;
debug!("flags: {}", flags);
let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json();
debug!("is json: {}", is_json);
let raw_msg = if is_tty || is_json {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.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())?;
imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags)
}
/// Compose a new message.
pub fn write<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
attachments_paths: Vec<&str>,
encrypt: bool,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
Msg::default()
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(account, printer, imap, smtp)
}

View file

@ -1,120 +0,0 @@
//! Module related to message template handling.
//!
//! This module gathers all message template commands.
use anyhow::Result;
use atty::Stream;
use imap::types::Flag;
use std::{
convert::TryFrom,
io::{self, BufRead},
};
use crate::{
config::Account,
domain::{
imap::ImapServiceInterface,
msg::{Msg, TplOverride},
Flags, Mbox, SmtpServiceInterface,
},
output::PrinterService,
};
/// Generate a new message template.
pub fn new<'a, Printer: PrinterService>(
opts: TplOverride<'a>,
account: &'a Account,
printer: &'a mut Printer,
) -> Result<()> {
let tpl = Msg::default().to_tpl(opts, account);
printer.print(tpl)
}
/// Generate a reply message template.
pub fn reply<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str,
all: bool,
opts: TplOverride<'a>,
account: &'a Account,
printer: &'a mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let tpl = imap
.find_msg(account, seq)?
.into_reply(all, account)?
.to_tpl(opts, account);
printer.print(tpl)
}
/// Generate a forward message template.
pub fn forward<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
seq: &str,
opts: TplOverride<'a>,
account: &'a Account,
printer: &'a mut Printer,
imap: &'a mut ImapService,
) -> Result<()> {
let tpl = imap
.find_msg(account, seq)?
.into_forward(account)?
.to_tpl(opts, account);
printer.print(tpl)
}
/// Saves a message based on a template.
pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
mbox: &Mbox,
account: &Account,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut Printer,
imap: &mut ImapService,
) -> Result<()> {
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
};
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
let raw_msg = msg.into_sendable_msg(account)?.formatted();
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(mbox, &raw_msg, flags)?;
printer.print("Template successfully saved")
}
/// Sends a message based on a template.
pub fn send<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
mbox: &Mbox,
account: &Account,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
};
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
let sent_msg = smtp.send_msg(account, &msg)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(mbox, &sent_msg.formatted(), flags)?;
printer.print("Template successfully sent")
}

View file

@ -1,4 +0,0 @@
//! Module related to SMTP.
pub mod smtp_service;
pub use smtp_service::*;

88
src/lib.rs Normal file
View file

@ -0,0 +1,88 @@
pub mod mbox {
pub mod mbox;
pub use mbox::*;
pub mod mbox_arg;
pub mod mbox_handler;
}
pub mod msg {
pub mod envelope;
pub use envelope::*;
pub mod msg_arg;
pub mod msg_handler;
pub mod msg_utils;
pub mod flag_arg;
pub mod flag_handler;
pub mod tpl_arg;
pub use tpl_arg::TplOverride;
pub mod tpl_handler;
pub mod msg_entity;
pub use msg_entity::*;
pub mod parts_entity;
pub use parts_entity::*;
pub mod addr_entity;
pub use addr_entity::*;
}
pub mod backends {
pub use backend::*;
pub mod backend;
pub use self::imap::*;
pub mod imap {
pub mod imap_arg;
pub use imap_backend::*;
pub mod imap_backend;
pub mod imap_handler;
pub use imap_mbox::*;
pub mod imap_mbox;
pub use imap_mbox_attr::*;
pub mod imap_mbox_attr;
pub use imap_envelope::*;
pub mod imap_envelope;
pub use imap_flag::*;
pub mod imap_flag;
pub mod msg_sort_criterion;
}
pub use self::maildir::*;
pub mod maildir {
pub mod maildir_backend;
pub use maildir_backend::*;
pub mod maildir_mbox;
pub use maildir_mbox::*;
pub mod maildir_envelope;
pub use maildir_envelope::*;
pub mod maildir_flag;
pub use maildir_flag::*;
}
}
pub mod smtp {
pub mod smtp_service;
pub use smtp_service::*;
}
pub mod compl;
pub mod config;
pub mod output;
pub mod ui;

View file

@ -1,23 +1,16 @@
use anyhow::Result;
use output::StdoutPrinter;
use std::{convert::TryFrom, env};
use url::Url;
mod compl;
mod config;
mod domain;
mod output;
mod ui;
use compl::{compl_arg, compl_handler};
use config::{config_arg, Account, Config};
use domain::{
imap::{imap_arg, imap_handler, ImapService, ImapServiceInterface},
mbox::{mbox_arg, mbox_handler, Mbox},
use himalaya::{
backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend},
compl::{compl_arg, compl_handler},
config::{account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig},
mbox::{mbox_arg, mbox_handler},
msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler},
smtp::SmtpService,
output::{output_arg, OutputFmt, StdoutPrinter},
smtp::LettreService,
};
use output::{output_arg, OutputFmt};
fn create_app<'a>() -> clap::App<'a, 'a> {
clap::App::new(env!("CARGO_PKG_NAME"))
@ -25,7 +18,8 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
.about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS"))
.global_setting(clap::AppSettings::GlobalVersion)
.args(&config_arg::args())
.arg(&config_args::path_arg())
.arg(&account_args::name_arg())
.args(&output_arg::args())
.arg(mbox_arg::source_arg())
.subcommands(compl_arg::subcmds())
@ -42,14 +36,27 @@ 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 config = Config::try_from(None)?;
let account = Account::try_from((&config, None))?;
let mbox = Mbox::new(&account.inbox_folder);
let config = DeserializedConfig::from_opt_path(None)?;
let (account_config, backend_config) =
AccountConfig::from_config_and_opt_account_name(&config, None)?;
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
let url = Url::parse(&raw_args[1])?;
let mut imap = ImapService::from((&account, &mbox));
let mut smtp = SmtpService::from(&account);
return msg_handler::mailto(&url, &account, &mut printer, &mut imap, &mut smtp);
let mut smtp = LettreService::from(&account_config);
let mut imap;
let mut maildir;
let backend: Box<&mut dyn Backend> = match backend_config {
BackendConfig::Imap(ref imap_config) => {
imap = ImapBackend::new(&account_config, imap_config);
Box::new(&mut imap)
}
BackendConfig::Maildir(ref maildir_config) => {
maildir = MaildirBackend::new(&account_config, maildir_config);
Box::new(&mut maildir)
}
};
return msg_handler::mailto(&url, &account_config, &mut printer, backend, &mut smtp);
}
let app = create_app();
@ -65,135 +72,192 @@ fn main() -> Result<()> {
}
// Init entities and services.
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 config = DeserializedConfig::from_opt_path(m.value_of("config"))?;
let (account_config, backend_config) =
AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?;
let mbox = m
.value_of("mbox-source")
.unwrap_or(&account_config.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);
let mut imap;
let mut maildir;
let backend: Box<&mut dyn Backend> = match backend_config {
BackendConfig::Imap(ref imap_config) => {
imap = ImapBackend::new(&account_config, imap_config);
Box::new(&mut imap)
}
BackendConfig::Maildir(ref maildir_config) => {
maildir = MaildirBackend::new(&account_config, maildir_config);
Box::new(&mut maildir)
}
};
let mut smtp = LettreService::from(&account_config);
// Check IMAP commands.
match imap_arg::matches(&m)? {
Some(imap_arg::Command::Notify(keepalive)) => {
return imap_handler::notify(keepalive, &config, &account, &mut imap);
if let BackendConfig::Imap(ref imap_config) = backend_config {
let mut imap = ImapBackend::new(&account_config, imap_config);
match imap_arg::matches(&m)? {
Some(imap_arg::Command::Notify(keepalive)) => {
return imap_handler::notify(keepalive, mbox, &mut imap);
}
Some(imap_arg::Command::Watch(keepalive)) => {
return imap_handler::watch(keepalive, mbox, &mut imap);
}
_ => (),
}
Some(imap_arg::Command::Watch(keepalive)) => {
return imap_handler::watch(keepalive, &account, &mut imap);
}
_ => (),
}
// Check mailbox commands.
match mbox_arg::matches(&m)? {
Some(mbox_arg::Cmd::List(max_width)) => {
return mbox_handler::list(max_width, &mut printer, &mut imap);
return mbox_handler::list(max_width, &mut printer, backend);
}
_ => (),
}
// Check message commands.
match msg_arg::matches(&m)? {
Some(msg_arg::Command::Attachments(seq)) => {
return msg_handler::attachments(seq, &account, &mut printer, &mut imap);
Some(msg_arg::Cmd::Attachments(seq)) => {
return msg_handler::attachments(seq, mbox, &account_config, &mut printer, backend);
}
Some(msg_arg::Command::Copy(seq, mbox)) => {
return msg_handler::copy(seq, mbox, &mut printer, &mut imap);
Some(msg_arg::Cmd::Copy(seq, mbox_dst)) => {
return msg_handler::copy(seq, mbox, mbox_dst, &mut printer, backend);
}
Some(msg_arg::Command::Delete(seq)) => {
return msg_handler::delete(seq, &mut printer, &mut imap);
Some(msg_arg::Cmd::Delete(seq)) => {
return msg_handler::delete(seq, mbox, &mut printer, backend);
}
Some(msg_arg::Command::Forward(seq, attachment_paths, encrypt)) => {
Some(msg_arg::Cmd::Forward(seq, attachment_paths, encrypt)) => {
return msg_handler::forward(
seq,
attachment_paths,
encrypt,
&account,
mbox,
&account_config,
&mut printer,
&mut imap,
backend,
&mut smtp,
);
}
Some(msg_arg::Command::List(max_width, page_size, page)) => {
Some(msg_arg::Cmd::List(max_width, page_size, page)) => {
return msg_handler::list(
max_width,
page_size,
page,
&account,
mbox,
&account_config,
&mut printer,
&mut imap,
backend,
);
}
Some(msg_arg::Command::Move(seq, mbox)) => {
return msg_handler::move_(seq, mbox, &mut printer, &mut imap);
Some(msg_arg::Cmd::Move(seq, mbox_dst)) => {
return msg_handler::move_(seq, mbox, mbox_dst, &mut printer, backend);
}
Some(msg_arg::Command::Read(seq, text_mime, raw)) => {
return msg_handler::read(seq, text_mime, raw, &account, &mut printer, &mut imap);
Some(msg_arg::Cmd::Read(seq, text_mime, raw)) => {
return msg_handler::read(seq, text_mime, raw, mbox, &mut printer, backend);
}
Some(msg_arg::Command::Reply(seq, all, attachment_paths, encrypt)) => {
Some(msg_arg::Cmd::Reply(seq, all, attachment_paths, encrypt)) => {
return msg_handler::reply(
seq,
all,
attachment_paths,
encrypt,
&account,
mbox,
&account_config,
&mut printer,
&mut imap,
backend,
&mut smtp,
);
}
Some(msg_arg::Command::Save(raw_msg)) => {
return msg_handler::save(&mbox, raw_msg, &mut printer, &mut imap);
Some(msg_arg::Cmd::Save(raw_msg)) => {
return msg_handler::save(mbox, raw_msg, &mut printer, backend);
}
Some(msg_arg::Command::Search(query, max_width, page_size, page)) => {
Some(msg_arg::Cmd::Search(query, max_width, page_size, page)) => {
return msg_handler::search(
query,
max_width,
page_size,
page,
&account,
mbox,
&account_config,
&mut printer,
&mut imap,
backend,
);
}
Some(msg_arg::Command::Send(raw_msg)) => {
return msg_handler::send(raw_msg, &account, &mut printer, &mut imap, &mut smtp);
Some(msg_arg::Cmd::Sort(criteria, query, max_width, page_size, page)) => {
return msg_handler::sort(
criteria,
query,
max_width,
page_size,
page,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(msg_arg::Command::Write(atts, encrypt)) => {
return msg_handler::write(atts, encrypt, &account, &mut printer, &mut imap, &mut smtp);
Some(msg_arg::Cmd::Send(raw_msg)) => {
return msg_handler::send(raw_msg, &account_config, &mut printer, backend, &mut smtp);
}
Some(msg_arg::Command::Flag(m)) => match m {
Some(flag_arg::Command::Set(seq_range, flags)) => {
return flag_handler::set(seq_range, flags, &mut printer, &mut imap);
Some(msg_arg::Cmd::Write(atts, encrypt)) => {
return msg_handler::write(
atts,
encrypt,
&account_config,
&mut printer,
backend,
&mut smtp,
);
}
Some(msg_arg::Cmd::Flag(m)) => match m {
Some(flag_arg::Cmd::Set(seq_range, flags)) => {
return flag_handler::set(seq_range, mbox, &flags, &mut printer, backend);
}
Some(flag_arg::Command::Add(seq_range, flags)) => {
return flag_handler::add(seq_range, flags, &mut printer, &mut imap);
Some(flag_arg::Cmd::Add(seq_range, flags)) => {
return flag_handler::add(seq_range, mbox, &flags, &mut printer, backend);
}
Some(flag_arg::Command::Remove(seq_range, flags)) => {
return flag_handler::remove(seq_range, flags, &mut printer, &mut imap);
Some(flag_arg::Cmd::Remove(seq_range, flags)) => {
return flag_handler::remove(seq_range, mbox, &flags, &mut printer, backend);
}
_ => (),
},
Some(msg_arg::Command::Tpl(m)) => match m {
Some(tpl_arg::Command::New(tpl)) => {
return tpl_handler::new(tpl, &account, &mut printer);
Some(msg_arg::Cmd::Tpl(m)) => match m {
Some(tpl_arg::Cmd::New(tpl)) => {
return tpl_handler::new(tpl, &account_config, &mut printer);
}
Some(tpl_arg::Command::Reply(seq, all, tpl)) => {
return tpl_handler::reply(seq, all, tpl, &account, &mut printer, &mut imap);
Some(tpl_arg::Cmd::Reply(seq, all, tpl)) => {
return tpl_handler::reply(
seq,
all,
tpl,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(tpl_arg::Command::Forward(seq, tpl)) => {
return tpl_handler::forward(seq, tpl, &account, &mut printer, &mut imap);
Some(tpl_arg::Cmd::Forward(seq, tpl)) => {
return tpl_handler::forward(
seq,
tpl,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(tpl_arg::Command::Save(atts, tpl)) => {
return tpl_handler::save(&mbox, &account, atts, tpl, &mut printer, &mut imap);
Some(tpl_arg::Cmd::Save(atts, tpl)) => {
return tpl_handler::save(mbox, &account_config, atts, tpl, &mut printer, backend);
}
Some(tpl_arg::Command::Send(atts, tpl)) => {
Some(tpl_arg::Cmd::Send(atts, tpl)) => {
return tpl_handler::send(
&mbox,
&account,
mbox,
&account_config,
atts,
tpl,
&mut printer,
&mut imap,
backend,
&mut smtp,
);
}
@ -202,5 +266,5 @@ fn main() -> Result<()> {
_ => (),
}
imap.logout()
backend.disconnect()
}

7
src/mbox/mbox.rs Normal file
View file

@ -0,0 +1,7 @@
use std::fmt;
use crate::output::PrintTable;
pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable {
//
}

View file

@ -6,31 +6,31 @@ use anyhow::Result;
use log::{info, trace};
use crate::{
domain::ImapServiceInterface,
backends::Backend,
output::{PrintTableOpts, PrinterService},
};
/// Lists all mailboxes.
pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
printer: &mut Printer,
imap: &'a mut ImapService,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
info!("entering list mailbox handler");
let mboxes = imap.fetch_mboxes()?;
let mboxes = backend.get_mboxes()?;
trace!("mailboxes: {:?}", mboxes);
printer.print_table(mboxes, PrintTableOpts { max_width })
}
#[cfg(test)]
mod tests {
use serde::Serialize;
use std::{fmt::Debug, io};
use termcolor::ColorSpec;
use crate::{
config::{Account, Config},
domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg},
backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes},
mbox::Mboxes,
msg::{Envelopes, Msg},
output::{Print, PrintTable, WriteColor},
};
@ -78,15 +78,15 @@ mod tests {
}
impl PrinterService for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + Serialize>(
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: T,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writter, opts)?;
Ok(())
}
fn print<T: Serialize + Print>(&mut self, _data: T) -> Result<()> {
fn print<T: serde::Serialize + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
@ -94,72 +94,73 @@ mod tests {
}
}
struct ImapServiceTest;
struct TestBackend;
impl<'a> ImapServiceInterface<'a> for ImapServiceTest {
fn fetch_mboxes(&'a mut self) -> Result<Mboxes> {
Ok(Mboxes(vec![
Mbox {
impl<'a> Backend<'a> for TestBackend {
fn add_mbox(&mut self, _: &str) -> Result<()> {
unimplemented!();
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
Ok(Box::new(ImapMboxes(vec![
ImapMbox {
delim: "/".into(),
name: "INBOX".into(),
attrs: Attrs::from(vec![AttrRemote::NoSelect]),
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
},
Mbox {
ImapMbox {
delim: "/".into(),
name: "Sent".into(),
attrs: Attrs::from(vec![
AttrRemote::NoInferiors,
AttrRemote::Custom("HasNoChildren".into()),
attrs: ImapMboxAttrs(vec![
ImapMboxAttr::NoInferiors,
ImapMboxAttr::Custom("HasNoChildren".into()),
]),
},
]))
])))
}
fn notify(&mut self, _: &Config, _: &Account, _: u64) -> Result<()> {
fn del_mbox(&mut self, _: &str) -> Result<()> {
unimplemented!();
}
fn get_envelopes(
&mut self,
_: &str,
_: &str,
_: &str,
_: usize,
_: usize,
) -> Result<Box<dyn Envelopes>> {
unimplemented!()
}
fn watch(&mut self, _: &Account, _: u64) -> Result<()> {
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result<Box<dyn ToString>> {
unimplemented!()
}
fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
fn get_msg(&mut self, _: &str, _: &str) -> Result<Msg> {
unimplemented!()
}
fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn find_msg(&mut self, _: &Account, _: &str) -> Result<Msg> {
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> {
fn del_msg(&mut self, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn append_msg(&mut self, _: &Mbox, _: &Account, _: Msg) -> Result<()> {
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> {
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn expunge(&mut self) -> Result<()> {
unimplemented!()
}
fn logout(&mut self) -> Result<()> {
unimplemented!()
}
fn add_flags(&mut self, _: &str, _: &Flags) -> Result<()> {
unimplemented!()
}
fn set_flags(&mut self, _: &str, _: &Flags) -> Result<()> {
unimplemented!()
}
fn remove_flags(&mut self, _: &str, _: &Flags) -> Result<()> {
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
}
let mut printer = PrinterServiceTest::default();
let mut imap = ImapServiceTest {};
let mut backend = TestBackend {};
let backend = Box::new(&mut backend);
assert!(list(None, &mut printer, &mut imap).is_ok());
assert!(list(None, &mut printer, backend).is_ok());
assert_eq!(
concat![
"\n",

133
src/msg/addr_entity.rs Normal file
View file

@ -0,0 +1,133 @@
//! Module related to email addresses.
//!
//! This module regroups email address entities and converters.
use anyhow::{Context, Result};
use log::trace;
use mailparse;
use std::fmt::Debug;
/// Defines a single email address.
pub type Addr = mailparse::MailAddr;
/// Defines a list of email addresses.
pub type Addrs = mailparse::MailAddrList;
/// Converts a slice into an optional list of addresses.
pub fn from_slice_to_addrs<S: AsRef<str> + Debug>(addrs: S) -> Result<Option<Addrs>> {
let addrs = mailparse::addrparse(addrs.as_ref())?;
Ok(if addrs.is_empty() { None } else { Some(addrs) })
}
/// Converts a list of addresses into a list of [`lettre::message::Mailbox`].
pub fn from_addrs_to_sendable_mbox(addrs: &Addrs) -> Result<Vec<lettre::message::Mailbox>> {
let mut sendable_addrs: Vec<lettre::message::Mailbox> = vec![];
for addr in addrs.iter() {
match addr {
Addr::Single(mailparse::SingleInfo { display_name, addr }) => sendable_addrs.push(
lettre::message::Mailbox::new(display_name.clone(), addr.parse()?),
),
Addr::Group(mailparse::GroupInfo { group_name, addrs }) => {
for addr in addrs {
sendable_addrs.push(lettre::message::Mailbox::new(
addr.display_name.clone().or(Some(group_name.clone())),
addr.to_string().parse()?,
))
}
}
}
}
Ok(sendable_addrs)
}
/// Converts a list of addresses into a list of [`lettre::Address`].
pub fn from_addrs_to_sendable_addrs(addrs: &Addrs) -> Result<Vec<lettre::Address>> {
let mut sendable_addrs = vec![];
for addr in addrs.iter() {
match addr {
mailparse::MailAddr::Single(mailparse::SingleInfo {
display_name: _,
addr,
}) => {
sendable_addrs.push(addr.parse()?);
}
mailparse::MailAddr::Group(mailparse::GroupInfo {
group_name: _,
addrs,
}) => {
for addr in addrs {
sendable_addrs.push(addr.addr.parse()?);
}
}
};
}
Ok(sendable_addrs)
}
/// Converts a [`imap_proto::Address`] into an address.
pub fn from_imap_addr_to_addr(addr: &imap_proto::Address) -> Result<Addr> {
let name = addr
.name
.as_ref()
.map(|name| {
rfc2047_decoder::decode(&name.to_vec())
.context("cannot decode address name")
.map(Some)
})
.unwrap_or(Ok(None))?;
let mbox = addr
.mailbox
.as_ref()
.map(|mbox| {
rfc2047_decoder::decode(&mbox.to_vec())
.context("cannot decode address mailbox")
.map(Some)
})
.unwrap_or(Ok(None))?;
let host = addr
.host
.as_ref()
.map(|host| {
rfc2047_decoder::decode(&host.to_vec())
.context("cannot decode address host")
.map(Some)
})
.unwrap_or(Ok(None))?;
trace!("parsing address from imap address");
trace!("name: {:?}", name);
trace!("mbox: {:?}", mbox);
trace!("host: {:?}", host);
Ok(Addr::Single(mailparse::SingleInfo {
display_name: name,
addr: match host {
Some(host) => format!("{}@{}", mbox.unwrap_or_default(), host),
None => mbox.unwrap_or_default(),
},
}))
}
/// Converts a list of [`imap_proto::Address`] into a list of addresses.
pub fn from_imap_addrs_to_addrs(proto_addrs: &[imap_proto::Address]) -> Result<Addrs> {
let mut addrs = vec![];
for addr in proto_addrs {
addrs.push(
from_imap_addr_to_addr(addr).context(format!("cannot parse address {:?}", addr))?,
);
}
Ok(addrs.into())
}
/// Converts an optional list of [`imap_proto::Address`] into an optional list of addresses.
pub fn from_imap_addrs_to_some_addrs(
addrs: &Option<Vec<imap_proto::Address>>,
) -> Result<Option<Addrs>> {
Ok(
if let Some(addrs) = addrs.as_deref().map(from_imap_addrs_to_addrs) {
Some(addrs?)
} else {
None
},
)
}

13
src/msg/envelope.rs Normal file
View file

@ -0,0 +1,13 @@
use std::{any, fmt};
use crate::output::PrintTable;
pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any {
fn as_any(&self) -> &dyn any::Any;
}
impl<T: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any> Envelopes for T {
fn as_any(&self) -> &dyn any::Any {
self
}
}

View file

@ -7,50 +7,63 @@ use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::{debug, info};
use crate::domain::msg::msg_arg;
use crate::msg::msg_arg;
type SeqRange<'a> = &'a str;
type Flags<'a> = Vec<&'a str>;
type Flags = String;
/// Represents the flag commands.
pub enum Command<'a> {
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
/// Represents the add flags command.
Add(SeqRange<'a>, Flags<'a>),
Add(SeqRange<'a>, Flags),
/// Represents the set flags command.
Set(SeqRange<'a>, Flags<'a>),
Set(SeqRange<'a>, Flags),
/// Represents the remove flags command.
Remove(SeqRange<'a>, Flags<'a>),
Remove(SeqRange<'a>, Flags),
}
/// Defines the flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
info!("entering message flag command matcher");
if let Some(m) = m.subcommand_matches("add") {
info!("add subcommand matched");
let seq_range = m.value_of("seq-range").unwrap();
debug!("seq range: {}", seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
let flags: String = m
.values_of("flags")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("flags: {:?}", flags);
return Ok(Some(Command::Add(seq_range, flags)));
return Ok(Some(Cmd::Add(seq_range, flags)));
}
if let Some(m) = m.subcommand_matches("set") {
info!("set subcommand matched");
let seq_range = m.value_of("seq-range").unwrap();
debug!("seq range: {}", seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
let flags: String = m
.values_of("flags")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("flags: {:?}", flags);
return Ok(Some(Command::Set(seq_range, flags)));
return Ok(Some(Cmd::Set(seq_range, flags)));
}
if let Some(m) = m.subcommand_matches("remove") {
info!("remove subcommand matched");
let seq_range = m.value_of("seq-range").unwrap();
debug!("seq range: {}", seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
let flags: String = m
.values_of("flags")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("flags: {:?}", flags);
return Ok(Some(Command::Remove(seq_range, flags)));
return Ok(Some(Cmd::Remove(seq_range, flags)));
}
Ok(None)

55
src/msg/flag_handler.rs Normal file
View file

@ -0,0 +1,55 @@
//! Message flag handling module.
//!
//! This module gathers all flag actions triggered by the CLI.
use anyhow::Result;
use crate::{backends::Backend, output::PrinterService};
/// Adds flags to all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq_range: &'a str,
flags: &'a str,
mbox: &'a str,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.add_flags(mbox, seq_range, flags)?;
printer.print(format!(
"Flag(s) {:?} successfully added to message(s) {:?}",
flags, seq_range
))
}
/// Removes flags from all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq_range: &'a str,
flags: &'a str,
mbox: &'a str,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.del_flags(mbox, seq_range, flags)?;
printer.print(format!(
"Flag(s) {:?} successfully removed from message(s) {:?}",
flags, seq_range
))
}
/// Replaces flags of all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq_range: &'a str,
flags: &'a str,
mbox: &'a str,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.set_flags(mbox, seq_range, flags)?;
printer.print(format!(
"Flag(s) {:?} successfully set for message(s) {:?}",
flags, seq_range
))
}

View file

@ -7,10 +7,8 @@ use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::{debug, info, trace};
use crate::{
domain::{
mbox::mbox_arg,
msg::{flag_arg, msg_arg, tpl_arg},
},
mbox::mbox_arg,
msg::{flag_arg, msg_arg, tpl_arg},
ui::table_arg,
};
@ -26,9 +24,11 @@ type Query = String;
type AttachmentPaths<'a> = Vec<&'a str>;
type MaxTableWidth = Option<usize>;
type Encrypt = bool;
type Criteria = String;
/// Message commands.
pub enum Command<'a> {
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Attachments(Seq<'a>),
Copy(Seq<'a>, Mbox<'a>),
Delete(Seq<'a>),
@ -39,22 +39,23 @@ pub enum Command<'a> {
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
Save(RawMsg<'a>),
Search(Query, MaxTableWidth, Option<PageSize>, Page),
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
Send(RawMsg<'a>),
Write(AttachmentPaths<'a>, Encrypt),
Flag(Option<flag_arg::Command<'a>>),
Tpl(Option<tpl_arg::Command<'a>>),
Flag(Option<flag_arg::Cmd<'a>>),
Tpl(Option<tpl_arg::Cmd<'a>>),
}
/// Message command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
info!("entering message command matcher");
if let Some(m) = m.subcommand_matches("attachments") {
info!("attachments command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
return Ok(Some(Command::Attachments(seq)));
return Ok(Some(Cmd::Attachments(seq)));
}
if let Some(m) = m.subcommand_matches("copy") {
@ -63,14 +64,14 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("seq: {}", seq);
let mbox = m.value_of("mbox-target").unwrap();
debug!(r#"target mailbox: "{:?}""#, mbox);
return Ok(Some(Command::Copy(seq, mbox)));
return Ok(Some(Cmd::Copy(seq, mbox)));
}
if let Some(m) = m.subcommand_matches("delete") {
info!("copy command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
return Ok(Some(Command::Delete(seq)));
return Ok(Some(Cmd::Delete(seq)));
}
if let Some(m) = m.subcommand_matches("forward") {
@ -81,7 +82,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("attachments paths: {:?}", paths);
let encrypt = m.is_present("encrypt");
debug!("encrypt: {}", encrypt);
return Ok(Some(Command::Forward(seq, paths, encrypt)));
return Ok(Some(Cmd::Forward(seq, paths, encrypt)));
}
if let Some(m) = m.subcommand_matches("list") {
@ -100,7 +101,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.map(|page| 1.max(page) - 1)
.unwrap_or_default();
debug!("page: {}", page);
return Ok(Some(Command::List(max_table_width, page_size, page)));
return Ok(Some(Cmd::List(max_table_width, page_size, page)));
}
if let Some(m) = m.subcommand_matches("move") {
@ -109,7 +110,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("seq: {}", seq);
let mbox = m.value_of("mbox-target").unwrap();
debug!("target mailbox: {:?}", mbox);
return Ok(Some(Command::Move(seq, mbox)));
return Ok(Some(Cmd::Move(seq, mbox)));
}
if let Some(m) = m.subcommand_matches("read") {
@ -120,7 +121,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("text mime: {}", mime);
let raw = m.is_present("raw");
debug!("raw: {}", raw);
return Ok(Some(Command::Read(seq, mime, raw)));
return Ok(Some(Cmd::Read(seq, mime, raw)));
}
if let Some(m) = m.subcommand_matches("reply") {
@ -134,14 +135,14 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
let encrypt = m.is_present("encrypt");
debug!("encrypt: {}", encrypt);
return Ok(Some(Command::Reply(seq, all, paths, encrypt)));
return Ok(Some(Cmd::Reply(seq, all, paths, encrypt)));
}
if let Some(m) = m.subcommand_matches("save") {
info!("save command matched");
let msg = m.value_of("message").unwrap_or_default();
trace!("message: {}", msg);
return Ok(Some(Command::Save(msg)));
return Ok(Some(Cmd::Save(msg)));
}
if let Some(m) = m.subcommand_matches("search") {
@ -185,7 +186,58 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.1
.join(" ");
debug!("query: {}", query);
return Ok(Some(Command::Search(
return Ok(Some(Cmd::Search(query, max_table_width, page_size, page)));
}
if let Some(m) = m.subcommand_matches("sort") {
info!("sort command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
debug!("page size: {:?}", page_size);
let page = m
.value_of("page")
.unwrap()
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default();
debug!("page: {:?}", page);
let criteria = m
.values_of("criterion")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("criteria: {:?}", criteria);
let query = m
.values_of("query")
.unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd, escape) {
// Next command is an arg and needs to be escaped
("subject", _) | ("body", _) | ("text", _) => {
cmds.push(cmd.to_string());
(true, cmds)
}
// Escaped arg commands
(_, true) => {
cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
}
})
.1
.join(" ");
debug!("query: {:?}", query);
return Ok(Some(Cmd::Sort(
criteria,
query,
max_table_width,
page_size,
@ -197,7 +249,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
info!("send command matched");
let msg = m.value_of("message").unwrap_or_default();
trace!("message: {}", msg);
return Ok(Some(Command::Send(msg)));
return Ok(Some(Cmd::Send(msg)));
}
if let Some(m) = m.subcommand_matches("write") {
@ -206,19 +258,19 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("attachments paths: {:?}", attachment_paths);
let encrypt = m.is_present("encrypt");
debug!("encrypt: {}", encrypt);
return Ok(Some(Command::Write(attachment_paths, encrypt)));
return Ok(Some(Cmd::Write(attachment_paths, encrypt)));
}
if let Some(m) = m.subcommand_matches("template") {
return Ok(Some(Command::Tpl(tpl_arg::matches(m)?)));
return Ok(Some(Cmd::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(Cmd::Flag(flag_arg::matches(m)?)));
}
info!("default list command matched");
Ok(Some(Command::List(None, None, 0)))
Ok(Some(Cmd::List(None, None, 0)))
}
/// Message sequence number argument.
@ -313,13 +365,45 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.multiple(true)
.required(true),
),
SubCommand::with_name("sort")
.about("Sorts messages by the given criteria and matching the given IMAP query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table_arg::max_width())
.arg(
Arg::with_name("criterion")
.long("criterion")
.short("c")
.help("Defines the message sorting preferences")
.value_name("CRITERION:ORDER")
.takes_value(true)
.multiple(true)
.required(true)
.possible_values(&[
"arrival", "arrival:asc", "arrival:desc",
"cc", "cc:asc", "cc:desc",
"date", "date:asc", "date:desc",
"from", "from:asc", "from:desc",
"size", "size:asc", "size:desc",
"subject", "subject:asc", "subject:desc",
"to", "to:asc", "to:desc",
]),
)
.arg(
Arg::with_name("query")
.help("IMAP query")
.long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.")
.value_name("QUERY")
.default_value("ALL")
.raw(true),
),
SubCommand::with_name("write")
.about("Writes a new message")
.arg(attachment_arg())
.arg(encrypt_arg()),
SubCommand::with_name("send")
.about("Sends a raw message")
.arg(Arg::with_name("message").raw(true).last(true)),
.arg(Arg::with_name("message").raw(true)),
SubCommand::with_name("save")
.about("Saves a raw message")
.arg(Arg::with_name("message").raw(true)),

View file

@ -2,38 +2,27 @@ use ammonia;
use anyhow::{anyhow, Context, Error, Result};
use chrono::{DateTime, FixedOffset};
use html_escape;
use imap::types::Flag;
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
use log::{debug, info, trace};
use regex::Regex;
use rfc2047_decoder;
use std::{
collections::HashSet,
convert::{TryFrom, TryInto},
env::temp_dir,
fmt::Debug,
fs,
path::PathBuf,
};
use std::{collections::HashSet, env::temp_dir, fmt::Debug, fs, path::PathBuf};
use uuid::Uuid;
use crate::{
config::{Account, DEFAULT_SIG_DELIM},
domain::{
imap::ImapServiceInterface,
mbox::Mbox,
msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride},
smtp::SmtpServiceInterface,
backends::Backend,
config::{AccountConfig, 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,
},
output::PrinterService,
smtp::SmtpService,
ui::{
choice::{self, PostEditChoice, PreEditChoice},
editor,
},
};
type Addr = lettre::message::Mailbox;
/// Representation of a message.
#[derive(Debug, Default)]
pub struct Msg {
@ -42,17 +31,14 @@ pub struct Msg {
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
pub id: u32,
/// The flags attached to the message.
pub flags: Flags,
/// The subject of the message.
pub subject: String,
pub from: Option<Vec<Addr>>,
pub reply_to: Option<Vec<Addr>>,
pub to: Option<Vec<Addr>>,
pub cc: Option<Vec<Addr>>,
pub bcc: Option<Vec<Addr>>,
pub from: Option<Addrs>,
pub reply_to: Option<Addrs>,
pub to: Option<Addrs>,
pub cc: Option<Addrs>,
pub bcc: Option<Addrs>,
pub in_reply_to: Option<String>,
pub message_id: Option<String>,
@ -63,6 +49,8 @@ pub struct Msg {
pub parts: Parts,
pub encrypt: bool,
pub raw: Vec<u8>,
}
impl Msg {
@ -173,8 +161,8 @@ impl Msg {
}
}
pub fn into_reply(mut self, all: bool, account: &Account) -> Result<Self> {
let account_addr: Addr = account.address().parse()?;
pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result<Self> {
let account_addr = account.address()?;
// Message-Id
self.message_id = None;
@ -183,13 +171,13 @@ impl Msg {
self.in_reply_to = self.message_id.to_owned();
// From
self.from = Some(vec![account_addr.to_owned()]);
self.from = Some(vec![account_addr.clone()].into());
// To
let addrs = self
.reply_to
.as_ref()
.or_else(|| self.from.as_ref())
.as_deref()
.or_else(|| self.from.as_deref())
.map(|addrs| {
addrs
.clone()
@ -197,11 +185,11 @@ impl Msg {
.filter(|addr| addr != &account_addr)
});
if all {
self.to = addrs.map(|addrs| addrs.collect());
self.to = addrs.map(|addrs| addrs.collect::<Vec<_>>().into());
} else {
self.to = addrs
.and_then(|mut addrs| addrs.next())
.map(|addr| vec![addr]);
.map(|addr| vec![addr].into());
}
// Cc & Bcc
@ -226,12 +214,8 @@ impl Msg {
.reply_to
.as_ref()
.or_else(|| self.from.as_ref())
.and_then(|addrs| addrs.first())
.map(|addr| {
addr.name
.to_owned()
.unwrap_or_else(|| addr.email.to_string())
})
.and_then(|addrs| addrs.clone().extract_single_info())
.map(|addr| addr.display_name.clone().unwrap_or_else(|| addr.addr))
.unwrap_or_else(|| "unknown sender".into());
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
@ -255,8 +239,8 @@ impl Msg {
Ok(self)
}
pub fn into_forward(mut self, account: &Account) -> Result<Self> {
let account_addr: Addr = account.address().parse()?;
pub fn into_forward(mut self, account: &AccountConfig) -> Result<Self> {
let account_addr = account.address()?;
let prev_subject = self.subject.to_owned();
let prev_date = self.date.to_owned();
@ -270,10 +254,10 @@ impl Msg {
self.in_reply_to = None;
// From
self.from = Some(vec![account_addr]);
self.from = Some(vec![account_addr].into());
// To
self.to = Some(vec![]);
self.to = Some(vec![].into());
// Cc
self.cc = None;
@ -295,22 +279,12 @@ impl Msg {
}
if let Some(addrs) = prev_from.as_ref() {
content.push_str("From: ");
let mut glue = "";
for addr in addrs {
content.push_str(glue);
content.push_str(&addr.to_string());
glue = ", ";
}
content.push_str(&addrs.to_string());
content.push('\n');
}
if let Some(addrs) = prev_to.as_ref() {
content.push_str("To: ");
let mut glue = "";
for addr in addrs {
content.push_str(glue);
content.push_str(&addr.to_string());
glue = ", ";
}
content.push_str(&addrs.to_string());
content.push('\n');
}
content.push('\n');
@ -321,24 +295,19 @@ impl Msg {
Ok(self)
}
fn _edit_with_editor(&self, account: &Account) -> Result<Self> {
let tpl = self.to_tpl(TplOverride::default(), account);
fn _edit_with_editor(&self, account: &AccountConfig) -> Result<Self> {
let tpl = self.to_tpl(TplOverride::default(), account)?;
let tpl = editor::open_with_tpl(tpl)?;
Self::from_tpl(&tpl)
}
pub fn edit_with_editor<
'a,
Printer: PrinterService,
ImapService: ImapServiceInterface<'a>,
SmtpService: SmtpServiceInterface,
>(
pub fn edit_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
mut self,
account: &Account,
printer: &mut Printer,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
account: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<Box<&'a mut B>> {
info!("start editing with editor");
let draft = msg_utils::local_draft_path();
@ -355,7 +324,7 @@ impl Msg {
self.merge_with(self._edit_with_editor(account)?);
break;
}
PreEditChoice::Quit => return Ok(()),
PreEditChoice::Quit => return Ok(backend),
},
Err(err) => {
println!("{}", err);
@ -370,10 +339,8 @@ impl Msg {
loop {
match choice::post_edit() {
Ok(PostEditChoice::Send) => {
let mbox = Mbox::new(&account.sent_folder);
let sent_msg = smtp.send_msg(account, &self)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
backend.add_msg(&account.sent_folder, &sent_msg.formatted(), "seen")?;
msg_utils::remove_local_draft()?;
printer.print("Message successfully sent")?;
break;
@ -387,10 +354,8 @@ impl Msg {
break;
}
Ok(PostEditChoice::RemoteDraft) => {
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)?;
let tpl = self.to_tpl(TplOverride::default(), account)?;
backend.add_msg(&account.draft_folder, tpl.as_bytes(), "seen draft")?;
msg_utils::remove_local_draft()?;
printer.print(format!(
"Message successfully saved to {}",
@ -409,7 +374,7 @@ impl Msg {
}
}
Ok(())
Ok(backend)
}
pub fn encrypt(mut self, encrypt: bool) -> Self {
@ -476,7 +441,8 @@ impl Msg {
}
}
pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> String {
pub fn to_tpl(&self, opts: TplOverride, account: &AccountConfig) -> Result<String> {
let account_addr: Addrs = vec![account.address()?].into();
let mut tpl = String::default();
tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
@ -490,7 +456,7 @@ impl Msg {
"From: {}\n",
opts.from
.map(|addrs| addrs.join(", "))
.unwrap_or_else(|| account.address())
.unwrap_or_else(|| account_addr.to_string())
));
// To
@ -498,37 +464,25 @@ impl Msg {
"To: {}\n",
opts.to
.map(|addrs| addrs.join(", "))
.or_else(|| self.to.clone().map(|addrs| addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")))
.or_else(|| self.to.clone().map(|addrs| addrs.to_string()))
.unwrap_or_default()
));
// Cc
if let Some(addrs) = opts.cc.map(|addrs| addrs.join(", ")).or_else(|| {
self.cc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
if let Some(addrs) = opts
.cc
.map(|addrs| addrs.join(", "))
.or_else(|| self.cc.clone().map(|addrs| addrs.to_string()))
{
tpl.push_str(&format!("Cc: {}\n", addrs));
}
// Bcc
if let Some(addrs) = opts.bcc.map(|addrs| addrs.join(", ")).or_else(|| {
self.bcc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
if let Some(addrs) = opts
.bcc
.map(|addrs| addrs.join(", "))
.or_else(|| self.bcc.clone().map(|addrs| addrs.to_string()))
{
tpl.push_str(&format!("Bcc: {}\n", addrs));
}
@ -560,72 +514,20 @@ impl Msg {
tpl.push('\n');
trace!("template: {:?}", tpl);
tpl
Ok(tpl)
}
pub fn from_tpl(tpl: &str) -> Result<Self> {
info!("begin: building message from template");
trace!("template: {:?}", tpl);
let mut msg = Msg::default();
let parsed_msg = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?;
debug!("parsing headers");
for header in parsed_msg.get_headers() {
let key = header.get_key();
debug!("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);
match key.to_lowercase().as_str() {
"message-id" => msg.message_id = Some(val),
"in-reply-to" => msg.in_reply_to = Some(val),
"subject" => {
msg.subject = val;
}
"from" => {
msg.from = parse_addrs(val).context(format!("cannot parse header {:?}", key))?
}
"to" => {
msg.to = parse_addrs(val).context(format!("cannot parse header {:?}", key))?
}
"reply-to" => {
msg.reply_to =
parse_addrs(val).context(format!("cannot parse header {:?}", key))?
}
"cc" => {
msg.cc = parse_addrs(val).context(format!("cannot parse header {:?}", key))?
}
"bcc" => {
msg.bcc = parse_addrs(val).context(format!("cannot parse header {:?}", key))?
}
_ => (),
}
}
debug!("parsing body");
let body = parsed_msg
.get_body_raw()
.context("cannot get raw body from message")
.and_then(|body| String::from_utf8(body).context("cannot decode body from utf8"))?;
trace!("body: {:?}", body);
msg.parts
.push(Part::TextPlain(TextPlainPart { content: body }));
let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?;
info!("end: building message from template");
trace!("message: {:?}", msg);
Ok(msg)
Self::from_parsed_mail(parsed_mail, &AccountConfig::default())
}
pub fn into_sendable_msg(&self, account: &Account) -> Result<lettre::Message> {
pub fn into_sendable_msg(&self, account: &AccountConfig) -> Result<lettre::Message> {
let mut msg_builder = lettre::Message::builder()
.message_id(self.message_id.to_owned())
.subject(self.subject.to_owned());
@ -635,33 +537,33 @@ impl Msg {
};
if let Some(addrs) = self.from.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.from(addr.to_owned()))
for addr in from_addrs_to_sendable_mbox(addrs)? {
msg_builder = msg_builder.from(addr)
}
};
if let Some(addrs) = self.to.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.to(addr.to_owned()))
for addr in from_addrs_to_sendable_mbox(addrs)? {
msg_builder = msg_builder.to(addr)
}
};
if let Some(addrs) = self.reply_to.as_ref() {
msg_builder = addrs.iter().fold(msg_builder, |builder, addr| {
builder.reply_to(addr.to_owned())
})
for addr in from_addrs_to_sendable_mbox(addrs)? {
msg_builder = msg_builder.reply_to(addr)
}
};
if let Some(addrs) = self.cc.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.cc(addr.to_owned()))
for addr in from_addrs_to_sendable_mbox(addrs)? {
msg_builder = msg_builder.cc(addr)
}
};
if let Some(addrs) = self.bcc.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.bcc(addr.to_owned()))
for addr in from_addrs_to_sendable_mbox(addrs)? {
msg_builder = msg_builder.bcc(addr)
}
};
let mut multipart = {
@ -682,11 +584,14 @@ impl Msg {
if self.encrypt {
let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string());
fs::write(multipart_buffer.clone(), multipart.formatted())?;
let addr = self
.to
.as_ref()
.and_then(|addrs| addrs.clone().extract_single_info())
.map(|addr| addr.addr)
.ok_or_else(|| anyhow!("cannot find recipient"))?;
let encrypted_multipart = account
.pgp_encrypt_file(
&self.to.as_ref().unwrap().first().unwrap().email.to_string(),
multipart_buffer.clone(),
)?
.pgp_encrypt_file(&addr, multipart_buffer.clone())?
.ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?;
trace!("encrypted multipart: {:#?}", encrypted_multipart);
multipart = MultiPart::encrypted(String::from("application/pgp-encrypted"))
@ -706,187 +611,82 @@ impl Msg {
.multipart(multipart)
.context("cannot build sendable message")
}
pub fn from_parsed_mail(
parsed_mail: mailparse::ParsedMail<'_>,
config: &AccountConfig,
) -> Result<Self> {
info!("begin: building message from parsed mail");
trace!("parsed mail: {:?}", parsed_mail);
let mut msg = Msg::default();
debug!("parsing headers");
for header in parsed_mail.get_headers() {
let key = header.get_key();
debug!("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);
match key.to_lowercase().as_str() {
"message-id" => msg.message_id = Some(val),
"in-reply-to" => msg.in_reply_to = Some(val),
"subject" => {
msg.subject = val;
}
"from" => {
msg.from = from_slice_to_addrs(val)
.context(format!("cannot parse header {:?}", key))?
}
"to" => {
msg.to = from_slice_to_addrs(val)
.context(format!("cannot parse header {:?}", key))?
}
"reply-to" => {
msg.reply_to = from_slice_to_addrs(val)
.context(format!("cannot parse header {:?}", key))?
}
"cc" => {
msg.cc = from_slice_to_addrs(val)
.context(format!("cannot parse header {:?}", key))?
}
"bcc" => {
msg.bcc = from_slice_to_addrs(val)
.context(format!("cannot parse header {:?}", key))?
}
_ => (),
}
}
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");
Ok(msg)
}
}
impl TryInto<lettre::address::Envelope> for Msg {
type Error = Error;
fn try_into(self) -> Result<lettre::address::Envelope> {
let from: Option<lettre::Address> = self
.from
.and_then(|addrs| addrs.into_iter().next())
.map(|addr| addr.email);
let from = match self.from.and_then(|addrs| addrs.extract_single_info()) {
Some(addr) => addr.addr.parse().map(Some),
None => Ok(None),
}?;
let to = self
.to
.map(|addrs| addrs.into_iter().map(|addr| addr.email).collect())
.unwrap_or_default();
let envelope =
lettre::address::Envelope::new(from, to).context("cannot create envelope")?;
Ok(envelope)
}
}
impl<'a> TryFrom<(&'a Account, &'a imap::types::Fetch)> for Msg {
type Error = Error;
fn try_from((account, fetch): (&'a Account, &'a imap::types::Fetch)) -> Result<Msg> {
let envelope = fetch
.envelope()
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number
let id = fetch.message;
// Get the flags
let flags = Flags::try_from(fetch.flags())?;
// Get the subject
let subject = envelope
.subject
.as_ref()
.map(|subj| {
rfc2047_decoder::decode(subj).context(format!(
"cannot decode subject of message {}",
fetch.message
))
})
.unwrap_or_else(|| Ok(String::default()))?;
// Get the sender(s) address(es)
let from = match envelope
.sender
.as_deref()
.or_else(|| envelope.from.as_deref())
.map(to_addrs)
{
Some(addrs) => Some(addrs?),
None => None,
};
// Get the "Reply-To" address(es)
let reply_to = to_some_addrs(&envelope.reply_to).context(format!(
r#"cannot parse "reply to" address of message {}"#,
id
))?;
// Get the recipient(s) address(es)
let to = to_some_addrs(&envelope.to)
.context(format!(r#"cannot parse "to" address of message {}"#, id))?;
// Get the "Cc" recipient(s) address(es)
let cc = to_some_addrs(&envelope.cc)
.context(format!(r#"cannot parse "cc" address of message {}"#, id))?;
// Get the "Bcc" recipient(s) address(es)
let bcc = to_some_addrs(&envelope.bcc)
.context(format!(r#"cannot parse "bcc" address of message {}"#, id))?;
// Get the "In-Reply-To" message identifier
let in_reply_to = match envelope
.in_reply_to
.as_ref()
.map(|cow| String::from_utf8(cow.to_vec()))
{
Some(id) => Some(id?),
None => None,
};
// Get the message identifier
let message_id = match envelope
.message_id
.as_ref()
.map(|cow| String::from_utf8(cow.to_vec()))
{
Some(id) => Some(id?),
None => None,
};
// Get the internal date
let date = fetch.internal_date();
// Get all parts
let body = fetch
.body()
.ok_or_else(|| anyhow!("cannot get body of message {}", id))?;
let parsed_mail =
mailparse::parse_mail(body).context(format!("cannot parse body of message {}", id))?;
let parts = Parts::from_parsed_mail(account, &parsed_mail)?;
Ok(Self {
id,
flags,
subject,
from,
reply_to,
to,
cc,
bcc,
in_reply_to,
message_id,
date,
parts,
encrypt: false,
})
.map(from_addrs_to_sendable_addrs)
.unwrap_or(Ok(vec![]))?;
Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?)
}
}
pub fn parse_addr<S: AsRef<str> + Debug>(raw_addr: S) -> Result<Addr> {
raw_addr
.as_ref()
.trim()
.parse()
.context(format!("cannot parse address {:?}", raw_addr))
}
pub fn parse_addrs<S: AsRef<str> + Debug>(raw_addrs: S) -> Result<Option<Vec<Addr>>> {
let mut addrs: Vec<Addr> = vec![];
for raw_addr in raw_addrs.as_ref().split(',') {
addrs
.push(parse_addr(raw_addr).context(format!("cannot parse addresses {:?}", raw_addrs))?);
}
Ok(if addrs.is_empty() { None } else { Some(addrs) })
}
pub fn to_addr(addr: &imap_proto::Address) -> Result<Addr> {
let name = addr
.name
.as_ref()
.map(|name| {
rfc2047_decoder::decode(&name.to_vec())
.context("cannot decode address name")
.map(Some)
})
.unwrap_or(Ok(None))?;
let mbox = addr
.mailbox
.as_ref()
.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_else(|| anyhow!("cannot get address host"))
.and_then(|host| {
rfc2047_decoder::decode(&host.to_vec()).context("cannot decode address host")
})?;
Ok(Addr::new(name, lettre::Address::new(mbox, host)?))
}
pub fn to_addrs(addrs: &[imap_proto::Address]) -> Result<Vec<Addr>> {
let mut parsed_addrs = vec![];
for addr in addrs {
parsed_addrs.push(to_addr(addr).context(format!(r#"cannot parse address "{:?}""#, addr))?);
}
Ok(parsed_addrs)
}
pub fn to_some_addrs(addrs: &Option<Vec<imap_proto::Address>>) -> Result<Option<Vec<Addr>>> {
Ok(match addrs.as_deref().map(to_addrs) {
Some(addrs) => Some(addrs?),
None => None,
})
}

348
src/msg/msg_handler.rs Normal file
View file

@ -0,0 +1,348 @@
//! Module related to message handling.
//!
//! This module gathers all message commands.
use anyhow::{Context, Result};
use atty::Stream;
use log::{debug, info, trace};
use mailparse::addrparse;
use std::{
borrow::Cow,
convert::TryInto,
fs,
io::{self, BufRead},
};
use url::Url;
use crate::{
backends::Backend,
config::AccountConfig,
msg::{Msg, Part, Parts, TextPlainPart},
output::{PrintTableOpts, PrinterService},
smtp::SmtpService,
};
/// Downloads all message attachments to the user account downloads directory.
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let attachments = backend.get_msg(mbox, seq)?.attachments();
let attachments_len = attachments.len();
debug!(
r#"{} attachment(s) found for message "{}""#,
attachments_len, seq
);
for attachment in attachments {
let file_path = config.get_download_file_path(&attachment.filename)?;
debug!("downloading {}…", attachment.filename);
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
))
}
/// Copy a message from a mailbox to another.
pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox_src: &str,
mbox_dst: &str,
printer: &mut P,
backend: Box<&mut B>,
) -> Result<()> {
backend.copy_msg(mbox_src, mbox_dst, seq)?;
printer.print(format!(
r#"Message {} successfully copied to folder "{}""#,
seq, mbox_dst
))
}
/// Delete messages matching the given sequence range.
pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox: &str,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.del_msg(mbox, seq)?;
printer.print(format!(r#"Message(s) {} successfully deleted"#, seq))
}
/// Forward the given message UID from the selected mailbox.
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
seq: &str,
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
backend
.get_msg(mbox, seq)?
.into_forward(config)?
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(config, printer, backend, smtp)?;
Ok(())
}
/// List paginated messages from the selected mailbox.
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
imap: Box<&'a mut B>,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.default_page_size);
debug!("page size: {}", page_size);
let msgs = imap.get_envelopes(mbox, "arrival:desc", "all", page_size, page)?;
trace!("envelopes: {:?}", msgs);
printer.print_table(msgs, PrintTableOpts { max_width })
}
/// Parses and edits a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
url: &Url,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
info!("entering mailto command handler");
let to = addrparse(url.path())?;
let mut cc = Vec::new();
let mut bcc = Vec::new();
let mut subject = Cow::default();
let mut body = Cow::default();
for (key, val) in url.query_pairs() {
match key.as_bytes() {
b"cc" => {
cc.push(val.to_string());
}
b"bcc" => {
bcc.push(val.to_string());
}
b"subject" => {
subject = val;
}
b"body" => {
body = val;
}
_ => (),
}
}
let msg = Msg {
from: Some(vec![config.address()?].into()),
to: if to.is_empty() { None } else { Some(to) },
cc: if cc.is_empty() {
None
} else {
Some(addrparse(&cc.join(","))?)
},
bcc: if bcc.is_empty() {
None
} else {
Some(addrparse(&bcc.join(","))?)
},
subject: subject.into(),
parts: Parts(vec![Part::TextPlain(TextPlainPart {
content: body.into(),
})]),
..Msg::default()
};
trace!("message: {:?}", msg);
msg.edit_with_editor(config, printer, backend, smtp)?;
Ok(())
}
/// Move a message from a mailbox to another.
pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox_src: &str,
mbox_dst: &str,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.move_msg(mbox_src, mbox_dst, seq)?;
printer.print(format!(
r#"Message {} successfully moved to folder "{}""#,
seq, mbox_dst
))
}
/// Read a message by its sequence number.
pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
text_mime: &str,
raw: bool,
mbox: &str,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let msg = backend.get_msg(mbox, seq)?;
let msg = 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)
}
/// Reply to the given message UID.
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
seq: &str,
all: bool,
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
backend
.get_msg(mbox, seq)?
.into_reply(all, config)?
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(config, printer, backend, smtp)?
.add_flags(mbox, seq, "replied")
}
/// Saves a raw message to the targetted mailbox.
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
mbox: &str,
raw_msg: &str,
printer: &mut P,
backend: Box<&mut B>,
) -> Result<()> {
info!("entering save message handler");
debug!("mailbox: {}", mbox);
let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json();
debug!("is json: {}", is_json);
let raw_msg = if is_tty || is_json {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
backend.add_msg(mbox, raw_msg.as_bytes(), "seen")?;
Ok(())
}
/// Paginate messages from the selected mailbox matching the specified query.
pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.default_page_size);
debug!("page size: {}", page_size);
let msgs = backend.get_envelopes(mbox, "arrival:desc", &query, page_size, page)?;
trace!("messages: {:#?}", msgs);
printer.print_table(msgs, PrintTableOpts { max_width })
}
/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria.
pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
sort: String,
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.default_page_size);
debug!("page size: {}", page_size);
let msgs = backend.get_envelopes(mbox, &sort, &query, page_size, page)?;
trace!("envelopes: {:#?}", msgs);
printer.print_table(msgs, PrintTableOpts { max_width })
}
/// Send a raw message.
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
raw_msg: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&mut B>,
smtp: &mut S,
) -> Result<()> {
info!("entering send message handler");
let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json();
debug!("is json: {}", is_json);
let raw_msg = if is_tty || is_json {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.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())?;
backend.add_msg(&config.sent_folder, raw_msg.as_bytes(), "seen")?;
Ok(())
}
/// Compose a new message.
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
attachments_paths: Vec<&str>,
encrypt: bool,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
Msg::default()
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(config, printer, backend, smtp)?;
Ok(())
}

View file

@ -7,7 +7,7 @@ use std::{
};
use uuid::Uuid;
use crate::config::Account;
use crate::config::AccountConfig;
#[derive(Debug, Clone, Default, Serialize)]
pub struct TextPlainPart {
@ -51,7 +51,7 @@ impl Parts {
}
pub fn from_parsed_mail<'a>(
account: &'a Account,
account: &'a AccountConfig,
part: &'a mailparse::ParsedMail<'a>,
) -> Result<Self> {
let mut parts = vec![];
@ -75,7 +75,7 @@ impl DerefMut for Parts {
}
fn build_parts_map_rec(
account: &Account,
account: &AccountConfig,
parsed_mail: &mailparse::ParsedMail,
parts: &mut Vec<Part>,
) -> Result<()> {
@ -133,7 +133,7 @@ fn build_parts_map_rec(
Ok(())
}
fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> Result<String> {
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result<String> {
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
let msg_body = msg
.get_body()

View file

@ -6,14 +6,14 @@ use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::{debug, info, trace};
use crate::domain::msg::msg_arg;
use crate::msg::msg_arg;
type Seq<'a> = &'a str;
type ReplyAll = bool;
type AttachmentPaths<'a> = Vec<&'a str>;
type Tpl<'a> = &'a str;
#[derive(Debug, Default)]
#[derive(Debug, Default, PartialEq, Eq)]
pub struct TplOverride<'a> {
pub subject: Option<&'a str>,
pub from: Option<Vec<&'a str>>,
@ -41,7 +41,8 @@ impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
}
/// Message template commands.
pub enum Command<'a> {
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
New(TplOverride<'a>),
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
Forward(Seq<'a>, TplOverride<'a>),
@ -50,14 +51,14 @@ pub enum Command<'a> {
}
/// Message template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
info!("entering message template command matcher");
if let Some(m) = m.subcommand_matches("new") {
info!("new subcommand matched");
let tpl = TplOverride::from(m);
trace!("template override: {:?}", tpl);
return Ok(Some(Command::New(tpl)));
return Ok(Some(Cmd::New(tpl)));
}
if let Some(m) = m.subcommand_matches("reply") {
@ -68,7 +69,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("reply all: {}", all);
let tpl = TplOverride::from(m);
trace!("template override: {:?}", tpl);
return Ok(Some(Command::Reply(seq, all, tpl)));
return Ok(Some(Cmd::Reply(seq, all, tpl)));
}
if let Some(m) = m.subcommand_matches("forward") {
@ -77,7 +78,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
debug!("sequence: {}", seq);
let tpl = TplOverride::from(m);
trace!("template args: {:?}", tpl);
return Ok(Some(Command::Forward(seq, tpl)));
return Ok(Some(Cmd::Forward(seq, tpl)));
}
if let Some(m) = m.subcommand_matches("save") {
@ -86,7 +87,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
trace!("attachments paths: {:?}", attachment_paths);
let tpl = m.value_of("template").unwrap_or_default();
trace!("template: {}", tpl);
return Ok(Some(Command::Save(attachment_paths, tpl)));
return Ok(Some(Cmd::Save(attachment_paths, tpl)));
}
if let Some(m) = m.subcommand_matches("send") {
@ -95,7 +96,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
trace!("attachments paths: {:?}", attachment_paths);
let tpl = m.value_of("template").unwrap_or_default();
trace!("template: {}", tpl);
return Ok(Some(Command::Send(attachment_paths, tpl)));
return Ok(Some(Cmd::Send(attachment_paths, tpl)));
}
Ok(None)

109
src/msg/tpl_handler.rs Normal file
View file

@ -0,0 +1,109 @@
//! Module related to message template handling.
//!
//! This module gathers all message template commands.
use anyhow::Result;
use atty::Stream;
use std::io::{self, BufRead};
use crate::{
backends::Backend,
config::AccountConfig,
msg::{Msg, TplOverride},
output::PrinterService,
smtp::SmtpService,
};
/// Generate a new message template.
pub fn new<'a, P: PrinterService>(
opts: TplOverride<'a>,
account: &'a AccountConfig,
printer: &'a mut P,
) -> Result<()> {
let tpl = Msg::default().to_tpl(opts, account)?;
printer.print(tpl)
}
/// Generate a reply message template.
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
all: bool,
opts: TplOverride<'a>,
mbox: &str,
config: &'a AccountConfig,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let tpl = backend
.get_msg(mbox, seq)?
.into_reply(all, config)?
.to_tpl(opts, config)?;
printer.print(tpl)
}
/// Generate a forward message template.
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
opts: TplOverride<'a>,
mbox: &str,
config: &'a AccountConfig,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let tpl = backend
.get_msg(mbox, seq)?
.into_forward(config)?
.to_tpl(opts, config)?;
printer.print(tpl)
}
/// Saves a message based on a template.
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
mbox: &str,
config: &AccountConfig,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P,
backend: Box<&mut B>,
) -> Result<()> {
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
};
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")
}
/// Sends a message based on a template.
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
mbox: &str,
account: &AccountConfig,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P,
backend: Box<&mut B>,
smtp: &mut S,
) -> Result<()> {
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.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")
}

View file

@ -1,9 +1,5 @@
use anyhow::{anyhow, Error, Result};
use serde::Serialize;
use std::{
convert::TryFrom,
fmt::{self, Display},
};
use std::{convert::TryFrom, fmt};
/// Represents the available output formats.
#[derive(Debug, PartialEq)]
@ -34,7 +30,7 @@ impl TryFrom<Option<&str>> for OutputFmt {
}
}
impl Display for OutputFmt {
impl fmt::Display for OutputFmt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fmt = match *self {
OutputFmt::Json => "JSON",
@ -45,12 +41,12 @@ impl Display for OutputFmt {
}
/// Defines a struct-wrapper to provide a JSON output.
#[derive(Debug, Serialize, Clone)]
pub struct OutputJson<T: Serialize> {
#[derive(Debug, Clone, serde::Serialize)]
pub struct OutputJson<T: serde::Serialize> {
response: T,
}
impl<T: Serialize> OutputJson<T> {
impl<T: serde::Serialize> OutputJson<T> {
pub fn new(response: T) -> Self {
Self { response }
}

View file

@ -1,16 +1,18 @@
use anyhow::{Context, Error, Result};
use atty::Stream;
use serde::Serialize;
use std::{convert::TryFrom, fmt::Debug};
use std::{
convert::TryFrom,
fmt::{self, Debug},
};
use termcolor::{ColorChoice, StandardStream};
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
pub trait PrinterService {
fn print<T: Debug + Print + Serialize>(&mut self, data: T) -> Result<()>;
fn print_table<T: Debug + PrintTable + Serialize>(
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self,
data: T,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()>;
fn is_json(&self) -> bool;
@ -22,7 +24,7 @@ pub struct StdoutPrinter {
}
impl PrinterService for StdoutPrinter {
fn print<T: Debug + Print + Serialize>(&mut self, data: T) -> Result<()> {
fn print<T: Debug + Print + serde::Serialize>(&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))
@ -30,15 +32,19 @@ impl PrinterService for StdoutPrinter {
}
}
fn print_table<T: Debug + PrintTable + Serialize>(
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self,
data: T,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print_table(self.writter.as_mut(), opts),
OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data))
.context("cannot write JSON to writter"),
OutputFmt::Json => {
let json = &mut serde_json::Serializer::new(self.writter.as_mut());
let ser = &mut <dyn erased_serde::Serializer>::erase(json);
data.erased_serialize(ser).unwrap();
Ok(())
}
}
}

1
src/smtp/mod.rs Normal file
View file

@ -0,0 +1 @@
//! Module related to SMTP.

View file

@ -9,19 +9,19 @@ use lettre::{
};
use log::debug;
use crate::{config::Account, domain::msg::Msg};
use crate::{config::AccountConfig, msg::Msg};
pub trait SmtpServiceInterface {
fn send_msg(&mut self, account: &Account, msg: &Msg) -> Result<lettre::Message>;
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<()>;
}
pub struct SmtpService<'a> {
account: &'a Account,
pub struct LettreService<'a> {
account: &'a AccountConfig,
transport: Option<SmtpTransport>,
}
impl<'a> SmtpService<'a> {
impl<'a> LettreService<'a> {
fn transport(&mut self) -> Result<&SmtpTransport> {
if let Some(ref transport) = self.transport {
Ok(transport)
@ -55,8 +55,8 @@ impl<'a> SmtpService<'a> {
}
}
impl<'a> SmtpServiceInterface for SmtpService<'a> {
fn send_msg(&mut self, account: &Account, msg: &Msg) -> Result<lettre::Message> {
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)?;
@ -70,8 +70,8 @@ impl<'a> SmtpServiceInterface for SmtpService<'a> {
}
}
impl<'a> From<&'a Account> for SmtpService<'a> {
fn from(account: &'a Account) -> Self {
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
fn from(account: &'a AccountConfig) -> Self {
debug!("init SMTP service");
Self {
account,

View file

@ -2,7 +2,7 @@ use anyhow::{Context, Result};
use log::debug;
use std::{env, fs, process::Command};
use crate::domain::msg::msg_utils;
use crate::msg::msg_utils;
pub fn open_with_tpl(tpl: String) -> Result<String> {
let path = msg_utils::local_draft_path();

View file

@ -0,0 +1,6 @@
From: alice@localhost
To: patrick@localhost
Subject: Plain message
Content-Type: text/plain; charset=utf-8
Ceci est un message.

View file

@ -0,0 +1,90 @@
use himalaya::{
backends::{Backend, ImapBackend, ImapEnvelopes},
config::{AccountConfig, ImapBackendConfig},
};
#[test]
fn test_imap_backend() {
// configure accounts
let account_config = AccountConfig {
smtp_host: "localhost".into(),
smtp_port: 3465,
smtp_starttls: false,
smtp_insecure: true,
smtp_login: "inbox@localhost".into(),
smtp_passwd_cmd: "echo 'password'".into(),
..AccountConfig::default()
};
let imap_config = ImapBackendConfig {
imap_host: "localhost".into(),
imap_port: 3993,
imap_starttls: false,
imap_insecure: true,
imap_login: "inbox@localhost".into(),
imap_passwd_cmd: "echo 'password'".into(),
};
let mut imap = ImapBackend::new(&account_config, &imap_config);
imap.connect().unwrap();
// set up mailboxes
if let Err(_) = imap.add_mbox("Mailbox1") {};
if let Err(_) = imap.add_mbox("Mailbox2") {};
imap.del_msg("Mailbox1", "1:*").unwrap();
imap.del_msg("Mailbox2", "1:*").unwrap();
// check that a message can be added
let msg = include_bytes!("./emails/alice-to-patrick.eml");
let id = imap.add_msg("Mailbox1", msg, "seen").unwrap().to_string();
// check that the added message exists
let msg = imap.get_msg("Mailbox1", &id).unwrap();
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
// check that the envelope of the added message exists
let envelopes = imap
.get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0)
.unwrap();
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
assert_eq!(1, envelopes.len());
let envelope = envelopes.first().unwrap();
assert_eq!("alice@localhost", envelope.sender);
assert_eq!("Plain message", envelope.subject);
// check that the message can be copied
imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
.unwrap();
let envelopes = imap
.get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0)
.unwrap();
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
assert_eq!(1, envelopes.len());
let envelopes = imap
.get_envelopes("Mailbox2", "arrival:desc", "ALL", 10, 0)
.unwrap();
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
assert_eq!(1, envelopes.len());
// check that the message can be moved
imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
.unwrap();
let envelopes = imap
.get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0)
.unwrap();
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
assert_eq!(0, envelopes.len());
let envelopes = imap
.get_envelopes("Mailbox2", "arrival:desc", "ALL", 10, 0)
.unwrap();
let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap();
assert_eq!(2, envelopes.len());
let id = envelopes.first().unwrap().id.to_string();
// check that the message can be deleted
imap.del_msg("Mailbox2", &id).unwrap();
assert!(imap.get_msg("Mailbox2", &id).is_err());
// check that disconnection works
imap.disconnect().unwrap();
}

View file

@ -0,0 +1,68 @@
use maildir::Maildir;
use std::{env, fs};
use himalaya::{
backends::{Backend, MaildirBackend, MaildirEnvelopes},
config::{AccountConfig, MaildirBackendConfig},
};
#[test]
fn test_maildir_backend() {
// set up maildir folders
let mdir: Maildir = env::temp_dir().join("himalaya-test-mdir").into();
if let Err(_) = fs::remove_dir_all(mdir.path()) {}
mdir.create_dirs().unwrap();
let mdir_sub: Maildir = mdir.path().join(".Subdir").into();
if let Err(_) = fs::remove_dir_all(mdir_sub.path()) {}
mdir_sub.create_dirs().unwrap();
// configure accounts
let account_config = AccountConfig {
inbox_folder: "INBOX".into(),
..AccountConfig::default()
};
let mdir_config = MaildirBackendConfig {
maildir_dir: mdir.path().to_owned(),
};
let mut mdir = MaildirBackend::new(&account_config, &mdir_config);
let mdir_sub_config = MaildirBackendConfig {
maildir_dir: mdir_sub.path().to_owned(),
};
let mut mdir_subdir = MaildirBackend::new(&account_config, &mdir_sub_config);
// check that a message can be added
let msg = include_bytes!("./emails/alice-to-patrick.eml");
let id = mdir.add_msg("INBOX", msg, "seen").unwrap().to_string();
// check that the added message exists
let msg = mdir.get_msg("INBOX", &id).unwrap();
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
// check that the envelope of the added message exists
let envelopes = mdir.get_envelopes("INBOX", "", "cur", 10, 0).unwrap();
let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap();
let envelope = envelopes.first().unwrap();
assert_eq!(1, envelopes.len());
assert_eq!("alice@localhost", envelope.sender);
assert_eq!("Plain message", envelope.subject);
// check that the message can be copied
mdir.copy_msg("INBOX", "Subdir", &envelope.id).unwrap();
assert!(mdir.get_msg("INBOX", &id).is_ok());
assert!(mdir.get_msg("Subdir", &id).is_ok());
assert!(mdir_subdir.get_msg("INBOX", &id).is_ok());
// check that the message can be moved
mdir.move_msg("INBOX", "Subdir", &envelope.id).unwrap();
assert!(mdir.get_msg("INBOX", &id).is_err());
assert!(mdir.get_msg("Subdir", &id).is_ok());
assert!(mdir_subdir.get_msg("INBOX", &id).is_ok());
// check that the message can be deleted
mdir.del_msg("Subdir", &id).unwrap();
assert!(mdir.get_msg("Subdir", &id).is_err());
assert!(mdir_subdir.get_msg("INBOX", &id).is_err());
}