mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-01 06:55:13 +00:00
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:
parent
585fa77af5
commit
158bc86cfa
13
.github/workflows/tests.yaml
vendored
13
.github/workflows/tests.yaml
vendored
|
@ -14,8 +14,17 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Start GreenMail testing server
|
- name: Start GreenMail testing server
|
||||||
run: |
|
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
|
- name: Install rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -7,17 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.5.5] - 2022-02-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [Contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) [#256]
|
- [Contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) [#256]
|
||||||
- Notify query config option [#289]
|
- Notify query config option [#289]
|
||||||
- End-to-end encryption *(EXPERIMENTAL)* [#54]
|
- End-to-end encryption [#54]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Multiple recipients issue [#288]
|
- Multiple recipients issue [#288]
|
||||||
|
- Cannot parse address [#227]
|
||||||
|
|
||||||
## [0.5.4] - 2022-02-05
|
## [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]
|
- Password from command [#22]
|
||||||
- Set up README [#20]
|
- 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.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.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
|
[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
|
[#39]: https://github.com/soywod/himalaya/issues/39
|
||||||
[#40]: https://github.com/soywod/himalaya/issues/40
|
[#40]: https://github.com/soywod/himalaya/issues/40
|
||||||
[#41]: https://github.com/soywod/himalaya/issues/41
|
[#41]: https://github.com/soywod/himalaya/issues/41
|
||||||
|
[#43]: https://github.com/soywod/himalaya/issues/43
|
||||||
[#47]: https://github.com/soywod/himalaya/issues/47
|
[#47]: https://github.com/soywod/himalaya/issues/47
|
||||||
[#48]: https://github.com/soywod/himalaya/issues/48
|
[#48]: https://github.com/soywod/himalaya/issues/48
|
||||||
[#50]: https://github.com/soywod/himalaya/issues/50
|
[#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
|
[#193]: https://github.com/soywod/himalaya/issues/193
|
||||||
[#196]: https://github.com/soywod/himalaya/issues/196
|
[#196]: https://github.com/soywod/himalaya/issues/196
|
||||||
[#199]: https://github.com/soywod/himalaya/issues/199
|
[#199]: https://github.com/soywod/himalaya/issues/199
|
||||||
|
[#204]: https://github.com/soywod/himalaya/issues/204
|
||||||
[#205]: https://github.com/soywod/himalaya/issues/205
|
[#205]: https://github.com/soywod/himalaya/issues/205
|
||||||
[#215]: https://github.com/soywod/himalaya/issues/215
|
[#215]: https://github.com/soywod/himalaya/issues/215
|
||||||
[#220]: https://github.com/soywod/himalaya/issues/220
|
[#220]: https://github.com/soywod/himalaya/issues/220
|
||||||
|
[#227]: https://github.com/soywod/himalaya/issues/227
|
||||||
[#228]: https://github.com/soywod/himalaya/issues/228
|
[#228]: https://github.com/soywod/himalaya/issues/228
|
||||||
[#229]: https://github.com/soywod/himalaya/issues/229
|
[#229]: https://github.com/soywod/himalaya/issues/229
|
||||||
[#249]: https://github.com/soywod/himalaya/issues/249
|
[#249]: https://github.com/soywod/himalaya/issues/249
|
||||||
|
|
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -226,6 +226,15 @@ dependencies = [
|
||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "erased-serde"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56047058e1ab118075ca22f9ecd737bcc961aa3566a3019cb71388afa280bd8a"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -322,6 +331,16 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gethostname"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4addc164932852d066774c405dbbdb7914742d2b39e39e1a7ca949c856d054d1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
|
@ -361,7 +380,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "himalaya"
|
name = "himalaya"
|
||||||
version = "0.5.5"
|
version = "0.5.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
@ -369,11 +388,13 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"erased-serde",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"imap",
|
"imap",
|
||||||
"imap-proto",
|
"imap-proto",
|
||||||
"lettre",
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
|
"maildir",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -563,6 +584,16 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
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]]
|
[[package]]
|
||||||
name = "mailparse"
|
name = "mailparse"
|
||||||
version = "0.13.6"
|
version = "0.13.6"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "himalaya"
|
name = "himalaya"
|
||||||
description = "Command-line interface for email management"
|
description = "Command-line interface for email management"
|
||||||
version = "0.5.5"
|
version = "0.5.6"
|
||||||
authors = ["soywod <clement.douin@posteo.net>"]
|
authors = ["soywod <clement.douin@posteo.net>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
categories = ["command-line-interface", "command-line-utilities", "email"]
|
categories = ["command-line-interface", "command-line-utilities", "email"]
|
||||||
|
@ -23,11 +23,13 @@ atty = "0.2.14"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
|
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
|
||||||
env_logger = "0.8.3"
|
env_logger = "0.8.3"
|
||||||
|
erased-serde = "0.3.18"
|
||||||
html-escape = "0.2.9"
|
html-escape = "0.2.9"
|
||||||
imap = "3.0.0-alpha.4"
|
imap = "3.0.0-alpha.4"
|
||||||
imap-proto = "0.14.3"
|
imap-proto = "0.14.3"
|
||||||
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
|
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
|
maildir = "0.6.0"
|
||||||
mailparse = "0.13.6"
|
mailparse = "0.13.6"
|
||||||
native-tls = "0.2.8"
|
native-tls = "0.2.8"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
|
|
37
README.md
37
README.md
|
@ -2,15 +2,23 @@
|
||||||
|
|
||||||
Command-line interface for email management
|
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)
|
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
|
||||||
|
|
||||||
## Motivation
|
## 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
|
## 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
|
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
|
## Configuration
|
||||||
|
|
||||||
|
@ -50,7 +60,9 @@ smtp-login = "your.email@gmail.com"
|
||||||
smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
|
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
|
## Features
|
||||||
|
|
||||||
|
@ -59,13 +71,17 @@ smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
|
||||||
- Email composition based on `$EDITOR`
|
- Email composition based on `$EDITOR`
|
||||||
- Email manipulation (copy/move/delete)
|
- Email manipulation (copy/move/delete)
|
||||||
- Multi-accounting
|
- Multi-accounting
|
||||||
|
- IMAP and Maildir support (POP and Notmuch are coming soon)
|
||||||
|
- PGP end-to-end encryption
|
||||||
- IDLE mode for real-time notifications
|
- IDLE mode for real-time notifications
|
||||||
- Vim plugin
|
- Vim plugin
|
||||||
- Completions for bash/zsh/fish
|
- Completions for bash/zsh/fish
|
||||||
- JSON output
|
- 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
|
## Sponsoring
|
||||||
|
|
||||||
|
@ -79,8 +95,11 @@ smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
|
||||||
|
|
||||||
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
|
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
|
||||||
- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
|
- [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
|
- [NeoMutt](https://neomutt.org/), an email terminal user interface
|
||||||
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface
|
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other
|
||||||
- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync
|
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
|
- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib
|
||||||
|
|
41
src/backends/backend.rs
Normal file
41
src/backends/backend.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
369
src/backends/imap/imap_backend.rs
Normal file
369
src/backends/imap/imap_backend.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 anyhow::{anyhow, Context, Error, Result};
|
||||||
use serde::Serialize;
|
use std::{convert::TryFrom, ops::Deref};
|
||||||
use std::{borrow::Cow, convert::TryFrom};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::msg::{Flag, Flags},
|
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||||
ui::{Cell, Row, Table},
|
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
|
/// Represents a list of IMAP envelopes.
|
||||||
/// is mostly used for listings.
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
#[derive(Debug, Default, Serialize)]
|
pub struct ImapEnvelopes(pub Vec<ImapEnvelope>);
|
||||||
pub struct Envelope<'a> {
|
|
||||||
/// The sequence number of the message.
|
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
|
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
|
|
||||||
/// The flags attached to the message.
|
/// Represents the flags attached to the message.
|
||||||
pub flags: Flags,
|
pub flags: ImapFlags,
|
||||||
|
|
||||||
/// The subject of the message.
|
/// Represents the subject of the message.
|
||||||
pub subject: Cow<'a, str>,
|
pub subject: String,
|
||||||
|
|
||||||
/// The sender of the message.
|
/// Represents the first sender of the message.
|
||||||
pub sender: String,
|
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
|
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
|
||||||
pub date: Option<String>,
|
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;
|
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
|
let envelope = fetch
|
||||||
.envelope()
|
.envelope()
|
||||||
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
|
.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;
|
let id = fetch.message;
|
||||||
|
|
||||||
// Get the flags
|
// Get the flags
|
||||||
let flags = Flags::try_from(fetch.flags())?;
|
let flags = ImapFlags::try_from(fetch.flags())?;
|
||||||
|
|
||||||
// Get the subject
|
// Get the subject
|
||||||
let subject: Cow<str> = envelope
|
let subject = envelope
|
||||||
.subject
|
.subject
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|subj| {
|
.map(|subj| {
|
||||||
|
@ -57,8 +130,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
|
||||||
fetch.message
|
fetch.message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| Ok(String::default()))?
|
.unwrap_or_else(|| Ok(String::default()))?;
|
||||||
.into();
|
|
||||||
|
|
||||||
// Get the sender
|
// Get the sender
|
||||||
let sender = envelope
|
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())
|
|
||||||
}
|
|
||||||
}
|
|
147
src/backends/imap/imap_flag.rs
Normal file
147
src/backends/imap/imap_flag.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
15
src/backends/imap/imap_handler.rs
Normal file
15
src/backends/imap/imap_handler.rs
Normal 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)
|
||||||
|
}
|
148
src/backends/imap/imap_mbox.rs
Normal file
148
src/backends/imap/imap_mbox.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
src/backends/imap/imap_mbox_attr.rs
Normal file
119
src/backends/imap/imap_mbox_attr.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
src/backends/imap/msg_sort_criterion.rs
Normal file
61
src/backends/imap/msg_sort_criterion.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
185
src/backends/maildir/maildir_backend.rs
Normal file
185
src/backends/maildir/maildir_backend.rs
Normal 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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
187
src/backends/maildir/maildir_envelope.rs
Normal file
187
src/backends/maildir/maildir_envelope.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
129
src/backends/maildir/maildir_flag.rs
Normal file
129
src/backends/maildir/maildir_flag.rs
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
src/backends/maildir/maildir_mbox.rs
Normal file
141
src/backends/maildir/maildir_mbox.rs
Normal 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
13
src/config/account_args.rs
Normal file
13
src/config/account_args.rs
Normal 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")
|
||||||
|
}
|
402
src/config/account_config.rs
Normal file
402
src/config/account_config.rs
Normal 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")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
13
src/config/config_args.rs
Normal 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")
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
124
src/config/deserialized_account_config.rs
Normal file
124
src/config/deserialized_account_config.rs
Normal 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);
|
103
src/config/deserialized_config.rs
Normal file
103
src/config/deserialized_config.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 mod account_args;
|
||||||
pub use account_entity::*;
|
pub mod account_config;
|
||||||
|
pub use account_config::*;
|
||||||
pub mod config_entity;
|
pub mod deserialized_account_config;
|
||||||
pub use config_entity::*;
|
pub use deserialized_account_config::*;
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
//! Module related to IMAP.
|
|
||||||
|
|
||||||
pub mod imap_arg;
|
|
||||||
pub mod imap_handler;
|
|
||||||
|
|
||||||
pub mod imap_service;
|
|
||||||
pub use imap_service::*;
|
|
|
@ -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"]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(","));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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::*;
|
|
|
@ -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::*;
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
// }
|
|
||||||
//}
|
|
|
@ -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::*;
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
//! Module related to SMTP.
|
|
||||||
|
|
||||||
pub mod smtp_service;
|
|
||||||
pub use smtp_service::*;
|
|
88
src/lib.rs
Normal file
88
src/lib.rs
Normal 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;
|
230
src/main.rs
230
src/main.rs
|
@ -1,23 +1,16 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use output::StdoutPrinter;
|
|
||||||
use std::{convert::TryFrom, env};
|
use std::{convert::TryFrom, env};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
mod compl;
|
use himalaya::{
|
||||||
mod config;
|
backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend},
|
||||||
mod domain;
|
compl::{compl_arg, compl_handler},
|
||||||
mod output;
|
config::{account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig},
|
||||||
mod ui;
|
mbox::{mbox_arg, mbox_handler},
|
||||||
|
|
||||||
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},
|
|
||||||
msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_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> {
|
fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||||
clap::App::new(env!("CARGO_PKG_NAME"))
|
clap::App::new(env!("CARGO_PKG_NAME"))
|
||||||
|
@ -25,7 +18,8 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||||
.author(env!("CARGO_PKG_AUTHORS"))
|
.author(env!("CARGO_PKG_AUTHORS"))
|
||||||
.global_setting(clap::AppSettings::GlobalVersion)
|
.global_setting(clap::AppSettings::GlobalVersion)
|
||||||
.args(&config_arg::args())
|
.arg(&config_args::path_arg())
|
||||||
|
.arg(&account_args::name_arg())
|
||||||
.args(&output_arg::args())
|
.args(&output_arg::args())
|
||||||
.arg(mbox_arg::source_arg())
|
.arg(mbox_arg::source_arg())
|
||||||
.subcommands(compl_arg::subcmds())
|
.subcommands(compl_arg::subcmds())
|
||||||
|
@ -42,14 +36,27 @@ fn main() -> Result<()> {
|
||||||
// Check mailto command BEFORE app initialization.
|
// Check mailto command BEFORE app initialization.
|
||||||
let raw_args: Vec<String> = env::args().collect();
|
let raw_args: Vec<String> = env::args().collect();
|
||||||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||||
let config = Config::try_from(None)?;
|
let config = DeserializedConfig::from_opt_path(None)?;
|
||||||
let account = Account::try_from((&config, None))?;
|
let (account_config, backend_config) =
|
||||||
let mbox = Mbox::new(&account.inbox_folder);
|
AccountConfig::from_config_and_opt_account_name(&config, None)?;
|
||||||
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
|
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
|
||||||
let url = Url::parse(&raw_args[1])?;
|
let url = Url::parse(&raw_args[1])?;
|
||||||
let mut imap = ImapService::from((&account, &mbox));
|
let mut smtp = LettreService::from(&account_config);
|
||||||
let mut smtp = SmtpService::from(&account);
|
|
||||||
return msg_handler::mailto(&url, &account, &mut printer, &mut imap, &mut smtp);
|
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();
|
let app = create_app();
|
||||||
|
@ -65,135 +72,192 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init entities and services.
|
// Init entities and services.
|
||||||
let config = Config::try_from(m.value_of("config"))?;
|
let config = DeserializedConfig::from_opt_path(m.value_of("config"))?;
|
||||||
let account = Account::try_from((&config, m.value_of("account")))?;
|
let (account_config, backend_config) =
|
||||||
let mbox = Mbox::new(m.value_of("mbox-source").unwrap_or(&account.inbox_folder));
|
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 printer = StdoutPrinter::try_from(m.value_of("output"))?;
|
||||||
let mut imap = ImapService::from((&account, &mbox));
|
let mut imap;
|
||||||
let mut smtp = SmtpService::from(&account);
|
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.
|
// Check IMAP commands.
|
||||||
match imap_arg::matches(&m)? {
|
if let BackendConfig::Imap(ref imap_config) = backend_config {
|
||||||
Some(imap_arg::Command::Notify(keepalive)) => {
|
let mut imap = ImapBackend::new(&account_config, imap_config);
|
||||||
return imap_handler::notify(keepalive, &config, &account, &mut imap);
|
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.
|
// Check mailbox commands.
|
||||||
match mbox_arg::matches(&m)? {
|
match mbox_arg::matches(&m)? {
|
||||||
Some(mbox_arg::Cmd::List(max_width)) => {
|
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.
|
// Check message commands.
|
||||||
match msg_arg::matches(&m)? {
|
match msg_arg::matches(&m)? {
|
||||||
Some(msg_arg::Command::Attachments(seq)) => {
|
Some(msg_arg::Cmd::Attachments(seq)) => {
|
||||||
return msg_handler::attachments(seq, &account, &mut printer, &mut imap);
|
return msg_handler::attachments(seq, mbox, &account_config, &mut printer, backend);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Copy(seq, mbox)) => {
|
Some(msg_arg::Cmd::Copy(seq, mbox_dst)) => {
|
||||||
return msg_handler::copy(seq, mbox, &mut printer, &mut imap);
|
return msg_handler::copy(seq, mbox, mbox_dst, &mut printer, backend);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Delete(seq)) => {
|
Some(msg_arg::Cmd::Delete(seq)) => {
|
||||||
return msg_handler::delete(seq, &mut printer, &mut imap);
|
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(
|
return msg_handler::forward(
|
||||||
seq,
|
seq,
|
||||||
attachment_paths,
|
attachment_paths,
|
||||||
encrypt,
|
encrypt,
|
||||||
&account,
|
mbox,
|
||||||
|
&account_config,
|
||||||
&mut printer,
|
&mut printer,
|
||||||
&mut imap,
|
backend,
|
||||||
&mut smtp,
|
&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(
|
return msg_handler::list(
|
||||||
max_width,
|
max_width,
|
||||||
page_size,
|
page_size,
|
||||||
page,
|
page,
|
||||||
&account,
|
mbox,
|
||||||
|
&account_config,
|
||||||
&mut printer,
|
&mut printer,
|
||||||
&mut imap,
|
backend,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Move(seq, mbox)) => {
|
Some(msg_arg::Cmd::Move(seq, mbox_dst)) => {
|
||||||
return msg_handler::move_(seq, mbox, &mut printer, &mut imap);
|
return msg_handler::move_(seq, mbox, mbox_dst, &mut printer, backend);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Read(seq, text_mime, raw)) => {
|
Some(msg_arg::Cmd::Read(seq, text_mime, raw)) => {
|
||||||
return msg_handler::read(seq, text_mime, raw, &account, &mut printer, &mut imap);
|
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(
|
return msg_handler::reply(
|
||||||
seq,
|
seq,
|
||||||
all,
|
all,
|
||||||
attachment_paths,
|
attachment_paths,
|
||||||
encrypt,
|
encrypt,
|
||||||
&account,
|
mbox,
|
||||||
|
&account_config,
|
||||||
&mut printer,
|
&mut printer,
|
||||||
&mut imap,
|
backend,
|
||||||
&mut smtp,
|
&mut smtp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Save(raw_msg)) => {
|
Some(msg_arg::Cmd::Save(raw_msg)) => {
|
||||||
return msg_handler::save(&mbox, raw_msg, &mut printer, &mut imap);
|
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(
|
return msg_handler::search(
|
||||||
query,
|
query,
|
||||||
max_width,
|
max_width,
|
||||||
page_size,
|
page_size,
|
||||||
page,
|
page,
|
||||||
&account,
|
mbox,
|
||||||
|
&account_config,
|
||||||
&mut printer,
|
&mut printer,
|
||||||
&mut imap,
|
backend,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Send(raw_msg)) => {
|
Some(msg_arg::Cmd::Sort(criteria, query, max_width, page_size, page)) => {
|
||||||
return msg_handler::send(raw_msg, &account, &mut printer, &mut imap, &mut smtp);
|
return msg_handler::sort(
|
||||||
|
criteria,
|
||||||
|
query,
|
||||||
|
max_width,
|
||||||
|
page_size,
|
||||||
|
page,
|
||||||
|
mbox,
|
||||||
|
&account_config,
|
||||||
|
&mut printer,
|
||||||
|
backend,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Write(atts, encrypt)) => {
|
Some(msg_arg::Cmd::Send(raw_msg)) => {
|
||||||
return msg_handler::write(atts, encrypt, &account, &mut printer, &mut imap, &mut smtp);
|
return msg_handler::send(raw_msg, &account_config, &mut printer, backend, &mut smtp);
|
||||||
}
|
}
|
||||||
Some(msg_arg::Command::Flag(m)) => match m {
|
Some(msg_arg::Cmd::Write(atts, encrypt)) => {
|
||||||
Some(flag_arg::Command::Set(seq_range, flags)) => {
|
return msg_handler::write(
|
||||||
return flag_handler::set(seq_range, flags, &mut printer, &mut imap);
|
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)) => {
|
Some(flag_arg::Cmd::Add(seq_range, flags)) => {
|
||||||
return flag_handler::add(seq_range, flags, &mut printer, &mut imap);
|
return flag_handler::add(seq_range, mbox, &flags, &mut printer, backend);
|
||||||
}
|
}
|
||||||
Some(flag_arg::Command::Remove(seq_range, flags)) => {
|
Some(flag_arg::Cmd::Remove(seq_range, flags)) => {
|
||||||
return flag_handler::remove(seq_range, flags, &mut printer, &mut imap);
|
return flag_handler::remove(seq_range, mbox, &flags, &mut printer, backend);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
Some(msg_arg::Command::Tpl(m)) => match m {
|
Some(msg_arg::Cmd::Tpl(m)) => match m {
|
||||||
Some(tpl_arg::Command::New(tpl)) => {
|
Some(tpl_arg::Cmd::New(tpl)) => {
|
||||||
return tpl_handler::new(tpl, &account, &mut printer);
|
return tpl_handler::new(tpl, &account_config, &mut printer);
|
||||||
}
|
}
|
||||||
Some(tpl_arg::Command::Reply(seq, all, tpl)) => {
|
Some(tpl_arg::Cmd::Reply(seq, all, tpl)) => {
|
||||||
return tpl_handler::reply(seq, all, tpl, &account, &mut printer, &mut imap);
|
return tpl_handler::reply(
|
||||||
|
seq,
|
||||||
|
all,
|
||||||
|
tpl,
|
||||||
|
mbox,
|
||||||
|
&account_config,
|
||||||
|
&mut printer,
|
||||||
|
backend,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(tpl_arg::Command::Forward(seq, tpl)) => {
|
Some(tpl_arg::Cmd::Forward(seq, tpl)) => {
|
||||||
return tpl_handler::forward(seq, tpl, &account, &mut printer, &mut imap);
|
return tpl_handler::forward(
|
||||||
|
seq,
|
||||||
|
tpl,
|
||||||
|
mbox,
|
||||||
|
&account_config,
|
||||||
|
&mut printer,
|
||||||
|
backend,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(tpl_arg::Command::Save(atts, tpl)) => {
|
Some(tpl_arg::Cmd::Save(atts, tpl)) => {
|
||||||
return tpl_handler::save(&mbox, &account, atts, tpl, &mut printer, &mut imap);
|
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(
|
return tpl_handler::send(
|
||||||
&mbox,
|
mbox,
|
||||||
&account,
|
&account_config,
|
||||||
atts,
|
atts,
|
||||||
tpl,
|
tpl,
|
||||||
&mut printer,
|
&mut printer,
|
||||||
&mut imap,
|
backend,
|
||||||
&mut smtp,
|
&mut smtp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -202,5 +266,5 @@ fn main() -> Result<()> {
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
imap.logout()
|
backend.disconnect()
|
||||||
}
|
}
|
||||||
|
|
7
src/mbox/mbox.rs
Normal file
7
src/mbox/mbox.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::output::PrintTable;
|
||||||
|
|
||||||
|
pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable {
|
||||||
|
//
|
||||||
|
}
|
|
@ -6,31 +6,31 @@ use anyhow::Result;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::ImapServiceInterface,
|
backends::Backend,
|
||||||
output::{PrintTableOpts, PrinterService},
|
output::{PrintTableOpts, PrinterService},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Lists all mailboxes.
|
/// 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>,
|
max_width: Option<usize>,
|
||||||
printer: &mut Printer,
|
printer: &mut P,
|
||||||
imap: &'a mut ImapService,
|
backend: Box<&'a mut B>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("entering list mailbox handler");
|
info!("entering list mailbox handler");
|
||||||
let mboxes = imap.fetch_mboxes()?;
|
let mboxes = backend.get_mboxes()?;
|
||||||
trace!("mailboxes: {:?}", mboxes);
|
trace!("mailboxes: {:?}", mboxes);
|
||||||
printer.print_table(mboxes, PrintTableOpts { max_width })
|
printer.print_table(mboxes, PrintTableOpts { max_width })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde::Serialize;
|
|
||||||
use std::{fmt::Debug, io};
|
use std::{fmt::Debug, io};
|
||||||
use termcolor::ColorSpec;
|
use termcolor::ColorSpec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Account, Config},
|
backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes},
|
||||||
domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg},
|
mbox::Mboxes,
|
||||||
|
msg::{Envelopes, Msg},
|
||||||
output::{Print, PrintTable, WriteColor},
|
output::{Print, PrintTable, WriteColor},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,15 +78,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrinterService for PrinterServiceTest {
|
impl PrinterService for PrinterServiceTest {
|
||||||
fn print_table<T: Debug + PrintTable + Serialize>(
|
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: T,
|
data: Box<T>,
|
||||||
opts: PrintTableOpts,
|
opts: PrintTableOpts,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
data.print_table(&mut self.writter, opts)?;
|
data.print_table(&mut self.writter, opts)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn print<T: Serialize + Print>(&mut self, _data: T) -> Result<()> {
|
fn print<T: serde::Serialize + Print>(&mut self, _data: T) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn is_json(&self) -> bool {
|
fn is_json(&self) -> bool {
|
||||||
|
@ -94,72 +94,73 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImapServiceTest;
|
struct TestBackend;
|
||||||
|
|
||||||
impl<'a> ImapServiceInterface<'a> for ImapServiceTest {
|
impl<'a> Backend<'a> for TestBackend {
|
||||||
fn fetch_mboxes(&'a mut self) -> Result<Mboxes> {
|
fn add_mbox(&mut self, _: &str) -> Result<()> {
|
||||||
Ok(Mboxes(vec![
|
unimplemented!();
|
||||||
Mbox {
|
}
|
||||||
|
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
|
||||||
|
Ok(Box::new(ImapMboxes(vec![
|
||||||
|
ImapMbox {
|
||||||
delim: "/".into(),
|
delim: "/".into(),
|
||||||
name: "INBOX".into(),
|
name: "INBOX".into(),
|
||||||
attrs: Attrs::from(vec![AttrRemote::NoSelect]),
|
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
|
||||||
},
|
},
|
||||||
Mbox {
|
ImapMbox {
|
||||||
delim: "/".into(),
|
delim: "/".into(),
|
||||||
name: "Sent".into(),
|
name: "Sent".into(),
|
||||||
attrs: Attrs::from(vec![
|
attrs: ImapMboxAttrs(vec![
|
||||||
AttrRemote::NoInferiors,
|
ImapMboxAttr::NoInferiors,
|
||||||
AttrRemote::Custom("HasNoChildren".into()),
|
ImapMboxAttr::Custom("HasNoChildren".into()),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
]))
|
])))
|
||||||
}
|
}
|
||||||
|
fn del_mbox(&mut self, _: &str) -> Result<()> {
|
||||||
fn notify(&mut self, _: &Config, _: &Account, _: u64) -> Result<()> {
|
unimplemented!();
|
||||||
|
}
|
||||||
|
fn get_envelopes(
|
||||||
|
&mut self,
|
||||||
|
_: &str,
|
||||||
|
_: &str,
|
||||||
|
_: &str,
|
||||||
|
_: usize,
|
||||||
|
_: usize,
|
||||||
|
) -> Result<Box<dyn Envelopes>> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn watch(&mut self, _: &Account, _: u64) -> Result<()> {
|
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result<Box<dyn ToString>> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
|
fn get_msg(&mut self, _: &str, _: &str) -> Result<Msg> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
|
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn find_msg(&mut self, _: &Account, _: &str) -> Result<Msg> {
|
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> {
|
fn del_msg(&mut self, _: &str, _: &str) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn append_msg(&mut self, _: &Mbox, _: &Account, _: Msg) -> Result<()> {
|
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> {
|
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
fn expunge(&mut self) -> Result<()> {
|
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> 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<()> {
|
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut printer = PrinterServiceTest::default();
|
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!(
|
assert_eq!(
|
||||||
concat![
|
concat![
|
||||||
"\n",
|
"\n",
|
133
src/msg/addr_entity.rs
Normal file
133
src/msg/addr_entity.rs
Normal 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
13
src/msg/envelope.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,50 +7,63 @@ use anyhow::Result;
|
||||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
|
||||||
use crate::domain::msg::msg_arg;
|
use crate::msg::msg_arg;
|
||||||
|
|
||||||
type SeqRange<'a> = &'a str;
|
type SeqRange<'a> = &'a str;
|
||||||
type Flags<'a> = Vec<&'a str>;
|
type Flags = String;
|
||||||
|
|
||||||
/// Represents the flag commands.
|
/// Represents the flag commands.
|
||||||
pub enum Command<'a> {
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Cmd<'a> {
|
||||||
/// Represents the add flags command.
|
/// Represents the add flags command.
|
||||||
Add(SeqRange<'a>, Flags<'a>),
|
Add(SeqRange<'a>, Flags),
|
||||||
/// Represents the set flags command.
|
/// Represents the set flags command.
|
||||||
Set(SeqRange<'a>, Flags<'a>),
|
Set(SeqRange<'a>, Flags),
|
||||||
/// Represents the remove flags command.
|
/// Represents the remove flags command.
|
||||||
Remove(SeqRange<'a>, Flags<'a>),
|
Remove(SeqRange<'a>, Flags),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defines the flag command matcher.
|
/// 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");
|
info!("entering message flag command matcher");
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("add") {
|
if let Some(m) = m.subcommand_matches("add") {
|
||||||
info!("add subcommand matched");
|
info!("add subcommand matched");
|
||||||
let seq_range = m.value_of("seq-range").unwrap();
|
let seq_range = m.value_of("seq-range").unwrap();
|
||||||
debug!("seq range: {}", seq_range);
|
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);
|
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") {
|
if let Some(m) = m.subcommand_matches("set") {
|
||||||
info!("set subcommand matched");
|
info!("set subcommand matched");
|
||||||
let seq_range = m.value_of("seq-range").unwrap();
|
let seq_range = m.value_of("seq-range").unwrap();
|
||||||
debug!("seq range: {}", seq_range);
|
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);
|
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") {
|
if let Some(m) = m.subcommand_matches("remove") {
|
||||||
info!("remove subcommand matched");
|
info!("remove subcommand matched");
|
||||||
let seq_range = m.value_of("seq-range").unwrap();
|
let seq_range = m.value_of("seq-range").unwrap();
|
||||||
debug!("seq range: {}", seq_range);
|
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);
|
debug!("flags: {:?}", flags);
|
||||||
return Ok(Some(Command::Remove(seq_range, flags)));
|
return Ok(Some(Cmd::Remove(seq_range, flags)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
55
src/msg/flag_handler.rs
Normal file
55
src/msg/flag_handler.rs
Normal 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
|
||||||
|
))
|
||||||
|
}
|
|
@ -7,10 +7,8 @@ use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::{
|
mbox::mbox_arg,
|
||||||
mbox::mbox_arg,
|
msg::{flag_arg, msg_arg, tpl_arg},
|
||||||
msg::{flag_arg, msg_arg, tpl_arg},
|
|
||||||
},
|
|
||||||
ui::table_arg,
|
ui::table_arg,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,9 +24,11 @@ type Query = String;
|
||||||
type AttachmentPaths<'a> = Vec<&'a str>;
|
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||||
type MaxTableWidth = Option<usize>;
|
type MaxTableWidth = Option<usize>;
|
||||||
type Encrypt = bool;
|
type Encrypt = bool;
|
||||||
|
type Criteria = String;
|
||||||
|
|
||||||
/// Message commands.
|
/// Message commands.
|
||||||
pub enum Command<'a> {
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Cmd<'a> {
|
||||||
Attachments(Seq<'a>),
|
Attachments(Seq<'a>),
|
||||||
Copy(Seq<'a>, Mbox<'a>),
|
Copy(Seq<'a>, Mbox<'a>),
|
||||||
Delete(Seq<'a>),
|
Delete(Seq<'a>),
|
||||||
|
@ -39,22 +39,23 @@ pub enum Command<'a> {
|
||||||
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
|
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
|
||||||
Save(RawMsg<'a>),
|
Save(RawMsg<'a>),
|
||||||
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
||||||
|
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
|
||||||
Send(RawMsg<'a>),
|
Send(RawMsg<'a>),
|
||||||
Write(AttachmentPaths<'a>, Encrypt),
|
Write(AttachmentPaths<'a>, Encrypt),
|
||||||
|
|
||||||
Flag(Option<flag_arg::Command<'a>>),
|
Flag(Option<flag_arg::Cmd<'a>>),
|
||||||
Tpl(Option<tpl_arg::Command<'a>>),
|
Tpl(Option<tpl_arg::Cmd<'a>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message command matcher.
|
/// 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");
|
info!("entering message command matcher");
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("attachments") {
|
if let Some(m) = m.subcommand_matches("attachments") {
|
||||||
info!("attachments command matched");
|
info!("attachments command matched");
|
||||||
let seq = m.value_of("seq").unwrap();
|
let seq = m.value_of("seq").unwrap();
|
||||||
debug!("seq: {}", seq);
|
debug!("seq: {}", seq);
|
||||||
return Ok(Some(Command::Attachments(seq)));
|
return Ok(Some(Cmd::Attachments(seq)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("copy") {
|
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);
|
debug!("seq: {}", seq);
|
||||||
let mbox = m.value_of("mbox-target").unwrap();
|
let mbox = m.value_of("mbox-target").unwrap();
|
||||||
debug!(r#"target mailbox: "{:?}""#, mbox);
|
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") {
|
if let Some(m) = m.subcommand_matches("delete") {
|
||||||
info!("copy command matched");
|
info!("copy command matched");
|
||||||
let seq = m.value_of("seq").unwrap();
|
let seq = m.value_of("seq").unwrap();
|
||||||
debug!("seq: {}", seq);
|
debug!("seq: {}", seq);
|
||||||
return Ok(Some(Command::Delete(seq)));
|
return Ok(Some(Cmd::Delete(seq)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("forward") {
|
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);
|
debug!("attachments paths: {:?}", paths);
|
||||||
let encrypt = m.is_present("encrypt");
|
let encrypt = m.is_present("encrypt");
|
||||||
debug!("encrypt: {}", 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") {
|
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)
|
.map(|page| 1.max(page) - 1)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
debug!("page: {}", page);
|
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") {
|
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);
|
debug!("seq: {}", seq);
|
||||||
let mbox = m.value_of("mbox-target").unwrap();
|
let mbox = m.value_of("mbox-target").unwrap();
|
||||||
debug!("target mailbox: {:?}", mbox);
|
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") {
|
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);
|
debug!("text mime: {}", mime);
|
||||||
let raw = m.is_present("raw");
|
let raw = m.is_present("raw");
|
||||||
debug!("raw: {}", 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") {
|
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");
|
let encrypt = m.is_present("encrypt");
|
||||||
debug!("encrypt: {}", 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") {
|
if let Some(m) = m.subcommand_matches("save") {
|
||||||
info!("save command matched");
|
info!("save command matched");
|
||||||
let msg = m.value_of("message").unwrap_or_default();
|
let msg = m.value_of("message").unwrap_or_default();
|
||||||
trace!("message: {}", msg);
|
trace!("message: {}", msg);
|
||||||
return Ok(Some(Command::Save(msg)));
|
return Ok(Some(Cmd::Save(msg)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("search") {
|
if let Some(m) = m.subcommand_matches("search") {
|
||||||
|
@ -185,7 +186,58 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||||
.1
|
.1
|
||||||
.join(" ");
|
.join(" ");
|
||||||
debug!("query: {}", query);
|
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,
|
query,
|
||||||
max_table_width,
|
max_table_width,
|
||||||
page_size,
|
page_size,
|
||||||
|
@ -197,7 +249,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||||
info!("send command matched");
|
info!("send command matched");
|
||||||
let msg = m.value_of("message").unwrap_or_default();
|
let msg = m.value_of("message").unwrap_or_default();
|
||||||
trace!("message: {}", msg);
|
trace!("message: {}", msg);
|
||||||
return Ok(Some(Command::Send(msg)));
|
return Ok(Some(Cmd::Send(msg)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("write") {
|
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);
|
debug!("attachments paths: {:?}", attachment_paths);
|
||||||
let encrypt = m.is_present("encrypt");
|
let encrypt = m.is_present("encrypt");
|
||||||
debug!("encrypt: {}", 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") {
|
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") {
|
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");
|
info!("default list command matched");
|
||||||
Ok(Some(Command::List(None, None, 0)))
|
Ok(Some(Cmd::List(None, None, 0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message sequence number argument.
|
/// Message sequence number argument.
|
||||||
|
@ -313,13 +365,45 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.required(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")
|
SubCommand::with_name("write")
|
||||||
.about("Writes a new message")
|
.about("Writes a new message")
|
||||||
.arg(attachment_arg())
|
.arg(attachment_arg())
|
||||||
.arg(encrypt_arg()),
|
.arg(encrypt_arg()),
|
||||||
SubCommand::with_name("send")
|
SubCommand::with_name("send")
|
||||||
.about("Sends a raw message")
|
.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")
|
SubCommand::with_name("save")
|
||||||
.about("Saves a raw message")
|
.about("Saves a raw message")
|
||||||
.arg(Arg::with_name("message").raw(true)),
|
.arg(Arg::with_name("message").raw(true)),
|
|
@ -2,38 +2,27 @@ use ammonia;
|
||||||
use anyhow::{anyhow, Context, Error, Result};
|
use anyhow::{anyhow, Context, Error, Result};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use html_escape;
|
use html_escape;
|
||||||
use imap::types::Flag;
|
|
||||||
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
|
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rfc2047_decoder;
|
use std::{collections::HashSet, env::temp_dir, fmt::Debug, fs, path::PathBuf};
|
||||||
use std::{
|
|
||||||
collections::HashSet,
|
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
env::temp_dir,
|
|
||||||
fmt::Debug,
|
|
||||||
fs,
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Account, DEFAULT_SIG_DELIM},
|
backends::Backend,
|
||||||
domain::{
|
config::{AccountConfig, DEFAULT_SIG_DELIM},
|
||||||
imap::ImapServiceInterface,
|
msg::{
|
||||||
mbox::Mbox,
|
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils,
|
||||||
msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride},
|
Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride,
|
||||||
smtp::SmtpServiceInterface,
|
|
||||||
},
|
},
|
||||||
output::PrinterService,
|
output::PrinterService,
|
||||||
|
smtp::SmtpService,
|
||||||
ui::{
|
ui::{
|
||||||
choice::{self, PostEditChoice, PreEditChoice},
|
choice::{self, PostEditChoice, PreEditChoice},
|
||||||
editor,
|
editor,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Addr = lettre::message::Mailbox;
|
|
||||||
|
|
||||||
/// Representation of a message.
|
/// Representation of a message.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Msg {
|
pub struct Msg {
|
||||||
|
@ -42,17 +31,14 @@ pub struct Msg {
|
||||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
|
|
||||||
/// The flags attached to the message.
|
|
||||||
pub flags: Flags,
|
|
||||||
|
|
||||||
/// The subject of the message.
|
/// The subject of the message.
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
|
|
||||||
pub from: Option<Vec<Addr>>,
|
pub from: Option<Addrs>,
|
||||||
pub reply_to: Option<Vec<Addr>>,
|
pub reply_to: Option<Addrs>,
|
||||||
pub to: Option<Vec<Addr>>,
|
pub to: Option<Addrs>,
|
||||||
pub cc: Option<Vec<Addr>>,
|
pub cc: Option<Addrs>,
|
||||||
pub bcc: Option<Vec<Addr>>,
|
pub bcc: Option<Addrs>,
|
||||||
pub in_reply_to: Option<String>,
|
pub in_reply_to: Option<String>,
|
||||||
pub message_id: Option<String>,
|
pub message_id: Option<String>,
|
||||||
|
|
||||||
|
@ -63,6 +49,8 @@ pub struct Msg {
|
||||||
pub parts: Parts,
|
pub parts: Parts,
|
||||||
|
|
||||||
pub encrypt: bool,
|
pub encrypt: bool,
|
||||||
|
|
||||||
|
pub raw: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Msg {
|
impl Msg {
|
||||||
|
@ -173,8 +161,8 @@ impl Msg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_reply(mut self, all: bool, account: &Account) -> Result<Self> {
|
pub fn into_reply(mut self, all: bool, account: &AccountConfig) -> Result<Self> {
|
||||||
let account_addr: Addr = account.address().parse()?;
|
let account_addr = account.address()?;
|
||||||
|
|
||||||
// Message-Id
|
// Message-Id
|
||||||
self.message_id = None;
|
self.message_id = None;
|
||||||
|
@ -183,13 +171,13 @@ impl Msg {
|
||||||
self.in_reply_to = self.message_id.to_owned();
|
self.in_reply_to = self.message_id.to_owned();
|
||||||
|
|
||||||
// From
|
// From
|
||||||
self.from = Some(vec![account_addr.to_owned()]);
|
self.from = Some(vec![account_addr.clone()].into());
|
||||||
|
|
||||||
// To
|
// To
|
||||||
let addrs = self
|
let addrs = self
|
||||||
.reply_to
|
.reply_to
|
||||||
.as_ref()
|
.as_deref()
|
||||||
.or_else(|| self.from.as_ref())
|
.or_else(|| self.from.as_deref())
|
||||||
.map(|addrs| {
|
.map(|addrs| {
|
||||||
addrs
|
addrs
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -197,11 +185,11 @@ impl Msg {
|
||||||
.filter(|addr| addr != &account_addr)
|
.filter(|addr| addr != &account_addr)
|
||||||
});
|
});
|
||||||
if all {
|
if all {
|
||||||
self.to = addrs.map(|addrs| addrs.collect());
|
self.to = addrs.map(|addrs| addrs.collect::<Vec<_>>().into());
|
||||||
} else {
|
} else {
|
||||||
self.to = addrs
|
self.to = addrs
|
||||||
.and_then(|mut addrs| addrs.next())
|
.and_then(|mut addrs| addrs.next())
|
||||||
.map(|addr| vec![addr]);
|
.map(|addr| vec![addr].into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cc & Bcc
|
// Cc & Bcc
|
||||||
|
@ -226,12 +214,8 @@ impl Msg {
|
||||||
.reply_to
|
.reply_to
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.or_else(|| self.from.as_ref())
|
.or_else(|| self.from.as_ref())
|
||||||
.and_then(|addrs| addrs.first())
|
.and_then(|addrs| addrs.clone().extract_single_info())
|
||||||
.map(|addr| {
|
.map(|addr| addr.display_name.clone().unwrap_or_else(|| addr.addr))
|
||||||
addr.name
|
|
||||||
.to_owned()
|
|
||||||
.unwrap_or_else(|| addr.email.to_string())
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "unknown sender".into());
|
.unwrap_or_else(|| "unknown sender".into());
|
||||||
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
|
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
|
||||||
|
|
||||||
|
@ -255,8 +239,8 @@ impl Msg {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_forward(mut self, account: &Account) -> Result<Self> {
|
pub fn into_forward(mut self, account: &AccountConfig) -> Result<Self> {
|
||||||
let account_addr: Addr = account.address().parse()?;
|
let account_addr = account.address()?;
|
||||||
|
|
||||||
let prev_subject = self.subject.to_owned();
|
let prev_subject = self.subject.to_owned();
|
||||||
let prev_date = self.date.to_owned();
|
let prev_date = self.date.to_owned();
|
||||||
|
@ -270,10 +254,10 @@ impl Msg {
|
||||||
self.in_reply_to = None;
|
self.in_reply_to = None;
|
||||||
|
|
||||||
// From
|
// From
|
||||||
self.from = Some(vec![account_addr]);
|
self.from = Some(vec![account_addr].into());
|
||||||
|
|
||||||
// To
|
// To
|
||||||
self.to = Some(vec![]);
|
self.to = Some(vec![].into());
|
||||||
|
|
||||||
// Cc
|
// Cc
|
||||||
self.cc = None;
|
self.cc = None;
|
||||||
|
@ -295,22 +279,12 @@ impl Msg {
|
||||||
}
|
}
|
||||||
if let Some(addrs) = prev_from.as_ref() {
|
if let Some(addrs) = prev_from.as_ref() {
|
||||||
content.push_str("From: ");
|
content.push_str("From: ");
|
||||||
let mut glue = "";
|
content.push_str(&addrs.to_string());
|
||||||
for addr in addrs {
|
|
||||||
content.push_str(glue);
|
|
||||||
content.push_str(&addr.to_string());
|
|
||||||
glue = ", ";
|
|
||||||
}
|
|
||||||
content.push('\n');
|
content.push('\n');
|
||||||
}
|
}
|
||||||
if let Some(addrs) = prev_to.as_ref() {
|
if let Some(addrs) = prev_to.as_ref() {
|
||||||
content.push_str("To: ");
|
content.push_str("To: ");
|
||||||
let mut glue = "";
|
content.push_str(&addrs.to_string());
|
||||||
for addr in addrs {
|
|
||||||
content.push_str(glue);
|
|
||||||
content.push_str(&addr.to_string());
|
|
||||||
glue = ", ";
|
|
||||||
}
|
|
||||||
content.push('\n');
|
content.push('\n');
|
||||||
}
|
}
|
||||||
content.push('\n');
|
content.push('\n');
|
||||||
|
@ -321,24 +295,19 @@ impl Msg {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _edit_with_editor(&self, account: &Account) -> Result<Self> {
|
fn _edit_with_editor(&self, account: &AccountConfig) -> Result<Self> {
|
||||||
let tpl = self.to_tpl(TplOverride::default(), account);
|
let tpl = self.to_tpl(TplOverride::default(), account)?;
|
||||||
let tpl = editor::open_with_tpl(tpl)?;
|
let tpl = editor::open_with_tpl(tpl)?;
|
||||||
Self::from_tpl(&tpl)
|
Self::from_tpl(&tpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_with_editor<
|
pub fn edit_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||||
'a,
|
|
||||||
Printer: PrinterService,
|
|
||||||
ImapService: ImapServiceInterface<'a>,
|
|
||||||
SmtpService: SmtpServiceInterface,
|
|
||||||
>(
|
|
||||||
mut self,
|
mut self,
|
||||||
account: &Account,
|
account: &AccountConfig,
|
||||||
printer: &mut Printer,
|
printer: &mut P,
|
||||||
imap: &mut ImapService,
|
backend: Box<&'a mut B>,
|
||||||
smtp: &mut SmtpService,
|
smtp: &mut S,
|
||||||
) -> Result<()> {
|
) -> Result<Box<&'a mut B>> {
|
||||||
info!("start editing with editor");
|
info!("start editing with editor");
|
||||||
|
|
||||||
let draft = msg_utils::local_draft_path();
|
let draft = msg_utils::local_draft_path();
|
||||||
|
@ -355,7 +324,7 @@ impl Msg {
|
||||||
self.merge_with(self._edit_with_editor(account)?);
|
self.merge_with(self._edit_with_editor(account)?);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
PreEditChoice::Quit => return Ok(()),
|
PreEditChoice::Quit => return Ok(backend),
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("{}", err);
|
println!("{}", err);
|
||||||
|
@ -370,10 +339,8 @@ impl Msg {
|
||||||
loop {
|
loop {
|
||||||
match choice::post_edit() {
|
match choice::post_edit() {
|
||||||
Ok(PostEditChoice::Send) => {
|
Ok(PostEditChoice::Send) => {
|
||||||
let mbox = Mbox::new(&account.sent_folder);
|
|
||||||
let sent_msg = smtp.send_msg(account, &self)?;
|
let sent_msg = smtp.send_msg(account, &self)?;
|
||||||
let flags = Flags::try_from(vec![Flag::Seen])?;
|
backend.add_msg(&account.sent_folder, &sent_msg.formatted(), "seen")?;
|
||||||
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
|
|
||||||
msg_utils::remove_local_draft()?;
|
msg_utils::remove_local_draft()?;
|
||||||
printer.print("Message successfully sent")?;
|
printer.print("Message successfully sent")?;
|
||||||
break;
|
break;
|
||||||
|
@ -387,10 +354,8 @@ impl Msg {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(PostEditChoice::RemoteDraft) => {
|
Ok(PostEditChoice::RemoteDraft) => {
|
||||||
let mbox = Mbox::new(&account.draft_folder);
|
let tpl = self.to_tpl(TplOverride::default(), account)?;
|
||||||
let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?;
|
backend.add_msg(&account.draft_folder, tpl.as_bytes(), "seen draft")?;
|
||||||
let tpl = self.to_tpl(TplOverride::default(), account);
|
|
||||||
imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?;
|
|
||||||
msg_utils::remove_local_draft()?;
|
msg_utils::remove_local_draft()?;
|
||||||
printer.print(format!(
|
printer.print(format!(
|
||||||
"Message successfully saved to {}",
|
"Message successfully saved to {}",
|
||||||
|
@ -409,7 +374,7 @@ impl Msg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encrypt(mut self, encrypt: bool) -> Self {
|
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();
|
let mut tpl = String::default();
|
||||||
|
|
||||||
tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
|
tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
|
||||||
|
@ -490,7 +456,7 @@ impl Msg {
|
||||||
"From: {}\n",
|
"From: {}\n",
|
||||||
opts.from
|
opts.from
|
||||||
.map(|addrs| addrs.join(", "))
|
.map(|addrs| addrs.join(", "))
|
||||||
.unwrap_or_else(|| account.address())
|
.unwrap_or_else(|| account_addr.to_string())
|
||||||
));
|
));
|
||||||
|
|
||||||
// To
|
// To
|
||||||
|
@ -498,37 +464,25 @@ impl Msg {
|
||||||
"To: {}\n",
|
"To: {}\n",
|
||||||
opts.to
|
opts.to
|
||||||
.map(|addrs| addrs.join(", "))
|
.map(|addrs| addrs.join(", "))
|
||||||
.or_else(|| self.to.clone().map(|addrs| addrs
|
.or_else(|| self.to.clone().map(|addrs| addrs.to_string()))
|
||||||
.iter()
|
|
||||||
.map(|addr| addr.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")))
|
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
));
|
));
|
||||||
|
|
||||||
// Cc
|
// Cc
|
||||||
if let Some(addrs) = opts.cc.map(|addrs| addrs.join(", ")).or_else(|| {
|
if let Some(addrs) = opts
|
||||||
self.cc.clone().map(|addrs| {
|
.cc
|
||||||
addrs
|
.map(|addrs| addrs.join(", "))
|
||||||
.iter()
|
.or_else(|| self.cc.clone().map(|addrs| addrs.to_string()))
|
||||||
.map(|addr| addr.to_string())
|
{
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
})
|
|
||||||
}) {
|
|
||||||
tpl.push_str(&format!("Cc: {}\n", addrs));
|
tpl.push_str(&format!("Cc: {}\n", addrs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bcc
|
// Bcc
|
||||||
if let Some(addrs) = opts.bcc.map(|addrs| addrs.join(", ")).or_else(|| {
|
if let Some(addrs) = opts
|
||||||
self.bcc.clone().map(|addrs| {
|
.bcc
|
||||||
addrs
|
.map(|addrs| addrs.join(", "))
|
||||||
.iter()
|
.or_else(|| self.bcc.clone().map(|addrs| addrs.to_string()))
|
||||||
.map(|addr| addr.to_string())
|
{
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
})
|
|
||||||
}) {
|
|
||||||
tpl.push_str(&format!("Bcc: {}\n", addrs));
|
tpl.push_str(&format!("Bcc: {}\n", addrs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,72 +514,20 @@ impl Msg {
|
||||||
tpl.push('\n');
|
tpl.push('\n');
|
||||||
|
|
||||||
trace!("template: {:?}", tpl);
|
trace!("template: {:?}", tpl);
|
||||||
tpl
|
Ok(tpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_tpl(tpl: &str) -> Result<Self> {
|
pub fn from_tpl(tpl: &str) -> Result<Self> {
|
||||||
info!("begin: building message from template");
|
info!("begin: building message from template");
|
||||||
trace!("template: {:?}", tpl);
|
trace!("template: {:?}", tpl);
|
||||||
|
|
||||||
let mut msg = Msg::default();
|
let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?;
|
||||||
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 }));
|
|
||||||
|
|
||||||
info!("end: building message from template");
|
info!("end: building message from template");
|
||||||
trace!("message: {:?}", msg);
|
Self::from_parsed_mail(parsed_mail, &AccountConfig::default())
|
||||||
Ok(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
let mut msg_builder = lettre::Message::builder()
|
||||||
.message_id(self.message_id.to_owned())
|
.message_id(self.message_id.to_owned())
|
||||||
.subject(self.subject.to_owned());
|
.subject(self.subject.to_owned());
|
||||||
|
@ -635,33 +537,33 @@ impl Msg {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(addrs) = self.from.as_ref() {
|
if let Some(addrs) = self.from.as_ref() {
|
||||||
msg_builder = addrs
|
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||||
.iter()
|
msg_builder = msg_builder.from(addr)
|
||||||
.fold(msg_builder, |builder, addr| builder.from(addr.to_owned()))
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(addrs) = self.to.as_ref() {
|
if let Some(addrs) = self.to.as_ref() {
|
||||||
msg_builder = addrs
|
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||||
.iter()
|
msg_builder = msg_builder.to(addr)
|
||||||
.fold(msg_builder, |builder, addr| builder.to(addr.to_owned()))
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(addrs) = self.reply_to.as_ref() {
|
if let Some(addrs) = self.reply_to.as_ref() {
|
||||||
msg_builder = addrs.iter().fold(msg_builder, |builder, addr| {
|
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||||
builder.reply_to(addr.to_owned())
|
msg_builder = msg_builder.reply_to(addr)
|
||||||
})
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(addrs) = self.cc.as_ref() {
|
if let Some(addrs) = self.cc.as_ref() {
|
||||||
msg_builder = addrs
|
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||||
.iter()
|
msg_builder = msg_builder.cc(addr)
|
||||||
.fold(msg_builder, |builder, addr| builder.cc(addr.to_owned()))
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(addrs) = self.bcc.as_ref() {
|
if let Some(addrs) = self.bcc.as_ref() {
|
||||||
msg_builder = addrs
|
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||||
.iter()
|
msg_builder = msg_builder.bcc(addr)
|
||||||
.fold(msg_builder, |builder, addr| builder.bcc(addr.to_owned()))
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut multipart = {
|
let mut multipart = {
|
||||||
|
@ -682,11 +584,14 @@ impl Msg {
|
||||||
if self.encrypt {
|
if self.encrypt {
|
||||||
let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string());
|
let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string());
|
||||||
fs::write(multipart_buffer.clone(), multipart.formatted())?;
|
fs::write(multipart_buffer.clone(), multipart.formatted())?;
|
||||||
|
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
|
let encrypted_multipart = account
|
||||||
.pgp_encrypt_file(
|
.pgp_encrypt_file(&addr, multipart_buffer.clone())?
|
||||||
&self.to.as_ref().unwrap().first().unwrap().email.to_string(),
|
|
||||||
multipart_buffer.clone(),
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?;
|
.ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?;
|
||||||
trace!("encrypted multipart: {:#?}", encrypted_multipart);
|
trace!("encrypted multipart: {:#?}", encrypted_multipart);
|
||||||
multipart = MultiPart::encrypted(String::from("application/pgp-encrypted"))
|
multipart = MultiPart::encrypted(String::from("application/pgp-encrypted"))
|
||||||
|
@ -706,187 +611,82 @@ impl Msg {
|
||||||
.multipart(multipart)
|
.multipart(multipart)
|
||||||
.context("cannot build sendable message")
|
.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 {
|
impl TryInto<lettre::address::Envelope> for Msg {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||||
let from: Option<lettre::Address> = self
|
let from = match self.from.and_then(|addrs| addrs.extract_single_info()) {
|
||||||
.from
|
Some(addr) => addr.addr.parse().map(Some),
|
||||||
.and_then(|addrs| addrs.into_iter().next())
|
None => Ok(None),
|
||||||
.map(|addr| addr.email);
|
}?;
|
||||||
let to = self
|
let to = self
|
||||||
.to
|
.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()
|
.as_ref()
|
||||||
.map(|subj| {
|
.map(from_addrs_to_sendable_addrs)
|
||||||
rfc2047_decoder::decode(subj).context(format!(
|
.unwrap_or(Ok(vec![]))?;
|
||||||
"cannot decode subject of message {}",
|
Ok(lettre::address::Envelope::new(from, to).context("cannot create envelope")?)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
348
src/msg/msg_handler.rs
Normal 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(())
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::config::Account;
|
use crate::config::AccountConfig;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize)]
|
#[derive(Debug, Clone, Default, Serialize)]
|
||||||
pub struct TextPlainPart {
|
pub struct TextPlainPart {
|
||||||
|
@ -51,7 +51,7 @@ impl Parts {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_parsed_mail<'a>(
|
pub fn from_parsed_mail<'a>(
|
||||||
account: &'a Account,
|
account: &'a AccountConfig,
|
||||||
part: &'a mailparse::ParsedMail<'a>,
|
part: &'a mailparse::ParsedMail<'a>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let mut parts = vec![];
|
let mut parts = vec![];
|
||||||
|
@ -75,7 +75,7 @@ impl DerefMut for Parts {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_parts_map_rec(
|
fn build_parts_map_rec(
|
||||||
account: &Account,
|
account: &AccountConfig,
|
||||||
parsed_mail: &mailparse::ParsedMail,
|
parsed_mail: &mailparse::ParsedMail,
|
||||||
parts: &mut Vec<Part>,
|
parts: &mut Vec<Part>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
@ -133,7 +133,7 @@ fn build_parts_map_rec(
|
||||||
Ok(())
|
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_path = env::temp_dir().join(Uuid::new_v4().to_string());
|
||||||
let msg_body = msg
|
let msg_body = msg
|
||||||
.get_body()
|
.get_body()
|
|
@ -6,14 +6,14 @@ use anyhow::Result;
|
||||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace};
|
||||||
|
|
||||||
use crate::domain::msg::msg_arg;
|
use crate::msg::msg_arg;
|
||||||
|
|
||||||
type Seq<'a> = &'a str;
|
type Seq<'a> = &'a str;
|
||||||
type ReplyAll = bool;
|
type ReplyAll = bool;
|
||||||
type AttachmentPaths<'a> = Vec<&'a str>;
|
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||||
type Tpl<'a> = &'a str;
|
type Tpl<'a> = &'a str;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
pub struct TplOverride<'a> {
|
pub struct TplOverride<'a> {
|
||||||
pub subject: Option<&'a str>,
|
pub subject: Option<&'a str>,
|
||||||
pub from: Option<Vec<&'a str>>,
|
pub from: Option<Vec<&'a str>>,
|
||||||
|
@ -41,7 +41,8 @@ impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message template commands.
|
/// Message template commands.
|
||||||
pub enum Command<'a> {
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Cmd<'a> {
|
||||||
New(TplOverride<'a>),
|
New(TplOverride<'a>),
|
||||||
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
|
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
|
||||||
Forward(Seq<'a>, TplOverride<'a>),
|
Forward(Seq<'a>, TplOverride<'a>),
|
||||||
|
@ -50,14 +51,14 @@ pub enum Command<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message template command matcher.
|
/// 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");
|
info!("entering message template command matcher");
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("new") {
|
if let Some(m) = m.subcommand_matches("new") {
|
||||||
info!("new subcommand matched");
|
info!("new subcommand matched");
|
||||||
let tpl = TplOverride::from(m);
|
let tpl = TplOverride::from(m);
|
||||||
trace!("template override: {:?}", tpl);
|
trace!("template override: {:?}", tpl);
|
||||||
return Ok(Some(Command::New(tpl)));
|
return Ok(Some(Cmd::New(tpl)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = m.subcommand_matches("reply") {
|
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);
|
debug!("reply all: {}", all);
|
||||||
let tpl = TplOverride::from(m);
|
let tpl = TplOverride::from(m);
|
||||||
trace!("template override: {:?}", tpl);
|
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") {
|
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);
|
debug!("sequence: {}", seq);
|
||||||
let tpl = TplOverride::from(m);
|
let tpl = TplOverride::from(m);
|
||||||
trace!("template args: {:?}", tpl);
|
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") {
|
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);
|
trace!("attachments paths: {:?}", attachment_paths);
|
||||||
let tpl = m.value_of("template").unwrap_or_default();
|
let tpl = m.value_of("template").unwrap_or_default();
|
||||||
trace!("template: {}", tpl);
|
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") {
|
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);
|
trace!("attachments paths: {:?}", attachment_paths);
|
||||||
let tpl = m.value_of("template").unwrap_or_default();
|
let tpl = m.value_of("template").unwrap_or_default();
|
||||||
trace!("template: {}", tpl);
|
trace!("template: {}", tpl);
|
||||||
return Ok(Some(Command::Send(attachment_paths, tpl)));
|
return Ok(Some(Cmd::Send(attachment_paths, tpl)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
109
src/msg/tpl_handler.rs
Normal file
109
src/msg/tpl_handler.rs
Normal 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")
|
||||||
|
}
|
|
@ -1,9 +1,5 @@
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use serde::Serialize;
|
use std::{convert::TryFrom, fmt};
|
||||||
use std::{
|
|
||||||
convert::TryFrom,
|
|
||||||
fmt::{self, Display},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents the available output formats.
|
/// Represents the available output formats.
|
||||||
#[derive(Debug, PartialEq)]
|
#[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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let fmt = match *self {
|
let fmt = match *self {
|
||||||
OutputFmt::Json => "JSON",
|
OutputFmt::Json => "JSON",
|
||||||
|
@ -45,12 +41,12 @@ impl Display for OutputFmt {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defines a struct-wrapper to provide a JSON output.
|
/// Defines a struct-wrapper to provide a JSON output.
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
pub struct OutputJson<T: Serialize> {
|
pub struct OutputJson<T: serde::Serialize> {
|
||||||
response: T,
|
response: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Serialize> OutputJson<T> {
|
impl<T: serde::Serialize> OutputJson<T> {
|
||||||
pub fn new(response: T) -> Self {
|
pub fn new(response: T) -> Self {
|
||||||
Self { response }
|
Self { response }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
use serde::Serialize;
|
use std::{
|
||||||
use std::{convert::TryFrom, fmt::Debug};
|
convert::TryFrom,
|
||||||
|
fmt::{self, Debug},
|
||||||
|
};
|
||||||
use termcolor::{ColorChoice, StandardStream};
|
use termcolor::{ColorChoice, StandardStream};
|
||||||
|
|
||||||
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
|
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
|
||||||
|
|
||||||
pub trait PrinterService {
|
pub trait PrinterService {
|
||||||
fn print<T: Debug + Print + Serialize>(&mut self, data: T) -> Result<()>;
|
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
|
||||||
fn print_table<T: Debug + PrintTable + Serialize>(
|
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: T,
|
data: Box<T>,
|
||||||
opts: PrintTableOpts,
|
opts: PrintTableOpts,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
fn is_json(&self) -> bool;
|
fn is_json(&self) -> bool;
|
||||||
|
@ -22,7 +24,7 @@ pub struct StdoutPrinter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrinterService for 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 {
|
match self.fmt {
|
||||||
OutputFmt::Plain => data.print(self.writter.as_mut()),
|
OutputFmt::Plain => data.print(self.writter.as_mut()),
|
||||||
OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data))
|
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,
|
&mut self,
|
||||||
data: T,
|
data: Box<T>,
|
||||||
opts: PrintTableOpts,
|
opts: PrintTableOpts,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match self.fmt {
|
match self.fmt {
|
||||||
OutputFmt::Plain => data.print_table(self.writter.as_mut(), opts),
|
OutputFmt::Plain => data.print_table(self.writter.as_mut(), opts),
|
||||||
OutputFmt::Json => serde_json::to_writer(self.writter.as_mut(), &OutputJson::new(data))
|
OutputFmt::Json => {
|
||||||
.context("cannot write JSON to writter"),
|
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
1
src/smtp/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
//! Module related to SMTP.
|
|
@ -9,19 +9,19 @@ use lettre::{
|
||||||
};
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{config::Account, domain::msg::Msg};
|
use crate::{config::AccountConfig, msg::Msg};
|
||||||
|
|
||||||
pub trait SmtpServiceInterface {
|
pub trait SmtpService {
|
||||||
fn send_msg(&mut self, account: &Account, msg: &Msg) -> Result<lettre::Message>;
|
fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result<lettre::Message>;
|
||||||
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
|
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SmtpService<'a> {
|
pub struct LettreService<'a> {
|
||||||
account: &'a Account,
|
account: &'a AccountConfig,
|
||||||
transport: Option<SmtpTransport>,
|
transport: Option<SmtpTransport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SmtpService<'a> {
|
impl<'a> LettreService<'a> {
|
||||||
fn transport(&mut self) -> Result<&SmtpTransport> {
|
fn transport(&mut self) -> Result<&SmtpTransport> {
|
||||||
if let Some(ref transport) = self.transport {
|
if let Some(ref transport) = self.transport {
|
||||||
Ok(transport)
|
Ok(transport)
|
||||||
|
@ -55,8 +55,8 @@ impl<'a> SmtpService<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SmtpServiceInterface for SmtpService<'a> {
|
impl<'a> SmtpService for LettreService<'a> {
|
||||||
fn send_msg(&mut self, account: &Account, msg: &Msg) -> Result<lettre::Message> {
|
fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result<lettre::Message> {
|
||||||
debug!("sending message…");
|
debug!("sending message…");
|
||||||
let sendable_msg = msg.into_sendable_msg(account)?;
|
let sendable_msg = msg.into_sendable_msg(account)?;
|
||||||
self.transport()?.send(&sendable_msg)?;
|
self.transport()?.send(&sendable_msg)?;
|
||||||
|
@ -70,8 +70,8 @@ impl<'a> SmtpServiceInterface for SmtpService<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Account> for SmtpService<'a> {
|
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
|
||||||
fn from(account: &'a Account) -> Self {
|
fn from(account: &'a AccountConfig) -> Self {
|
||||||
debug!("init SMTP service");
|
debug!("init SMTP service");
|
||||||
Self {
|
Self {
|
||||||
account,
|
account,
|
|
@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::{env, fs, process::Command};
|
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> {
|
pub fn open_with_tpl(tpl: String) -> Result<String> {
|
||||||
let path = msg_utils::local_draft_path();
|
let path = msg_utils::local_draft_path();
|
||||||
|
|
6
tests/emails/alice-to-patrick.eml
Normal file
6
tests/emails/alice-to-patrick.eml
Normal 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.
|
90
tests/test_imap_backend.rs
Normal file
90
tests/test_imap_backend.rs
Normal 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();
|
||||||
|
}
|
68
tests/test_maildir_backend.rs
Normal file
68
tests/test_maildir_backend.rs
Normal 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());
|
||||||
|
}
|
Loading…
Reference in a new issue