mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-21 18:40:19 +00:00
release v0.5.5 (#290)
* update main screenshot readme * add contributing file * update changelog * doc: remove roadmap from reame * improve main comments * improve arg and handler logs * fix multiple recipients issue (#288) * add notify-query config option (#289) * set up end-to-end encryption (#287) * init basic pgp encrypt/decrypt * add small rpgp poc for (#286) * improve decrypt parts logs * add pgp-decrypt-cmd to config * add pgp-encrypt-cmd to config * init pgp signature * improve decrypt part readability * improve encrypt multipart, remove sign * remove unused md5 lib * add encrypt arg to reply and forward commands * fix typos * prepare v0.5.5
This commit is contained in:
parent
e33a9a72e9
commit
585fa77af5
29 changed files with 768 additions and 280 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.5] - 2022-02-08
|
||||
|
||||
### Added
|
||||
|
||||
- [Contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) [#256]
|
||||
- Notify query config option [#289]
|
||||
- End-to-end encryption *(EXPERIMENTAL)* [#54]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Multiple recipients issue [#288]
|
||||
|
||||
## [0.5.4] - 2022-02-05
|
||||
|
||||
### Fixed
|
||||
|
@ -280,7 +292,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Password from command [#22]
|
||||
- Set up README [#20]
|
||||
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.4...HEAD
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.5...HEAD
|
||||
[0.5.5]: https://github.com/soywod/himalaya/compare/v0.5.4...v0.5.5
|
||||
[0.5.4]: https://github.com/soywod/himalaya/compare/v0.5.3...v0.5.4
|
||||
[0.5.3]: https://github.com/soywod/himalaya/compare/v0.5.2...v0.5.3
|
||||
[0.5.2]: https://github.com/soywod/himalaya/compare/v0.5.1...v0.5.2
|
||||
|
@ -336,6 +349,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#47]: https://github.com/soywod/himalaya/issues/47
|
||||
[#48]: https://github.com/soywod/himalaya/issues/48
|
||||
[#50]: https://github.com/soywod/himalaya/issues/50
|
||||
[#54]: https://github.com/soywod/himalaya/issues/54
|
||||
[#58]: https://github.com/soywod/himalaya/issues/58
|
||||
[#59]: https://github.com/soywod/himalaya/issues/59
|
||||
[#61]: https://github.com/soywod/himalaya/issues/61
|
||||
|
@ -392,6 +406,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#228]: https://github.com/soywod/himalaya/issues/228
|
||||
[#229]: https://github.com/soywod/himalaya/issues/229
|
||||
[#249]: https://github.com/soywod/himalaya/issues/249
|
||||
[#256]: https://github.com/soywod/himalaya/issues/256
|
||||
[#259]: https://github.com/soywod/himalaya/issues/259
|
||||
[#268]: https://github.com/soywod/himalaya/issues/268
|
||||
[#272]: https://github.com/soywod/himalaya/issues/272
|
||||
|
@ -400,3 +415,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#271]: https://github.com/soywod/himalaya/issues/271
|
||||
[#276]: https://github.com/soywod/himalaya/issues/276
|
||||
[#280]: https://github.com/soywod/himalaya/issues/280
|
||||
[#288]: https://github.com/soywod/himalaya/issues/288
|
||||
[#289]: https://github.com/soywod/himalaya/issues/289
|
||||
|
|
42
CONTRIBUTING.md
Normal file
42
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Himalaya contributing guide
|
||||
|
||||
Thank you for investing your time in contributing to Himalaya!
|
||||
|
||||
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
|
||||
|
||||
## New contributor guide
|
||||
|
||||
To get an overview of the project, read the [README](README.md). To get more information about the project, read the [wiki](https://github.com/soywod/himalaya/wiki).
|
||||
|
||||
## Getting started
|
||||
|
||||
### Issues
|
||||
|
||||
#### Create a new issue
|
||||
|
||||
If you spot a problem with the docs, [search if an issue already exists](https://github.com/soywod/himalaya/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/soywod/himalaya/issues/new/choose).
|
||||
|
||||
#### Solve an issue
|
||||
|
||||
Scan through our [existing issues](https://github.com/soywod/himalaya/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
|
||||
### Make Changes
|
||||
|
||||
#### Make changes in the UI
|
||||
|
||||
Click **Make a contribution** at the bottom of any docs page to make small changes such as a typo, sentence fix, or a broken link. This takes you to the `.md` file where you can make your changes and [create a pull request](#pull-request) for a review.
|
||||
|
||||
#### Make changes locally
|
||||
|
||||
First, follow the instructions on [how to install Himalaya from sources](https://github.com/soywod/himalaya/wiki/Installation:sources). Then, create a working branch and start with your changes!
|
||||
|
||||
### Commit your update
|
||||
|
||||
Commit the changes once you are happy with them. Commit messages follow the [Angular Convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153), but contain only a subject. The subject can be prefixed with a custom context like `msg: `, `mbox: `, `imap: ` etc.
|
||||
|
||||
> Use imperative, present tense: “change” not “changed” nor
|
||||
> “changes”<br>Don't capitalize first letter<br>No dot (.) at the end
|
||||
|
||||
### Pull Request
|
||||
|
||||
When you're finished with the changes, create a pull request, also known as a PR.
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -361,7 +361,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "himalaya"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2018"
|
||||
license-file = "LICENSE"
|
||||
|
|
26
README.md
26
README.md
|
@ -2,22 +2,15 @@
|
|||
|
||||
Command-line interface for email management
|
||||
|
||||
*The project is under active development. Do not use in production before the
|
||||
`v1.0.0` (see the [roadmap](https://github.com/soywod/himalaya/milestone/5)).*
|
||||
*The project is under active development. Do not use in production before the `v1.0.0`.*
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/115144003-8a1b4880-a04a-11eb-80d2-245027e28591.png)
|
||||
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
|
||||
|
||||
## Motivation
|
||||
|
||||
Bringing emails to the terminal is a *pain*. First, because they are sensitive
|
||||
data. Secondly, the existing TUIs ([Mutt](http://www.mutt.org/),
|
||||
[NeoMutt](https://neomutt.org/), [Alpine](https://alpine.x10host.com/),
|
||||
[aerc](https://aerc-mail.org/)…) are really hard to configure. They require time
|
||||
and patience.
|
||||
Bringing emails to the terminal is a *pain*. First, because they are sensitive data. Secondly, the existing TUIs ([Mutt](http://www.mutt.org/), [NeoMutt](https://neomutt.org/), [Alpine](https://alpine.x10host.com/), [aerc](https://aerc-mail.org/)…) are really hard to configure. They require time and patience.
|
||||
|
||||
The aim of Himalaya is to extract the email logic into a simple (yet solid) CLI
|
||||
API that can be used directly from the terminal, from scripts, from UIs…
|
||||
Possibilities are endless!
|
||||
The aim of Himalaya is to extract the email logic into a simple (yet solid) CLI API that can be used directly from the terminal, from scripts, from UIs… Possibilities are endless!
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -28,9 +21,7 @@ Possibilities are endless!
|
|||
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
|
||||
|
||||
|
@ -59,9 +50,7 @@ smtp-login = "your.email@gmail.com"
|
|||
smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
|
||||
```
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file) for
|
||||
all the options.*
|
||||
*See the [wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file) for all the options.*
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -76,8 +65,7 @@ all the options.*
|
|||
- 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
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, Shell, SubCommand};
|
||||
use log::debug;
|
||||
use log::{debug, info};
|
||||
|
||||
type OptionShell<'a> = Option<&'a str>;
|
||||
|
||||
|
@ -16,10 +16,12 @@ pub enum Command<'a> {
|
|||
|
||||
/// Completion command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
info!("entering completion command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("completion") {
|
||||
debug!("completion command matched");
|
||||
info!("completion command matched");
|
||||
let shell = m.value_of("shell");
|
||||
debug!("shell: `{:?}`", shell);
|
||||
debug!("shell: {:?}", shell);
|
||||
return Ok(Some(Command::Generate(shell)));
|
||||
};
|
||||
|
||||
|
|
|
@ -4,13 +4,18 @@
|
|||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{App, Shell};
|
||||
use log::{debug, info};
|
||||
use std::{io, str::FromStr};
|
||||
|
||||
/// Generate completion script from the given [`clap::App`] for the given shell slice.
|
||||
/// Generates completion script from the given [`clap::App`] for the given shell slice.
|
||||
pub fn generate<'a>(mut app: App<'a, 'a>, shell: Option<&'a str>) -> Result<()> {
|
||||
info!("entering generate completion handler");
|
||||
|
||||
let shell = Shell::from_str(shell.unwrap_or_default())
|
||||
.map_err(|err| anyhow!(err))
|
||||
.context("cannot parse shell")?;
|
||||
debug!("shell: {}", shell);
|
||||
|
||||
app.gen_completions_to("himalaya", shell, &mut io::stdout());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ pub struct 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,
|
||||
|
@ -43,6 +45,9 @@ pub struct Account {
|
|||
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 {
|
||||
|
@ -77,6 +82,30 @@ impl Account {
|
|||
|
||||
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 {
|
||||
|
@ -162,6 +191,12 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
|
|||
.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()
|
||||
|
@ -184,9 +219,12 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
|
|||
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);
|
||||
trace!("account: {:?}", account);
|
||||
Ok(account)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ pub struct Config {
|
|||
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>>,
|
||||
|
||||
|
@ -56,6 +58,8 @@ pub struct ConfigAccountEntry {
|
|||
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,
|
||||
|
@ -73,6 +77,9 @@ pub struct ConfigAccountEntry {
|
|||
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 {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{App, ArgMatches};
|
||||
use log::debug;
|
||||
use log::{debug, info};
|
||||
|
||||
type Keepalive = u64;
|
||||
|
||||
|
@ -19,15 +19,17 @@ pub enum Command {
|
|||
|
||||
/// IMAP command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Command>> {
|
||||
info!("entering imap command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("notify") {
|
||||
debug!("notify command matched");
|
||||
info!("notify command matched");
|
||||
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Command::Notify(keepalive)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("watch") {
|
||||
debug!("watch command matched");
|
||||
info!("watch command matched");
|
||||
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Command::Watch(keepalive)));
|
||||
|
|
|
@ -9,16 +9,15 @@ use crate::{
|
|||
domain::imap::ImapServiceInterface,
|
||||
};
|
||||
|
||||
/// Notify handler.
|
||||
pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
|
||||
keepalive: u64,
|
||||
config: &Config,
|
||||
account: &Account,
|
||||
imap: &mut ImapService,
|
||||
) -> Result<()> {
|
||||
imap.notify(config, keepalive)
|
||||
imap.notify(config, account, keepalive)
|
||||
}
|
||||
|
||||
/// Watch handler.
|
||||
pub fn watch<'a, ImapService: ImapServiceInterface<'a>>(
|
||||
keepalive: u64,
|
||||
account: &Account,
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
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 std::{collections::HashSet, convert::TryFrom, net::TcpStream, thread};
|
||||
|
||||
use crate::{
|
||||
config::{Account, Config},
|
||||
|
@ -21,7 +16,7 @@ use crate::{
|
|||
type ImapSession = imap::Session<TlsStream<TcpStream>>;
|
||||
|
||||
pub trait ImapServiceInterface<'a> {
|
||||
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>;
|
||||
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>;
|
||||
|
@ -31,9 +26,9 @@ pub trait ImapServiceInterface<'a> {
|
|||
page_size: &usize,
|
||||
page: &usize,
|
||||
) -> Result<Envelopes>;
|
||||
fn find_msg(&mut self, seq: &str) -> Result<Msg>;
|
||||
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, msg: Msg) -> Result<()>;
|
||||
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<()>;
|
||||
|
@ -98,10 +93,10 @@ impl<'a> ImapService<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn search_new_msgs(&mut self) -> Result<Vec<u32>> {
|
||||
fn search_new_msgs(&mut self, account: &Account) -> Result<Vec<u32>> {
|
||||
let uids: Vec<u32> = self
|
||||
.sess()?
|
||||
.uid_search("NEW")
|
||||
.uid_search(&account.notify_query)
|
||||
.context("cannot search new messages")?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
@ -197,11 +192,11 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
|||
}
|
||||
|
||||
/// Find a message by sequence number.
|
||||
fn find_msg(&mut self, seq: &str) -> Result<Msg> {
|
||||
fn find_msg(&mut self, account: &Account, seq: &str) -> Result<Msg> {
|
||||
let mbox = self.mbox.to_owned();
|
||||
self.sess()?
|
||||
.select(&mbox.name)
|
||||
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
|
||||
.context(format!("cannot select mailbox {}", self.mbox.name))?;
|
||||
let fetches = self
|
||||
.sess()?
|
||||
.fetch(seq, "(ENVELOPE FLAGS INTERNALDATE BODY[])")
|
||||
|
@ -210,7 +205,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
|||
.first()
|
||||
.ok_or_else(|| anyhow!(r#"cannot find message "{}"#, seq))?;
|
||||
|
||||
Msg::try_from(fetch)
|
||||
Msg::try_from((account, fetch))
|
||||
}
|
||||
|
||||
fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>> {
|
||||
|
@ -238,8 +233,8 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn append_msg(&mut self, mbox: &Mbox, msg: Msg) -> Result<()> {
|
||||
let msg_raw: Vec<u8> = (&msg).try_into()?;
|
||||
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)
|
||||
|
@ -248,7 +243,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()> {
|
||||
fn notify(&mut self, config: &Config, account: &Account, keepalive: u64) -> Result<()> {
|
||||
debug!("notify");
|
||||
|
||||
let mbox = self.mbox.to_owned();
|
||||
|
@ -260,7 +255,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
|||
|
||||
debug!("init messages hashset");
|
||||
let mut msgs_set: HashSet<u32> = self
|
||||
.search_new_msgs()?
|
||||
.search_new_msgs(account)?
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
|
@ -281,7 +276,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
|
|||
.context("cannot start the idle mode")?;
|
||||
|
||||
let uids: Vec<u32> = self
|
||||
.search_new_msgs()?
|
||||
.search_new_msgs(account)?
|
||||
.into_iter()
|
||||
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
|
||||
.collect();
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap;
|
||||
use log::trace;
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table_arg;
|
||||
|
||||
|
@ -20,12 +20,14 @@ pub enum Cmd {
|
|||
|
||||
/// Defines the mailbox command matcher.
|
||||
pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
|
||||
info!("entering mailbox command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("mailboxes") {
|
||||
trace!("mailboxes subcommand matched");
|
||||
info!("mailboxes command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
trace!(r#"max table width: "{:?}""#, max_table_width);
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
return Ok(Some(Cmd::List(max_table_width)));
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
//! This module gathers all mailbox actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use log::trace;
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
domain::ImapServiceInterface,
|
||||
|
@ -16,8 +16,9 @@ pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
|||
printer: &mut Printer,
|
||||
imap: &'a mut ImapService,
|
||||
) -> Result<()> {
|
||||
info!("entering list mailbox handler");
|
||||
let mboxes = imap.fetch_mboxes()?;
|
||||
trace!("mailboxes: {:#?}", mboxes);
|
||||
trace!("mailboxes: {:?}", mboxes);
|
||||
printer.print_table(mboxes, PrintTableOpts { max_width })
|
||||
}
|
||||
|
||||
|
@ -114,7 +115,7 @@ mod tests {
|
|||
]))
|
||||
}
|
||||
|
||||
fn notify(&mut self, _: &Config, _: u64) -> Result<()> {
|
||||
fn notify(&mut self, _: &Config, _: &Account, _: u64) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn watch(&mut self, _: &Account, _: u64) -> Result<()> {
|
||||
|
@ -126,13 +127,13 @@ mod tests {
|
|||
fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn find_msg(&mut self, _: &str) -> Result<Msg> {
|
||||
fn find_msg(&mut self, _: &Account, _: &str) -> Result<Msg> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn find_raw_msg(&mut self, _: &str) -> Result<Vec<u8>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn append_msg(&mut self, _: &Mbox, _: Msg) -> Result<()> {
|
||||
fn append_msg(&mut self, _: &Mbox, _: &Account, _: Msg) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn append_raw_msg_with_flags(&mut self, _: &Mbox, _: &[u8], _: Flags) -> Result<()> {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, trace};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::domain::msg::msg_arg;
|
||||
|
||||
|
@ -24,30 +24,32 @@ pub enum Command<'a> {
|
|||
|
||||
/// Defines the flag command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
info!("entering message flag command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("add") {
|
||||
debug!("add subcommand matched");
|
||||
info!("add subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
trace!(r#"seq range: "{}""#, seq_range);
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
|
||||
trace!(r#"flags: "{:?}""#, flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Command::Add(seq_range, flags)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("set") {
|
||||
debug!("set subcommand matched");
|
||||
info!("set subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
trace!(r#"seq range: "{}""#, seq_range);
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
|
||||
trace!(r#"flags: "{:?}""#, flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Command::Set(seq_range, flags)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("remove") {
|
||||
trace!("remove subcommand matched");
|
||||
info!("remove subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
trace!(r#"seq range: "{}""#, seq_range);
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
|
||||
trace!(r#"flags: "{:?}""#, flags);
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Command::Remove(seq_range, flags)));
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, trace};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
|
@ -25,21 +25,22 @@ type RawMsg<'a> = &'a str;
|
|||
type Query = String;
|
||||
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||
type MaxTableWidth = Option<usize>;
|
||||
type Encrypt = bool;
|
||||
|
||||
/// Message commands.
|
||||
pub enum Command<'a> {
|
||||
Attachments(Seq<'a>),
|
||||
Copy(Seq<'a>, Mbox<'a>),
|
||||
Delete(Seq<'a>),
|
||||
Forward(Seq<'a>, AttachmentPaths<'a>),
|
||||
Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt),
|
||||
List(MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Seq<'a>, Mbox<'a>),
|
||||
Read(Seq<'a>, TextMime<'a>, Raw),
|
||||
Reply(Seq<'a>, All, AttachmentPaths<'a>),
|
||||
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
|
||||
Save(RawMsg<'a>),
|
||||
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawMsg<'a>),
|
||||
Write(AttachmentPaths<'a>),
|
||||
Write(AttachmentPaths<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag_arg::Command<'a>>),
|
||||
Tpl(Option<tpl_arg::Command<'a>>),
|
||||
|
@ -47,46 +48,50 @@ pub enum Command<'a> {
|
|||
|
||||
/// Message command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
info!("entering message command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("attachments") {
|
||||
debug!("attachments command matched");
|
||||
info!("attachments command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
return Ok(Some(Command::Attachments(seq)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("copy") {
|
||||
debug!("copy command matched");
|
||||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
trace!(r#"target mailbox: "{:?}""#, mbox);
|
||||
debug!(r#"target mailbox: "{:?}""#, mbox);
|
||||
return Ok(Some(Command::Copy(seq, mbox)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("delete") {
|
||||
debug!("copy command matched");
|
||||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
return Ok(Some(Command::Delete(seq)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("forward") {
|
||||
debug!("forward command matched");
|
||||
info!("forward command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", paths);
|
||||
return Ok(Some(Command::Forward(seq, paths)));
|
||||
debug!("attachments paths: {:?}", paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Command::Forward(seq, paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("list") {
|
||||
debug!("list command matched");
|
||||
info!("list command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
trace!(r#"max table width: "{:?}""#, max_table_width);
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
trace!(r#"page size: "{:?}""#, page_size);
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap_or("1")
|
||||
|
@ -94,56 +99,59 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
trace!(r#"page: "{:?}""#, page);
|
||||
debug!("page: {}", page);
|
||||
return Ok(Some(Command::List(max_table_width, page_size, page)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("move") {
|
||||
debug!("move command matched");
|
||||
info!("move command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
trace!(r#"target mailbox: "{:?}""#, mbox);
|
||||
debug!("target mailbox: {:?}", mbox);
|
||||
return Ok(Some(Command::Move(seq, mbox)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("read") {
|
||||
debug!("read command matched");
|
||||
info!("read command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
let mime = m.value_of("mime-type").unwrap();
|
||||
trace!("text mime: {}", mime);
|
||||
debug!("text mime: {}", mime);
|
||||
let raw = m.is_present("raw");
|
||||
trace!("raw: {}", raw);
|
||||
debug!("raw: {}", raw);
|
||||
return Ok(Some(Command::Read(seq, mime, raw)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("reply") {
|
||||
debug!("reply command matched");
|
||||
info!("reply command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
trace!("seq: {}", seq);
|
||||
debug!("seq: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
trace!("reply all: {}", all);
|
||||
debug!("reply all: {}", all);
|
||||
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:#?}", paths);
|
||||
return Ok(Some(Command::Reply(seq, all, paths)));
|
||||
debug!("attachments paths: {:?}", paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
|
||||
return Ok(Some(Command::Reply(seq, all, paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("save") {
|
||||
debug!("save command matched");
|
||||
info!("save command matched");
|
||||
let msg = m.value_of("message").unwrap_or_default();
|
||||
trace!("message: {}", msg);
|
||||
return Ok(Some(Command::Save(msg)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("search") {
|
||||
debug!("search command matched");
|
||||
info!("search command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
trace!(r#"max table width: "{:?}""#, max_table_width);
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
trace!(r#"page size: "{:?}""#, page_size);
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap()
|
||||
|
@ -151,7 +159,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
trace!(r#"page: "{:?}""#, page);
|
||||
debug!("page: {}", page);
|
||||
let query = m
|
||||
.values_of("query")
|
||||
.unwrap_or_default()
|
||||
|
@ -176,7 +184,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
})
|
||||
.1
|
||||
.join(" ");
|
||||
trace!(r#"query: "{:?}""#, query);
|
||||
debug!("query: {}", query);
|
||||
return Ok(Some(Command::Search(
|
||||
query,
|
||||
max_table_width,
|
||||
|
@ -186,17 +194,19 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("send") {
|
||||
debug!("send command matched");
|
||||
info!("send command matched");
|
||||
let msg = m.value_of("message").unwrap_or_default();
|
||||
trace!("message: {}", msg);
|
||||
return Ok(Some(Command::Send(msg)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("write") {
|
||||
debug!("write command matched");
|
||||
info!("write command matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
return Ok(Some(Command::Write(attachment_paths)));
|
||||
debug!("attachments paths: {:?}", attachment_paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Command::Write(attachment_paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("template") {
|
||||
|
@ -207,7 +217,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
return Ok(Some(Command::Flag(flag_arg::matches(m)?)));
|
||||
}
|
||||
|
||||
debug!("default list command matched");
|
||||
info!("default list command matched");
|
||||
Ok(Some(Command::List(None, None, 0)))
|
||||
}
|
||||
|
||||
|
@ -265,6 +275,14 @@ pub fn attachment_arg<'a>() -> Arg<'a, 'a> {
|
|||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Message encrypt argument.
|
||||
pub fn encrypt_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("encrypt")
|
||||
.help("Encrypts the message")
|
||||
.short("e")
|
||||
.long("encrypt")
|
||||
}
|
||||
|
||||
/// Message subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![
|
||||
|
@ -297,7 +315,8 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
),
|
||||
SubCommand::with_name("write")
|
||||
.about("Writes a new message")
|
||||
.arg(attachment_arg()),
|
||||
.arg(attachment_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("send")
|
||||
.about("Sends a raw message")
|
||||
.arg(Arg::with_name("message").raw(true).last(true)),
|
||||
|
@ -327,12 +346,14 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
.about("Answers to a message")
|
||||
.arg(seq_arg())
|
||||
.arg(reply_all_arg())
|
||||
.arg(attachment_arg()),
|
||||
.arg(attachment_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "f"])
|
||||
.about("Forwards a message")
|
||||
.arg(seq_arg())
|
||||
.arg(attachment_arg()),
|
||||
.arg(attachment_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("copy")
|
||||
.aliases(&["cp", "c"])
|
||||
.about("Copies a message to the targetted mailbox")
|
||||
|
|
|
@ -3,16 +3,19 @@ use anyhow::{anyhow, Context, Error, Result};
|
|||
use chrono::{DateTime, FixedOffset};
|
||||
use html_escape;
|
||||
use imap::types::Flag;
|
||||
use lettre::message::{Attachment, MultiPart, SinglePart};
|
||||
use log::trace;
|
||||
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
|
||||
use log::{debug, info, trace};
|
||||
use regex::Regex;
|
||||
use rfc2047_decoder;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::{TryFrom, TryInto},
|
||||
env::temp_dir,
|
||||
fmt::Debug,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
config::{Account, DEFAULT_SIG_DELIM},
|
||||
|
@ -58,6 +61,8 @@ pub struct Msg {
|
|||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
|
||||
pub date: Option<DateTime<FixedOffset>>,
|
||||
pub parts: Parts,
|
||||
|
||||
pub encrypt: bool,
|
||||
}
|
||||
|
||||
impl Msg {
|
||||
|
@ -71,7 +76,7 @@ impl Msg {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Fold string body from all plain text parts into a single string body. If no plain text
|
||||
/// Folds string body from all plain text parts into a single string body. If no plain text
|
||||
/// parts are found, HTML parts are used instead. The result is sanitized (all HTML markup is
|
||||
/// removed).
|
||||
pub fn fold_text_plain_parts(&self) -> String {
|
||||
|
@ -334,6 +339,8 @@ impl Msg {
|
|||
imap: &mut ImapService,
|
||||
smtp: &mut SmtpService,
|
||||
) -> Result<()> {
|
||||
info!("start editing with editor");
|
||||
|
||||
let draft = msg_utils::local_draft_path();
|
||||
if draft.exists() {
|
||||
loop {
|
||||
|
@ -364,7 +371,7 @@ impl Msg {
|
|||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
let mbox = Mbox::new(&account.sent_folder);
|
||||
let sent_msg = smtp.send_msg(&self)?;
|
||||
let sent_msg = smtp.send_msg(account, &self)?;
|
||||
let flags = Flags::try_from(vec![Flag::Seen])?;
|
||||
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
|
||||
msg_utils::remove_local_draft()?;
|
||||
|
@ -405,6 +412,11 @@ impl Msg {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn encrypt(mut self, encrypt: bool) -> Self {
|
||||
self.encrypt = encrypt;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result<Self> {
|
||||
for path in attachments_paths {
|
||||
let path = shellexpand::full(path)
|
||||
|
@ -547,99 +559,73 @@ impl Msg {
|
|||
|
||||
tpl.push('\n');
|
||||
|
||||
trace!("template: {:#?}", tpl);
|
||||
trace!("template: {:?}", tpl);
|
||||
tpl
|
||||
}
|
||||
|
||||
pub fn from_tpl(tpl: &str) -> Result<Self> {
|
||||
info!("begin: building message from template");
|
||||
trace!("template: {:?}", tpl);
|
||||
|
||||
let mut msg = Msg::default();
|
||||
let parsed_msg = mailparse::parse_mail(tpl.as_bytes()).context("cannot parse template")?;
|
||||
|
||||
let parsed_msg =
|
||||
mailparse::parse_mail(tpl.as_bytes()).context("cannot parse message from 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())?;
|
||||
.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.to_owned()),
|
||||
"from" => {
|
||||
msg.from = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"to" => {
|
||||
msg.to = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"reply-to" => {
|
||||
msg.reply_to = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"in-reply-to" => msg.in_reply_to = Some(val.to_owned()),
|
||||
"cc" => {
|
||||
msg.cc = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"bcc" => {
|
||||
msg.bcc = Some(
|
||||
val.split(',')
|
||||
.filter_map(|addr| addr.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
"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))?
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let content = parsed_msg
|
||||
debug!("parsing body");
|
||||
let body = parsed_msg
|
||||
.get_body_raw()
|
||||
.context("cannot get body from parsed message")?;
|
||||
let content = String::from_utf8(content).context("cannot decode body from utf-8")?;
|
||||
msg.parts.push(Part::TextPlain(TextPlainPart { content }));
|
||||
.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");
|
||||
trace!("message: {:?}", msg);
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<lettre::address::Envelope> for Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||
let from: Option<lettre::Address> = self
|
||||
.from
|
||||
.and_then(|addrs| addrs.into_iter().next())
|
||||
.map(|addr| addr.email);
|
||||
let to = self
|
||||
.to
|
||||
.map(|addrs| addrs.into_iter().map(|addr| addr.email).collect())
|
||||
.unwrap_or_default();
|
||||
let envelope =
|
||||
lettre::address::Envelope::new(from, to).context("cannot create envelope")?;
|
||||
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<lettre::Message> for &Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<lettre::Message> {
|
||||
pub fn into_sendable_msg(&self, account: &Account) -> Result<lettre::Message> {
|
||||
let mut msg_builder = lettre::Message::builder()
|
||||
.message_id(self.message_id.to_owned())
|
||||
.subject(self.subject.to_owned());
|
||||
|
@ -678,17 +664,42 @@ impl TryInto<lettre::Message> for &Msg {
|
|||
.fold(msg_builder, |builder, addr| builder.bcc(addr.to_owned()))
|
||||
};
|
||||
|
||||
let mut multipart =
|
||||
MultiPart::mixed().singlepart(SinglePart::plain(self.fold_text_plain_parts()));
|
||||
let mut multipart = {
|
||||
let mut multipart =
|
||||
MultiPart::mixed().singlepart(SinglePart::plain(self.fold_text_plain_parts()));
|
||||
for part in self.attachments() {
|
||||
multipart = multipart.singlepart(Attachment::new(part.filename.clone()).body(
|
||||
part.content,
|
||||
part.mime.parse().context(format!(
|
||||
"cannot parse content type of attachment {}",
|
||||
part.filename
|
||||
))?,
|
||||
))
|
||||
}
|
||||
multipart
|
||||
};
|
||||
|
||||
for part in self.attachments() {
|
||||
let filename = part.filename;
|
||||
let content = part.content;
|
||||
let mime = part.mime.parse().context(format!(
|
||||
r#"cannot parse content type of attachment "{}""#,
|
||||
filename
|
||||
))?;
|
||||
multipart = multipart.singlepart(Attachment::new(filename).body(content, mime))
|
||||
if self.encrypt {
|
||||
let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string());
|
||||
fs::write(multipart_buffer.clone(), multipart.formatted())?;
|
||||
let encrypted_multipart = account
|
||||
.pgp_encrypt_file(
|
||||
&self.to.as_ref().unwrap().first().unwrap().email.to_string(),
|
||||
multipart_buffer.clone(),
|
||||
)?
|
||||
.ok_or_else(|| anyhow!("cannot find pgp encrypt command in config"))?;
|
||||
trace!("encrypted multipart: {:#?}", encrypted_multipart);
|
||||
multipart = MultiPart::encrypted(String::from("application/pgp-encrypted"))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::parse("application/pgp-encrypted").unwrap())
|
||||
.body(String::from("Version: 1")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::parse("application/octet-stream").unwrap())
|
||||
.body(encrypted_multipart),
|
||||
)
|
||||
}
|
||||
|
||||
msg_builder
|
||||
|
@ -697,19 +708,29 @@ impl TryInto<lettre::Message> for &Msg {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryInto<Vec<u8>> for &Msg {
|
||||
impl TryInto<lettre::address::Envelope> for Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<Vec<u8>> {
|
||||
let msg: lettre::Message = self.try_into()?;
|
||||
Ok(msg.formatted())
|
||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||
let from: Option<lettre::Address> = self
|
||||
.from
|
||||
.and_then(|addrs| addrs.into_iter().next())
|
||||
.map(|addr| addr.email);
|
||||
let to = self
|
||||
.to
|
||||
.map(|addrs| addrs.into_iter().map(|addr| addr.email).collect())
|
||||
.unwrap_or_default();
|
||||
let envelope =
|
||||
lettre::address::Envelope::new(from, to).context("cannot create envelope")?;
|
||||
|
||||
Ok(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
||||
impl<'a> TryFrom<(&'a Account, &'a imap::types::Fetch)> for Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fetch: &'a imap::types::Fetch) -> Result<Msg> {
|
||||
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))?;
|
||||
|
@ -737,28 +758,28 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
|||
.sender
|
||||
.as_deref()
|
||||
.or_else(|| envelope.from.as_deref())
|
||||
.map(parse_addrs)
|
||||
.map(to_addrs)
|
||||
{
|
||||
Some(addrs) => Some(addrs?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Get the "Reply-To" address(es)
|
||||
let reply_to = parse_some_addrs(&envelope.reply_to).context(format!(
|
||||
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 = parse_some_addrs(&envelope.to)
|
||||
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 = parse_some_addrs(&envelope.cc)
|
||||
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 = parse_some_addrs(&envelope.bcc)
|
||||
let bcc = to_some_addrs(&envelope.bcc)
|
||||
.context(format!(r#"cannot parse "bcc" address of message {}"#, id))?;
|
||||
|
||||
// Get the "In-Reply-To" message identifier
|
||||
|
@ -785,14 +806,12 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
|||
let date = fetch.internal_date();
|
||||
|
||||
// Get all parts
|
||||
let parts = Parts::from(
|
||||
&mailparse::parse_mail(
|
||||
fetch
|
||||
.body()
|
||||
.ok_or_else(|| anyhow!("cannot get body of message {}", id))?,
|
||||
)
|
||||
.context(format!("cannot parse body of message {}", id))?,
|
||||
);
|
||||
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,
|
||||
|
@ -807,11 +826,29 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
|
|||
message_id,
|
||||
date,
|
||||
parts,
|
||||
encrypt: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
|
||||
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()
|
||||
|
@ -839,17 +876,16 @@ pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
|
|||
Ok(Addr::new(name, lettre::Address::new(mbox, host)?))
|
||||
}
|
||||
|
||||
pub fn parse_addrs(addrs: &[imap_proto::Address]) -> Result<Vec<Addr>> {
|
||||
pub fn to_addrs(addrs: &[imap_proto::Address]) -> Result<Vec<Addr>> {
|
||||
let mut parsed_addrs = vec![];
|
||||
for addr in addrs {
|
||||
parsed_addrs
|
||||
.push(parse_addr(addr).context(format!(r#"cannot parse address "{:?}""#, addr))?);
|
||||
parsed_addrs.push(to_addr(addr).context(format!(r#"cannot parse address "{:?}""#, addr))?);
|
||||
}
|
||||
Ok(parsed_addrs)
|
||||
}
|
||||
|
||||
pub fn parse_some_addrs(addrs: &Option<Vec<imap_proto::Address>>) -> Result<Option<Vec<Addr>>> {
|
||||
Ok(match addrs.as_deref().map(parse_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,
|
||||
})
|
||||
|
|
|
@ -33,7 +33,7 @@ pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterfac
|
|||
printer: &mut Printer,
|
||||
imap: &mut ImapService,
|
||||
) -> Result<()> {
|
||||
let attachments = imap.find_msg(seq)?.attachments();
|
||||
let attachments = imap.find_msg(account, seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
debug!(
|
||||
r#"{} attachment(s) found for message "{}""#,
|
||||
|
@ -91,14 +91,16 @@ pub fn forward<
|
|||
>(
|
||||
seq: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
account: &Account,
|
||||
printer: &mut Printer,
|
||||
imap: &mut ImapService,
|
||||
smtp: &mut SmtpService,
|
||||
) -> Result<()> {
|
||||
imap.find_msg(seq)?
|
||||
imap.find_msg(account, seq)?
|
||||
.into_forward(account)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(account, printer, imap, smtp)
|
||||
}
|
||||
|
||||
|
@ -119,7 +121,7 @@ pub fn list<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
|||
printer.print_table(msgs, PrintTableOpts { max_width })
|
||||
}
|
||||
|
||||
/// Parse and edit a message from a [mailto] URL string.
|
||||
/// Parses and edits a message from a [mailto] URL string.
|
||||
///
|
||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
pub fn mailto<
|
||||
|
@ -134,6 +136,8 @@ pub fn mailto<
|
|||
imap: &mut ImapService,
|
||||
smtp: &mut SmtpService,
|
||||
) -> Result<()> {
|
||||
info!("entering mailto command handler");
|
||||
|
||||
let to: Vec<lettre::message::Mailbox> = url
|
||||
.path()
|
||||
.split(';')
|
||||
|
@ -173,6 +177,7 @@ pub fn mailto<
|
|||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
msg.edit_with_editor(account, printer, imap, smtp)
|
||||
}
|
||||
|
@ -208,6 +213,7 @@ 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<()> {
|
||||
|
@ -215,7 +221,7 @@ pub fn read<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
|||
// 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(seq)?.fold_text_parts(text_mime)
|
||||
imap.find_msg(account, seq)?.fold_text_parts(text_mime)
|
||||
};
|
||||
|
||||
printer.print(msg)
|
||||
|
@ -231,14 +237,16 @@ pub fn reply<
|
|||
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(seq)?
|
||||
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)
|
||||
|
@ -344,6 +352,7 @@ pub fn write<
|
|||
SmtpService: SmtpServiceInterface,
|
||||
>(
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
account: &Account,
|
||||
printer: &mut Printer,
|
||||
imap: &mut ImapService,
|
||||
|
@ -351,5 +360,6 @@ pub fn write<
|
|||
) -> Result<()> {
|
||||
Msg::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(account, printer, imap, smtp)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use mailparse::MailHeaderMap;
|
||||
use serde::Serialize;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{
|
||||
env, fs,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::Account;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextPlainPart {
|
||||
|
@ -43,9 +50,13 @@ impl Parts {
|
|||
self.push(Part::TextPlain(part));
|
||||
}
|
||||
|
||||
pub fn replace_text_html_parts_with(&mut self, part: TextHtmlPart) {
|
||||
self.retain(|part| !matches!(part, Part::TextHtml(_)));
|
||||
self.push(Part::TextHtml(part));
|
||||
pub fn from_parsed_mail<'a>(
|
||||
account: &'a Account,
|
||||
part: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Result<Self> {
|
||||
let mut parts = vec![];
|
||||
build_parts_map_rec(account, part, &mut parts)?;
|
||||
Ok(Self(parts))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,25 +74,21 @@ impl DerefMut for Parts {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mailparse::ParsedMail<'a>> for Parts {
|
||||
fn from(part: &'a mailparse::ParsedMail<'a>) -> Self {
|
||||
let mut parts = vec![];
|
||||
build_parts_map_rec(part, &mut parts);
|
||||
Self(parts)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
|
||||
if part.subparts.is_empty() {
|
||||
let content_disp = part.get_content_disposition();
|
||||
match content_disp.disposition {
|
||||
fn build_parts_map_rec(
|
||||
account: &Account,
|
||||
parsed_mail: &mailparse::ParsedMail,
|
||||
parts: &mut Vec<Part>,
|
||||
) -> Result<()> {
|
||||
if parsed_mail.subparts.is_empty() {
|
||||
let cdisp = parsed_mail.get_content_disposition();
|
||||
match cdisp.disposition {
|
||||
mailparse::DispositionType::Attachment => {
|
||||
let filename = content_disp
|
||||
let filename = cdisp
|
||||
.params
|
||||
.get("filename")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| String::from("noname"));
|
||||
let content = part.get_body_raw().unwrap_or_default();
|
||||
let content = parsed_mail.get_body_raw().unwrap_or_default();
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
parts.push(Part::Binary(BinaryPart {
|
||||
filename,
|
||||
|
@ -91,8 +98,8 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
|
|||
}
|
||||
// TODO: manage other use cases
|
||||
_ => {
|
||||
if let Some(ctype) = part.get_headers().get_first_value("content-type") {
|
||||
let content = part.get_body().unwrap_or_default();
|
||||
if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") {
|
||||
let content = parsed_mail.get_body().unwrap_or_default();
|
||||
if ctype.starts_with("text/plain") {
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else if ctype.starts_with("text/html") {
|
||||
|
@ -102,8 +109,38 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
|
|||
}
|
||||
};
|
||||
} else {
|
||||
part.subparts
|
||||
.iter()
|
||||
.for_each(|part| build_parts_map_rec(part, parts));
|
||||
let ctype = parsed_mail
|
||||
.get_headers()
|
||||
.get_first_value("content-type")
|
||||
.ok_or_else(|| anyhow!("cannot get content type of multipart"))?;
|
||||
if ctype.starts_with("multipart/encrypted") {
|
||||
let decrypted_part = parsed_mail
|
||||
.subparts
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("cannot find encrypted part of multipart"))
|
||||
.and_then(|part| decrypt_part(account, part))
|
||||
.context("cannot decrypt part of multipart")?;
|
||||
let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes())
|
||||
.context("cannot parse decrypted part of multipart")?;
|
||||
build_parts_map_rec(account, &parsed_mail, parts)?;
|
||||
} else {
|
||||
for part in parsed_mail.subparts.iter() {
|
||||
build_parts_map_rec(account, part, parts)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> Result<String> {
|
||||
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
|
||||
let msg_body = msg
|
||||
.get_body()
|
||||
.context("cannot get body from encrypted part")?;
|
||||
fs::write(msg_path.clone(), &msg_body)
|
||||
.context(format!("cannot write encrypted part to temporary file"))?;
|
||||
account
|
||||
.pgp_decrypt_file(msg_path.clone())?
|
||||
.ok_or_else(|| anyhow!("cannot find pgp decrypt command in config"))
|
||||
}
|
||||
|
|
|
@ -51,15 +51,17 @@ pub enum Command<'a> {
|
|||
|
||||
/// Message template command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
info!("entering message template command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("new") {
|
||||
info!("new command matched");
|
||||
info!("new subcommand matched");
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Command::New(tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("reply") {
|
||||
info!("reply command matched");
|
||||
info!("reply subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
|
@ -70,7 +72,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("forward") {
|
||||
info!("forward command matched");
|
||||
info!("forward subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let tpl = TplOverride::from(m);
|
||||
|
@ -79,7 +81,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("save") {
|
||||
info!("save command matched");
|
||||
info!("save subcommand matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
let tpl = m.value_of("template").unwrap_or_default();
|
||||
|
@ -88,7 +90,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
|||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("send") {
|
||||
info!("send command matched");
|
||||
info!("send subcommand matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
let tpl = m.value_of("template").unwrap_or_default();
|
||||
|
|
|
@ -6,7 +6,7 @@ use anyhow::Result;
|
|||
use atty::Stream;
|
||||
use imap::types::Flag;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
convert::TryFrom,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
|
||||
|
@ -40,7 +40,7 @@ pub fn reply<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>
|
|||
imap: &'a mut ImapService,
|
||||
) -> Result<()> {
|
||||
let tpl = imap
|
||||
.find_msg(seq)?
|
||||
.find_msg(account, seq)?
|
||||
.into_reply(all, account)?
|
||||
.to_tpl(opts, account);
|
||||
printer.print(tpl)
|
||||
|
@ -55,7 +55,7 @@ pub fn forward<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a
|
|||
imap: &'a mut ImapService,
|
||||
) -> Result<()> {
|
||||
let tpl = imap
|
||||
.find_msg(seq)?
|
||||
.find_msg(account, seq)?
|
||||
.into_forward(account)?
|
||||
.to_tpl(opts, account);
|
||||
printer.print(tpl)
|
||||
|
@ -64,6 +64,7 @@ pub fn forward<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a
|
|||
/// 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,
|
||||
|
@ -80,7 +81,7 @@ pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
|
|||
.join("\n")
|
||||
};
|
||||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let raw_msg: Vec<u8> = TryInto::try_into(&msg)?;
|
||||
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")
|
||||
|
@ -94,6 +95,7 @@ pub fn send<
|
|||
SmtpService: SmtpServiceInterface,
|
||||
>(
|
||||
mbox: &Mbox,
|
||||
account: &Account,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut Printer,
|
||||
|
@ -111,7 +113,7 @@ pub fn send<
|
|||
.join("\n")
|
||||
};
|
||||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let sent_msg = smtp.send_msg(&msg)?;
|
||||
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")
|
||||
|
|
|
@ -8,12 +8,11 @@ use lettre::{
|
|||
Transport,
|
||||
};
|
||||
use log::debug;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::{config::Account, domain::msg::Msg};
|
||||
|
||||
pub trait SmtpServiceInterface {
|
||||
fn send_msg(&mut self, msg: &Msg) -> Result<lettre::Message>;
|
||||
fn send_msg(&mut self, account: &Account, msg: &Msg) -> Result<lettre::Message>;
|
||||
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
|
||||
}
|
||||
|
||||
|
@ -57,9 +56,9 @@ impl<'a> SmtpService<'a> {
|
|||
}
|
||||
|
||||
impl<'a> SmtpServiceInterface for SmtpService<'a> {
|
||||
fn send_msg(&mut self, msg: &Msg) -> Result<lettre::Message> {
|
||||
fn send_msg(&mut self, account: &Account, msg: &Msg) -> Result<lettre::Message> {
|
||||
debug!("sending message…");
|
||||
let sendable_msg: lettre::Message = msg.try_into()?;
|
||||
let sendable_msg = msg.into_sendable_msg(account)?;
|
||||
self.transport()?.send(&sendable_msg)?;
|
||||
Ok(sendable_msg)
|
||||
}
|
||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -36,10 +36,8 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
|
|||
|
||||
#[allow(clippy::single_match)]
|
||||
fn main() -> Result<()> {
|
||||
// Init env logger
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"),
|
||||
);
|
||||
let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
|
||||
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
|
||||
|
||||
// Check mailto command BEFORE app initialization.
|
||||
let raw_args: Vec<String> = env::args().collect();
|
||||
|
@ -77,7 +75,7 @@ fn main() -> Result<()> {
|
|||
// Check IMAP commands.
|
||||
match imap_arg::matches(&m)? {
|
||||
Some(imap_arg::Command::Notify(keepalive)) => {
|
||||
return imap_handler::notify(keepalive, &config, &mut imap);
|
||||
return imap_handler::notify(keepalive, &config, &account, &mut imap);
|
||||
}
|
||||
Some(imap_arg::Command::Watch(keepalive)) => {
|
||||
return imap_handler::watch(keepalive, &account, &mut imap);
|
||||
|
@ -104,8 +102,16 @@ fn main() -> Result<()> {
|
|||
Some(msg_arg::Command::Delete(seq)) => {
|
||||
return msg_handler::delete(seq, &mut printer, &mut imap);
|
||||
}
|
||||
Some(msg_arg::Command::Forward(seq, atts)) => {
|
||||
return msg_handler::forward(seq, atts, &account, &mut printer, &mut imap, &mut smtp);
|
||||
Some(msg_arg::Command::Forward(seq, attachment_paths, encrypt)) => {
|
||||
return msg_handler::forward(
|
||||
seq,
|
||||
attachment_paths,
|
||||
encrypt,
|
||||
&account,
|
||||
&mut printer,
|
||||
&mut imap,
|
||||
&mut smtp,
|
||||
);
|
||||
}
|
||||
Some(msg_arg::Command::List(max_width, page_size, page)) => {
|
||||
return msg_handler::list(
|
||||
|
@ -121,13 +127,14 @@ fn main() -> Result<()> {
|
|||
return msg_handler::move_(seq, mbox, &mut printer, &mut imap);
|
||||
}
|
||||
Some(msg_arg::Command::Read(seq, text_mime, raw)) => {
|
||||
return msg_handler::read(seq, text_mime, raw, &mut printer, &mut imap);
|
||||
return msg_handler::read(seq, text_mime, raw, &account, &mut printer, &mut imap);
|
||||
}
|
||||
Some(msg_arg::Command::Reply(seq, all, atts)) => {
|
||||
Some(msg_arg::Command::Reply(seq, all, attachment_paths, encrypt)) => {
|
||||
return msg_handler::reply(
|
||||
seq,
|
||||
all,
|
||||
atts,
|
||||
attachment_paths,
|
||||
encrypt,
|
||||
&account,
|
||||
&mut printer,
|
||||
&mut imap,
|
||||
|
@ -151,8 +158,8 @@ fn main() -> Result<()> {
|
|||
Some(msg_arg::Command::Send(raw_msg)) => {
|
||||
return msg_handler::send(raw_msg, &account, &mut printer, &mut imap, &mut smtp);
|
||||
}
|
||||
Some(msg_arg::Command::Write(atts)) => {
|
||||
return msg_handler::write(atts, &account, &mut printer, &mut imap, &mut smtp);
|
||||
Some(msg_arg::Command::Write(atts, encrypt)) => {
|
||||
return msg_handler::write(atts, encrypt, &account, &mut printer, &mut imap, &mut smtp);
|
||||
}
|
||||
Some(msg_arg::Command::Flag(m)) => match m {
|
||||
Some(flag_arg::Command::Set(seq_range, flags)) => {
|
||||
|
@ -177,10 +184,18 @@ fn main() -> Result<()> {
|
|||
return tpl_handler::forward(seq, tpl, &account, &mut printer, &mut imap);
|
||||
}
|
||||
Some(tpl_arg::Command::Save(atts, tpl)) => {
|
||||
return tpl_handler::save(&mbox, atts, tpl, &mut printer, &mut imap);
|
||||
return tpl_handler::save(&mbox, &account, atts, tpl, &mut printer, &mut imap);
|
||||
}
|
||||
Some(tpl_arg::Command::Send(atts, tpl)) => {
|
||||
return tpl_handler::send(&mbox, atts, tpl, &mut printer, &mut imap, &mut smtp);
|
||||
return tpl_handler::send(
|
||||
&mbox,
|
||||
&account,
|
||||
atts,
|
||||
tpl,
|
||||
&mut printer,
|
||||
&mut imap,
|
||||
&mut smtp,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use std::process::Command;
|
||||
|
||||
/// TODO: move this in a more approriate place.
|
||||
pub fn run_cmd(cmd: &str) -> Result<String> {
|
||||
debug!("running command: {}", cmd);
|
||||
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd").args(&["/C", cmd]).output()
|
||||
} else {
|
||||
|
|
19
tests/emails/alice-to-patrick-encrypted.eml
Normal file
19
tests/emails/alice-to-patrick-encrypted.eml
Normal file
|
@ -0,0 +1,19 @@
|
|||
From: alice@localhost
|
||||
To: patrick@localhost
|
||||
Subject: Encrypted message
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="boundary"
|
||||
|
||||
--boundary
|
||||
Content-Type: application/pgp-encrypted
|
||||
|
||||
Version: 1
|
||||
|
||||
--boundary
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--boundary
|
81
tests/keys/alice.asc
Normal file
81
tests/keys/alice.asc
Normal file
|
@ -0,0 +1,81 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
=XrAt
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
41
tests/keys/alice.pub.asc
Normal file
41
tests/keys/alice.pub.asc
Normal file
|
@ -0,0 +1,41 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
qtuSHJqRM01nuXfcSw==
|
||||
=JGp0
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
81
tests/keys/patrick.asc
Normal file
81
tests/keys/patrick.asc
Normal file
|
@ -0,0 +1,81 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lQVYBGH/wDYBDADlRkqjj5jOTBc0p+9Fk8sIstXjLbxUl4lMsw9Mh6rnuoCVc49D
|
||||
nlG8ZbqS/j2jpNE8e4F3rFCkLnirGLT9tYIDE0xC6/B8AtDJNSaxb0AJKqIR4v6O
|
||||
qunndGrg616H7U55NcLCT9zEJ8+lo/i7b0KcKt7RVdw064Vj1KwhEeEgdQ8WCrsq
|
||||
TA18f3HBRS5ChqEDxYwYfet5rn5BF0ok5/aWHJkxOh+VnZwszjahxkzJ6BtDOJq+
|
||||
HGrhCFT+YCxLmFJIGZF95RPOH2TBCqJweh83opY/cnbu8zV4Zh2tGQu/ohZC2uPM
|
||||
G/n6QoXv/7n/7/8dTtHH01enoCJxxSONfPg/F4PlUyZJcQOI+FR8HVrrhlVBf2co
|
||||
G506J9C31san59jjtsMxHnrDvinusnr/wpy25R0KwHBXseNk9YInKc5tnjVNNLOa
|
||||
XZGAcKD7WMtbG20N9oqJl2aWf50CTj4IMBbSclw7fcok81Z7DK8a2uYINPk2ozTQ
|
||||
6En5iIvFwTmFJwkAEQEAAQAL/2AWR22o3reGuCr/Po4AVJT+rhkZr9Yb9BTK7lx6
|
||||
dyvKw9zeo2oJTeQRFlJIbvjIOFCKykWnV9yXBUdfgWrayPQVAF8DlrPCUlIhDmhK
|
||||
YaH11hp88YZFJuYzqh89RU7eK4cs+sSIx9MFhEa9I58aD+Z3KQ6+Vx1un2apWMI7
|
||||
RgheRsZMFQiy+uv0VW5UWgDTf2OfRQl2rFtAv/Tzl8VD2dorfiBdZaNEfJFikw7V
|
||||
lpT/y30umduW+Uv6O/Snxaig2v/98IRgNbnwwxrC9l4nxftDJEURkzkQOZkC+pjZ
|
||||
+8uzrND3aF7o3lXKDmW0gw4ECW9GQpkebde2xLfyvTh+3kLHTYWjf1UoaY4R/3U7
|
||||
wxQySH5d1tOf3fUw0C1XNVL8octTT5AFIOvPhCwh4yyhX1HYzE63Eu2qlptANj0S
|
||||
uFMpuFsGmxQV4W7ULVRf1MFHV+upq73hCuT2Rtx7GHFlhm6e41XcIF67B4n3rG1p
|
||||
BIByaNGGy/iGnsQXxJUEUy0pyQYA8M7whL84GazJ6zrR9cBkrWxhIupX5nxJUqTu
|
||||
wofSkc0DAL4fllIi7PkE8EQZsyyGZ4zljHs4VdikNnh8eAkB0VwMlZBqE6dZAmqz
|
||||
CVbD943q661INdBxvKU+SlVSHDBeBHLjlxV2pTnmYP+iTLUyyCZlO8m7Hj6z5ZbB
|
||||
dxpObA/7K0w6Tm9Dja9fMqiFkcrz5s+lEqwRBuHSGoJlcNijmbqQSPkIs3jC9Z81
|
||||
jzK4oZvp05yEcyadQc4SWupEcsQ3BgDzvRTJytnNVEQLy9CLJaj9JKIWC8gQ1u/w
|
||||
Us/sEmHk9/xEg9cI6E6OAExNa8we1wzIoBJNkNaxH5ssvuTUp72rXUAf77nftHbi
|
||||
iII7QDO+qZM/JmMCtchwh1AQRJqliQTMif5UJI8eO6NHjRX3460yisNx8yHSQbDG
|
||||
pYUBU86eAtBWJoeM+tX8Pzba4+X1yply5SK5SxsLz91VpkG5HqulrqmySTHcTHSR
|
||||
RawNnDEdiM/SIaYZ6mTLDey+SbrETr8F/0pKkNRdX5Jt0pKI5AyiqU9a50RsggG3
|
||||
7W+5SwbcMlNXx/FzM7XklmuLb0tjbo2tmWSZVC6ewrmWOSsJ58Hz447BWM+e3gJF
|
||||
8+81Ko0fKidQBPDTJlR1xQhuIAfqVti2QMl9P81moIp/yks9V0fBmhhBTvpSG4nA
|
||||
fE6x1n6+13la1GHAHMbbtLv7rLZ7ly5yTaYewoZZZgJbms9oTrRWzsq7wDwYXzWI
|
||||
VeAVTFLkUnxk2aD7+XEL7QrkIHwHjWveHua+tBtQYXRyaWNrIDxwYXRyaWNrQGxv
|
||||
Y2FsaG9zdD6JAdIEEwEKADwWIQQgIAsdfZhSAa/Tv/C756VEEqufYwUCYf/ANgIb
|
||||
AwUJA8JnAAQLCQgHBBUKCQgFFgIDAQACHgECF4AACgkQu+elRBKrn2Poywv+KeLR
|
||||
3aHRmPioVjmiXdDnkQFoAXlmhgtUcfnCHaLJ9bPuoe/2PiI5O+gEHpLfwufn+7Dq
|
||||
I3ve3oZL3BaCuUy1qboU2yT8vCEMkUlrqErrrYws6Fz3Gn3uLcHeoycfvrhN6FVk
|
||||
40+btcApnRKWdUq0XOgS6MdCz5nfHq9RQZ73zNVYIIlK6HeuUj2OSFbmHogmI+wO
|
||||
OopU0ZE48PLKKkP38N9Rr6SKk8VPyRrfLq+Guq50LfYz2gMuyEzoaYQT0A8oPVHu
|
||||
6fquoLaKHnKgW62PPriBQB0pITmkmDNUNMJZ60fKZtNF/EI3jSYgquILyFaKkYKm
|
||||
Sd8ghqp3LXTzH1JX2N4ant3z5AQQGcL2HafCxPw+C+ipVnfSH2qTvqUDjTuIxAFx
|
||||
4l75o/B16zI4t7cQlQzeBNAu4TyFAKkUUKfzshi99PNQ4pPxMFBNROWzDb8/GXeP
|
||||
T+P4gQo4CwukP+/GAxtqpOuvlDu8sfFo66F0FQWOvR8QGLdIxiadEwqesWMxnQVY
|
||||
BGH/wDYBDACj11gdzw0YfmwrjLKae4z/J5D5ivHjE9GD4a1zHOQmgrt4mYIUjVt5
|
||||
F30EERnHEl1fIlAZkMuLcgmCfGwmjz/mJsji8yb+dbZlIGPBs2aw2Ikznzx7lsO/
|
||||
u6SK2w+SkJhYmhW3zMyFSYLgxINVxQWBhUNaJhFHZnHD1iE20QLVQEunh8ReuoQH
|
||||
a0ErG/g0Url1vBlmAg99R5YR2uwRPbdso3PDA5f3EbDzCRg/XZtK/yQhPSt7DAhl
|
||||
Ya+2+Ovh5oZ2GowiFuXYteE8yEiyP4IPy5DvuB20c2QtBkHyBr2a3/+DujJGL5Fh
|
||||
U+E0+ClHrsfCWOD4+sHSn+NUCz+8FvGVMepJPWyx3rdd4rLnzb9h45Q9lXEBfIEQ
|
||||
KdltxE+EdYFIPDpz0a4AOeBghdpQe5fREaSomGgGyqUFLqVJRNbE6509gtfMiGld
|
||||
11lRaZ9PgKSm7JbIjSDF4ZbA859ipPicuu8eW2Y7PAUOLfc5QLzBOQHA/uMadWnY
|
||||
WZwFJLIYROkAEQEAAQAL+QEoZcrjIk9uoEbQAhiZoCnS7qE20EYHpzLAguRl+z5C
|
||||
7P55jjvlMlTpG7TuRoF7wZ1pHYoKtgeEnSjXBoAgwcW3dzK0X22LqSfuikntgb+k
|
||||
7hZHbSrd6kD1+2AQU3w4iZ0RrK7dc4ILHpHGTbvKzkLHrW3LCFL5+DqXLimoITYe
|
||||
09IJoXN+a62uPjoG4vKCtaUNeNv5zoB3A6pZYtLt3diWkJw7j6S7MyYKhcl32L+3
|
||||
TRrvhtnCIGKQBcj8GhWg9oYkWoA5bDg10lZiEhh98EWKoFWMbZ327VOENYAkYgr7
|
||||
ApyupgzWqKf9yt2jUHaBL4UnAYFgnq824+9e0oNohDGstXt5C7JcX/+x+JzHYwti
|
||||
FOKsfj627QOW0F/wiIn2up9ZvF1yMLqwgIA2EsjYY291p7OD0PGWIqhmQvOacsBD
|
||||
ZXIuY8F2+2CPmwtvrqBafFrA8oEpv/2vMuLnfdFtaiMUUnXzUcz2kI5f6uphIl4M
|
||||
wWwfVN7v+qhNVhBDTMOkwQYAxTTSfVcg3SV9WalguAj2mDpvEg/JEEAKgNM2mnz8
|
||||
Y/3JHVdFNFdSylc8mh8+3MW2xkfnHYA6+D5YyHb0hd3qlJuef2M8HzbJXlrtFiG3
|
||||
t5Kd4W9t+RE4wW8hnBc8pfHhUeIMxky0rldhl70+Sj8cjFx/FWNLBQydEo3OXdm6
|
||||
/en11hOu0jktbE8P/ohK91PmWZwGTYPJcktddgUh71ajnKkUa+hhXSopy87V/pgc
|
||||
JnEYQFsTZvIf5qBFGCG0lospBgDUsAcgQ/sUjc6qTj+gF9vWJXsKfm+l51KElohr
|
||||
KBbUmZxZHTfWpvtqLA12MjNp7hi+ayDA8hjxsa3HNHP9M8nilYSxK2v6VENcnnkx
|
||||
F/x18OitDsV97Py1XNY4IHnBI3cDfV4DcasZyhF+vbHVoqhDwmS0KBO7kPvWNJRi
|
||||
zV/J9xrSAG8ww4ppoWEAHcDxgWiyt/8KwNfzO0EuiBr28W5//Rp1xDS7mKbZXEZO
|
||||
vPF7sF2mo/QI/4ovoyo8M7AU48EGAIRCfwPmGstu/3GW/YyOPrQaNBpB9G+Rnpvo
|
||||
lQ8K++hhRIQmGPpbUTLydmY1U7V8ZPob8PpT+wVgkAq8OYYHoHSYK1EhmqBEJaXT
|
||||
3YtKLYVtwg+frKO2k+WKhrxbxL5aBa6Vsx+YQzcz8L/mTtwlCORzyertdJ+IyY9y
|
||||
eXW/3Pp/HrxN9s5Ioa/HKL3idhABKCx/mqKhfJ28dKWjTn/RVImgBZKGkPvUrzFN
|
||||
0uT9WYHSW29yzWVtLnENKVQ3bz+OJ+SqiQG8BBgBCgAmFiEEICALHX2YUgGv07/w
|
||||
u+elRBKrn2MFAmH/wDYCGwwFCQPCZwAACgkQu+elRBKrn2Mn3wwAjITl+3zbS2RA
|
||||
L6MUUqCxmqRmWRoSjU8R4nb45NJvm11C0IYk/0MvZg8FTSjqf65uRrYnZzJPWW/0
|
||||
UTS314bQaezLZTwUfrjrGRnUMKayVpPr+24ZZoRFDIs6Wnd8PtLzh0jy8jnwQVjV
|
||||
DN/9ktruNMf5lB6kIuAHQtXyUNepxdRFaF79Z21zKUeTcyfLR7jKicC/55NakWI3
|
||||
GwbGCvUS0oaWXEHTIT+OjfA0jyfAo1cBvGU2tfUTYjLcFwWxV4KDJNAXfZWm9u6G
|
||||
zXJ4IVwtHTdztbR4PzP9VnPbxGeGL+UyRj+kdh1WBGg5pXnWeoHaAQjT/DXScFON
|
||||
OQ/MCj/Ch5lxdl8kLoY8Hn5ADn3WiXeBONZiP6lIDhh3jFdPZOQWxBjFHozLQTok
|
||||
RRAYjPLTrppnDH+s5FDZzbeWwRv+yBqfo0s/97bjQEw4HeiJwX4yPupV+5gnovca
|
||||
3994zx37Xsw54NJaoln7fZ4qBYqgL3Z74sTuF62usumUM1KHbkeC
|
||||
=OpBu
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
41
tests/keys/patrick.pub.asc
Normal file
41
tests/keys/patrick.pub.asc
Normal file
|
@ -0,0 +1,41 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
dnvixO4Xra6y6ZQzUoduR4I=
|
||||
=CQBw
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
Loading…
Reference in a new issue