mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 09:05:13 +00:00
add idle mode
This commit is contained in:
parent
b2f1543bbf
commit
6e9e7cd30e
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- IDLE support [#29]
|
||||
|
||||
## [0.2.0] - 2021-03-10
|
||||
|
||||
### Added
|
||||
|
@ -66,4 +70,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#21]: https://github.com/soywod/himalaya/issues/21
|
||||
[#22]: https://github.com/soywod/himalaya/issues/22
|
||||
[#25]: https://github.com/soywod/himalaya/issues/25
|
||||
[#29]: https://github.com/soywod/himalaya/issues/29
|
||||
[#32]: https://github.com/soywod/himalaya/issues/32
|
||||
|
|
119
README.md
119
README.md
|
@ -18,6 +18,7 @@ Minimalist CLI email client, written in Rust.
|
|||
* [Write a new message](#write-a-new-message)
|
||||
* [Reply to a message](#reply-to-a-message)
|
||||
* [Forward a message](#forward-a-message)
|
||||
* [Listen to new messages](#listen-to-new-messages)
|
||||
* [License](https://github.com/soywod/himalaya/blob/master/LICENSE)
|
||||
* [Changelog](https://github.com/soywod/himalaya/blob/master/CHANGELOG.md)
|
||||
* [Credits](#credits)
|
||||
|
@ -39,8 +40,8 @@ more flexibility.
|
|||
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | bash
|
||||
```
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Installation) for
|
||||
more information.*
|
||||
*See the [wiki section](https://github.com/soywod/himalaya/wiki/Installation)
|
||||
for other installation methods.*
|
||||
|
||||
## Configuration
|
||||
|
||||
|
@ -50,8 +51,6 @@ more information.*
|
|||
name = "Your full name"
|
||||
downloads-dir = "/abs/path/to/downloads"
|
||||
|
||||
# Himalaya supports the multi-account
|
||||
# Each account should be inside a TOML section
|
||||
[gmail]
|
||||
default = true
|
||||
email = "my.email@gmail.com"
|
||||
|
@ -59,34 +58,16 @@ email = "my.email@gmail.com"
|
|||
imap-host = "imap.gmail.com"
|
||||
imap-port = 993
|
||||
imap-login = "test@gmail.com"
|
||||
imap-passwd_cmd = "pass show gmail"
|
||||
imap-passwd-cmd = "pass show gmail"
|
||||
|
||||
smtp-host = "smtp.gmail.com"
|
||||
smtp-port = 487
|
||||
smtp-login = "test@gmail.com"
|
||||
smtp-passwd_cmd = "pass show gmail"
|
||||
|
||||
[posteo]
|
||||
name = "Your overriden full name"
|
||||
downloads-dir = "/abs/path/to/overriden/downloads"
|
||||
email = "test@posteo.net"
|
||||
|
||||
imap-host = "posteo.de"
|
||||
imap-port = 993
|
||||
imap-login = "test@posteo.net"
|
||||
imap-passwd_cmd = "security find-internet-password -gs posteo -w"
|
||||
|
||||
smtp-host = "posteo.de"
|
||||
smtp-port = 487
|
||||
smtp-login = "test@posteo.net"
|
||||
smtp-passwd_cmd = "security find-internet-password -gs posteo -w"
|
||||
|
||||
# [other accounts]
|
||||
# ...
|
||||
smtp-passwd-cmd = "security find-internet-password -gs posteo -w"
|
||||
```
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Configuration) for
|
||||
more information.*
|
||||
*See the [wiki section](https://github.com/soywod/himalaya/wiki/Configuration)
|
||||
for all the options.*
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -104,12 +85,13 @@ FLAGS:
|
|||
|
||||
OPTIONS:
|
||||
-a, --account <STRING> Name of the account to use
|
||||
-o, --output <STRING> Format of the output to print [default: text] [possible values: text, json]
|
||||
-o, --output <STRING> Format of the output to print [possible values: text, json]
|
||||
|
||||
SUBCOMMANDS:
|
||||
attachments Downloads all attachments from an email
|
||||
forward Forwards an email
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
idle Starts the idle mode
|
||||
list Lists emails sorted by arrival date
|
||||
mailboxes Lists all available mailboxes
|
||||
read Reads text bodies of an email
|
||||
|
@ -121,89 +103,98 @@ SUBCOMMANDS:
|
|||
write Writes a new email
|
||||
```
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage) for more
|
||||
information.*
|
||||
*See the [wiki section](https://github.com/soywod/himalaya/wiki/Usage) for more
|
||||
information about commands.*
|
||||
|
||||
### List mailboxes
|
||||
|
||||
Shows mailboxes in a basic table.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/104848169-0e432000-58e4-11eb-8410-05f0404c0d99.png)
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:list-mailboxes)
|
||||
for more information.*
|
||||
Shows mailboxes in a basic table.
|
||||
|
||||
### List messages
|
||||
|
||||
Shows messages in a basic table.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/104848096-aee51000-58e3-11eb-8d99-bcfab5ca28ba.png)
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:list-messages) for
|
||||
more information.*
|
||||
Shows messages in a basic table.
|
||||
|
||||
### Search messages
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/110698977-9d86f880-81ee-11eb-8990-0ca89c7d4640.png)
|
||||
|
||||
Shows filtered messages in a basic table. The query should follow the
|
||||
[RFC-3501](https://tools.ietf.org/html/rfc3501#section-6.4.4).
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/110698977-9d86f880-81ee-11eb-8990-0ca89c7d4640.png)
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:search-messages) for
|
||||
more information.*
|
||||
|
||||
### Download attachments
|
||||
|
||||
Downloads all attachments directly to the [`downloads-dir`](#configuration).
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/104848278-890c3b00-58e4-11eb-9b5c-48807c04f762.png)
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:download-attachments)
|
||||
for more information.*
|
||||
Downloads all attachments from a message directly to the
|
||||
[`downloads-dir`](https://github.com/soywod/himalaya/wiki/Configuration).
|
||||
|
||||
### Read a message
|
||||
|
||||
Shows the text content of a message (`text/plain` if exists, otherwise
|
||||
`text/html`). Can be overriden by the `--mime-type` option.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/110701369-5d754500-81f1-11eb-932f-94c2ca8db068.png)
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:read-a-message) for
|
||||
more information.*
|
||||
Shows the text content of a message (`text/plain` if exists, otherwise
|
||||
`text/html`).
|
||||
|
||||
### Write a new message
|
||||
|
||||
Opens your default editor (from the `$EDITOR` environment variable) to compose
|
||||
a new message.
|
||||
|
||||
```bash
|
||||
himalaya write
|
||||
```
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:write-a-new-message) for
|
||||
more information.*
|
||||
Opens your default editor (from the `$EDITOR` environment variable) to compose
|
||||
a new message.
|
||||
|
||||
### Reply to a message
|
||||
|
||||
Opens your default editor to reply to a message.
|
||||
|
||||
```bash
|
||||
himalaya reply --all 5123
|
||||
```
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:reply-to-a-message) for
|
||||
more information.*
|
||||
Opens your default editor to reply to a message.
|
||||
|
||||
### Forward a message
|
||||
|
||||
Opens your default editor to forward a message.
|
||||
|
||||
```bash
|
||||
himalaya forward 5123
|
||||
```
|
||||
|
||||
*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:forward-a-message) for
|
||||
more information.*
|
||||
Opens your default editor to forward a message.
|
||||
|
||||
### Listen to new messages
|
||||
|
||||
```bash
|
||||
himalaya idle
|
||||
```
|
||||
|
||||
Starts a session in idle mode (blocking). When a new message arrives, it runs
|
||||
the command `notification-cmd` defined in the [config
|
||||
file](https://github.com/soywod/himalaya/wiki/Configuration).
|
||||
|
||||
Here a use case with [`systemd`](https://en.wikipedia.org/wiki/Systemd):
|
||||
|
||||
```ini
|
||||
# ~/.config/systemd/user/himalaya.service
|
||||
|
||||
[Unit]
|
||||
Description=Himalaya new messages notifier
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/himalaya idle
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
systemctl --user enable himalaya.service
|
||||
systemctl --user start himalaya.service
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
|
|
|
@ -24,21 +24,28 @@ pub enum Error {
|
|||
GetAccountNotFoundError(String),
|
||||
GetAccountDefaultNotFoundError,
|
||||
OutputError(output::Error),
|
||||
|
||||
// new erorrs,
|
||||
RunNotifyCmdError(output::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "config: ")?;
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
Error::IoError(err) => err.fmt(f),
|
||||
Error::ParseTomlError(err) => err.fmt(f),
|
||||
Error::ParseTomlAccountsError => write!(f, "no account found"),
|
||||
Error::GetEnvVarError(err) => err.fmt(f),
|
||||
Error::GetPathNotFoundError => write!(f, "path not found"),
|
||||
Error::GetAccountNotFoundError(account) => write!(f, "account {} not found", account),
|
||||
Error::GetAccountDefaultNotFoundError => write!(f, "no default account found"),
|
||||
Error::OutputError(err) => err.fmt(f),
|
||||
IoError(err) => err.fmt(f),
|
||||
ParseTomlError(err) => err.fmt(f),
|
||||
ParseTomlAccountsError => write!(f, "no account found"),
|
||||
GetEnvVarError(err) => err.fmt(f),
|
||||
GetPathNotFoundError => write!(f, "path not found"),
|
||||
GetAccountNotFoundError(account) => write!(f, "account {} not found", account),
|
||||
GetAccountDefaultNotFoundError => write!(f, "no default account found"),
|
||||
OutputError(err) => err.fmt(f),
|
||||
RunNotifyCmdError(err) => {
|
||||
write!(f, "run notification cmd: ")?;
|
||||
err.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +131,7 @@ impl Account {
|
|||
pub struct Config {
|
||||
pub name: String,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
pub notification_cmd: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, Account>,
|
||||
|
@ -202,4 +210,15 @@ impl Config {
|
|||
let name = account.name.as_ref().unwrap_or(&self.name);
|
||||
format!("{} <{}>", name, account.email)
|
||||
}
|
||||
|
||||
pub fn run_notify_cmd(&self, subject: &str, sender: &str) -> Result<()> {
|
||||
let default_cmd = format!(r#"notify-send "📫 {}" "{}""#, sender, subject);
|
||||
let cmd = self
|
||||
.notification_cmd
|
||||
.as_ref()
|
||||
.map(|s| format!(r#"{} "{}" "{}""#, s, subject, sender))
|
||||
.unwrap_or(default_cmd);
|
||||
run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
62
src/imap.rs
62
src/imap.rs
|
@ -2,9 +2,11 @@ use imap;
|
|||
use native_tls::{self, TlsConnector, TlsStream};
|
||||
use std::{fmt, net::TcpStream, result};
|
||||
|
||||
use crate::config::{self, Account};
|
||||
use crate::mbox::{Mbox, Mboxes};
|
||||
use crate::msg::{Msg, Msgs};
|
||||
use crate::{
|
||||
config::{self, Account, Config},
|
||||
mbox::{Mbox, Mboxes},
|
||||
msg::{Msg, Msgs},
|
||||
};
|
||||
|
||||
// Error wrapper
|
||||
|
||||
|
@ -17,26 +19,33 @@ pub enum Error {
|
|||
ReadEmailEmptyPartError(String, String),
|
||||
ExtractAttachmentsEmptyError(String),
|
||||
ConfigError(config::Error),
|
||||
|
||||
// new errors
|
||||
IdleError(imap::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "imap: ")?;
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
Error::CreateTlsConnectorError(err) => err.fmt(f),
|
||||
Error::CreateImapSession(err) => err.fmt(f),
|
||||
Error::ParseEmailError(err) => err.fmt(f),
|
||||
Error::ConfigError(err) => err.fmt(f),
|
||||
Error::ReadEmailNotFoundError(uid) => {
|
||||
CreateTlsConnectorError(err) => err.fmt(f),
|
||||
CreateImapSession(err) => err.fmt(f),
|
||||
ParseEmailError(err) => err.fmt(f),
|
||||
ConfigError(err) => err.fmt(f),
|
||||
ReadEmailNotFoundError(uid) => {
|
||||
write!(f, "no email found for uid {}", uid)
|
||||
}
|
||||
Error::ReadEmailEmptyPartError(uid, mime) => {
|
||||
ReadEmailEmptyPartError(uid, mime) => {
|
||||
write!(f, "no {} content found for uid {}", mime, uid)
|
||||
}
|
||||
Error::ExtractAttachmentsEmptyError(uid) => {
|
||||
ExtractAttachmentsEmptyError(uid) => {
|
||||
write!(f, "no attachment found for uid {}", uid)
|
||||
}
|
||||
IdleError(err) => {
|
||||
write!(f, "IMAP idle mode: ")?;
|
||||
err.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +106,37 @@ impl<'a> ImapConnector<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn last_new_seq(&mut self) -> Result<Option<u32>> {
|
||||
Ok(self.sess.uid_search("NEW")?.into_iter().next())
|
||||
}
|
||||
|
||||
pub fn idle(&mut self, config: &Config, mbox: &str) -> Result<()> {
|
||||
let mut prev_seq = 0;
|
||||
self.sess.examine(mbox)?;
|
||||
|
||||
loop {
|
||||
self.sess
|
||||
.idle()
|
||||
.and_then(|idle| idle.wait_keepalive())
|
||||
.map_err(Error::IdleError)?;
|
||||
|
||||
if let Some(seq) = self.last_new_seq()? {
|
||||
if prev_seq != seq {
|
||||
if let Some(msg) = self
|
||||
.sess
|
||||
.uid_fetch(seq.to_string(), "(ENVELOPE)")?
|
||||
.iter()
|
||||
.next()
|
||||
.map(Msg::from)
|
||||
{
|
||||
config.run_notify_cmd(&msg.subject, &msg.sender)?;
|
||||
prev_seq = seq;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_mboxes(&mut self) -> Result<Mboxes> {
|
||||
let mboxes = self
|
||||
.sess
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -253,6 +253,11 @@ fn run() -> Result<()> {
|
|||
.arg(mailbox_arg()),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("idle")
|
||||
.about("Starts the idle mode")
|
||||
.arg(mailbox_arg()),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let account_name = matches.value_of("account");
|
||||
|
@ -525,6 +530,14 @@ fn run() -> Result<()> {
|
|||
imap_conn.logout();
|
||||
}
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("idle") {
|
||||
let config = Config::new_from_file()?;
|
||||
let account = config.find_account_by_name(account_name)?;
|
||||
let mut imap_conn = ImapConnector::new(&account)?;
|
||||
let mbox = matches.value_of("mailbox").unwrap();
|
||||
imap_conn.idle(&config, &mbox)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue