release v0.5.4 (#285)

* replace bsd3 license by bsd4

* add attachments with save and send commands (#284)

* set up tpl save and send commands

* improve msg save and send handlers

* add vim msg#add_attachment fn

* improve vim logs

* update changelog

* add attachment keybind vim doc

* reverse range order fetch envelopes (#276)

* bump version v0.5.4
This commit is contained in:
Clément DOUIN 2022-02-05 00:29:57 +01:00 committed by GitHub
parent 0e452d8a47
commit e33a9a72e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 243 additions and 91 deletions

View file

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.4] - 2022-02-05
### Fixed
- Add attachments with save and send commands [#47] [#259]
- Invalid sequence set [#276]
## [0.5.3] - 2022-02-03
### Added
@ -273,7 +280,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.3...HEAD
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.4...HEAD
[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
[0.5.1]: https://github.com/soywod/himalaya/compare/v0.5.0...v0.5.1
@ -325,6 +333,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#39]: https://github.com/soywod/himalaya/issues/39
[#40]: https://github.com/soywod/himalaya/issues/40
[#41]: https://github.com/soywod/himalaya/issues/41
[#47]: https://github.com/soywod/himalaya/issues/47
[#48]: https://github.com/soywod/himalaya/issues/48
[#50]: https://github.com/soywod/himalaya/issues/50
[#58]: https://github.com/soywod/himalaya/issues/58
@ -383,9 +392,11 @@ 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
[#259]: https://github.com/soywod/himalaya/issues/259
[#268]: https://github.com/soywod/himalaya/issues/268
[#272]: https://github.com/soywod/himalaya/issues/272
[#273]: https://github.com/soywod/himalaya/issues/273
[#276]: https://github.com/soywod/himalaya/issues/276
[#271]: https://github.com/soywod/himalaya/issues/271
[#276]: https://github.com/soywod/himalaya/issues/276
[#280]: https://github.com/soywod/himalaya/issues/280

2
Cargo.lock generated
View file

@ -361,7 +361,7 @@ dependencies = [
[[package]]
name = "himalaya"
version = "0.5.3"
version = "0.5.4"
dependencies = [
"ammonia",
"anyhow",

View file

@ -1,7 +1,7 @@
[package]
name = "himalaya"
description = "Command-line interface for email management"
version = "0.5.3"
version = "0.5.4"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2018"
license-file = "LICENSE"

44
LICENSE
View file

@ -1,30 +1,32 @@
Copyright © 2020,2021 soywod <clement.douin@posteo.net>
Copyright (c) 2020-2021, soywod (Clément DOUIN) <clement.douin@posteo.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Author name here nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
3. All advertising materials mentioning features or use of this software must
display the following acknowledgement:
This product includes software developed by Clément DOUIN.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -144,11 +144,11 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
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!("{}:{}", begin, end)
format!("{}:{}", end, begin)
} else {
String::from("1:*")
};
debug!("range: {:?}", range);
debug!("range: {}", range);
let fetches = self
.sess()?

View file

@ -23,7 +23,7 @@ type Raw = bool;
type All = bool;
type RawMsg<'a> = &'a str;
type Query = String;
type AttachmentsPaths<'a> = Vec<&'a str>;
type AttachmentPaths<'a> = Vec<&'a str>;
type MaxTableWidth = Option<usize>;
/// Message commands.
@ -31,15 +31,15 @@ pub enum Command<'a> {
Attachments(Seq<'a>),
Copy(Seq<'a>, Mbox<'a>),
Delete(Seq<'a>),
Forward(Seq<'a>, AttachmentsPaths<'a>),
Forward(Seq<'a>, AttachmentPaths<'a>),
List(MaxTableWidth, Option<PageSize>, Page),
Move(Seq<'a>, Mbox<'a>),
Read(Seq<'a>, TextMime<'a>, Raw),
Reply(Seq<'a>, All, AttachmentsPaths<'a>),
Reply(Seq<'a>, All, AttachmentPaths<'a>),
Save(RawMsg<'a>),
Search(Query, MaxTableWidth, Option<PageSize>, Page),
Send(RawMsg<'a>),
Write(AttachmentsPaths<'a>),
Write(AttachmentPaths<'a>),
Flag(Option<flag_arg::Command<'a>>),
Tpl(Option<tpl_arg::Command<'a>>),
@ -256,7 +256,7 @@ fn page_arg<'a>() -> Arg<'a, 'a> {
}
/// Message attachment argument.
fn attachment_arg<'a>() -> Arg<'a, 'a> {
pub fn attachment_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("attachments")
.help("Adds attachment to the message")
.short("a")

View file

@ -5,7 +5,7 @@
use anyhow::{Context, Result};
use atty::Stream;
use imap::types::Flag;
use log::{debug, trace};
use log::{debug, info, trace};
use std::{
borrow::Cow,
convert::{TryFrom, TryInto},
@ -244,14 +244,25 @@ pub fn reply<
imap.add_flags(seq, &flags)
}
/// Save a raw message to the targetted mailbox.
/// 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<()> {
let raw_msg = if atty::is(Stream::Stdin) || printer.is_json() {
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()
@ -261,8 +272,6 @@ pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
.collect::<Vec<String>>()
.join("\r\n")
};
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(mbox, raw_msg.as_bytes(), flags)
}
@ -297,7 +306,19 @@ pub fn send<
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let raw_msg = if atty::is(Stream::Stdin) || printer.is_json() {
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()
@ -307,15 +328,11 @@ pub fn send<
.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);
let msg = Msg::from_tpl(&raw_msg)?;
let envelope: lettre::address::Envelope = msg.try_into()?;
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
debug!("message sent!");
// Save message to sent folder
let mbox = Mbox::new(&account.sent_folder);
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags)
}

View file

@ -4,12 +4,14 @@
use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::{debug, trace};
use log::{debug, info, trace};
use crate::domain::msg::msg_arg;
type Seq<'a> = &'a str;
type All = bool;
type ReplyAll = bool;
type AttachmentPaths<'a> = Vec<&'a str>;
type Tpl<'a> = &'a str;
#[derive(Debug, Default)]
pub struct TplOverride<'a> {
@ -23,69 +25,77 @@ pub struct TplOverride<'a> {
pub sig: Option<&'a str>,
}
impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
fn from(matches: &'a ArgMatches<'a>) -> Self {
Self {
subject: matches.value_of("subject"),
from: matches.values_of("from").map(|v| v.collect()),
to: matches.values_of("to").map(|v| v.collect()),
cc: matches.values_of("cc").map(|v| v.collect()),
bcc: matches.values_of("bcc").map(|v| v.collect()),
headers: matches.values_of("headers").map(|v| v.collect()),
body: matches.value_of("body"),
sig: matches.value_of("signature"),
}
}
}
/// Message template commands.
pub enum Command<'a> {
New(TplOverride<'a>),
Reply(Seq<'a>, All, TplOverride<'a>),
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
Forward(Seq<'a>, TplOverride<'a>),
Save(AttachmentPaths<'a>, Tpl<'a>),
Send(AttachmentPaths<'a>, Tpl<'a>),
}
/// Message template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("new") {
debug!("new command matched");
let tpl = TplOverride {
subject: m.value_of("subject"),
from: m.values_of("from").map(|v| v.collect()),
to: m.values_of("to").map(|v| v.collect()),
cc: m.values_of("cc").map(|v| v.collect()),
bcc: m.values_of("bcc").map(|v| v.collect()),
headers: m.values_of("headers").map(|v| v.collect()),
body: m.value_of("body"),
sig: m.value_of("signature"),
};
trace!(r#"template args: "{:?}""#, tpl);
info!("new command matched");
let tpl = TplOverride::from(m);
trace!("template override: {:?}", tpl);
return Ok(Some(Command::New(tpl)));
}
if let Some(m) = m.subcommand_matches("reply") {
debug!("reply command matched");
info!("reply command matched");
let seq = m.value_of("seq").unwrap();
trace!(r#"seq: "{}""#, seq);
debug!("sequence: {}", seq);
let all = m.is_present("reply-all");
trace!("reply all: {}", all);
let tpl = TplOverride {
subject: m.value_of("subject"),
from: m.values_of("from").map(|v| v.collect()),
to: m.values_of("to").map(|v| v.collect()),
cc: m.values_of("cc").map(|v| v.collect()),
bcc: m.values_of("bcc").map(|v| v.collect()),
headers: m.values_of("headers").map(|v| v.collect()),
body: m.value_of("body"),
sig: m.value_of("signature"),
};
trace!(r#"template args: "{:?}""#, tpl);
debug!("reply all: {}", all);
let tpl = TplOverride::from(m);
trace!("template override: {:?}", tpl);
return Ok(Some(Command::Reply(seq, all, tpl)));
}
if let Some(m) = m.subcommand_matches("forward") {
debug!("forward command matched");
info!("forward command matched");
let seq = m.value_of("seq").unwrap();
trace!(r#"seq: "{}""#, seq);
let tpl = TplOverride {
subject: m.value_of("subject"),
from: m.values_of("from").map(|v| v.collect()),
to: m.values_of("to").map(|v| v.collect()),
cc: m.values_of("cc").map(|v| v.collect()),
bcc: m.values_of("bcc").map(|v| v.collect()),
headers: m.values_of("headers").map(|v| v.collect()),
body: m.value_of("body"),
sig: m.value_of("signature"),
};
trace!(r#"template args: "{:?}""#, tpl);
debug!("sequence: {}", seq);
let tpl = TplOverride::from(m);
trace!("template args: {:?}", tpl);
return Ok(Some(Command::Forward(seq, tpl)));
}
if let Some(m) = m.subcommand_matches("save") {
info!("save command 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();
trace!("template: {}", tpl);
return Ok(Some(Command::Save(attachment_paths, tpl)));
}
if let Some(m) = m.subcommand_matches("send") {
info!("send command 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();
trace!("template: {}", tpl);
return Ok(Some(Command::Send(attachment_paths, tpl)));
}
Ok(None)
}
@ -154,7 +164,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
)
.subcommand(
SubCommand::with_name("reply")
.aliases(&["rep", "r"])
.aliases(&["rep", "re", "r"])
.about("Generates a reply message template")
.arg(msg_arg::seq_arg())
.arg(msg_arg::reply_all_arg())
@ -166,5 +176,17 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.about("Generates a forward message template")
.arg(msg_arg::seq_arg())
.args(&tpl_args()),
)
.subcommand(
SubCommand::with_name("save")
.about("Saves a message based on the given template")
.arg(&msg_arg::attachment_arg())
.arg(Arg::with_name("template").raw(true)),
)
.subcommand(
SubCommand::with_name("send")
.about("Sends a message based on the given template")
.arg(&msg_arg::attachment_arg())
.arg(Arg::with_name("template").raw(true)),
)]
}

View file

@ -3,12 +3,19 @@
//! This module gathers all message template commands.
use anyhow::Result;
use atty::Stream;
use imap::types::Flag;
use std::{
convert::{TryFrom, TryInto},
io::{self, BufRead},
};
use crate::{
config::Account,
domain::{
imap::ImapServiceInterface,
msg::{Msg, TplOverride},
Flags, Mbox, SmtpServiceInterface,
},
output::PrinterService,
};
@ -53,3 +60,59 @@ pub fn forward<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a
.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,
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: Vec<u8> = TryInto::try_into(&msg)?;
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,
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(&msg)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(mbox, &sent_msg.formatted(), flags)?;
printer.print("Template successfully sent")
}

View file

@ -176,6 +176,12 @@ fn main() -> Result<()> {
Some(tpl_arg::Command::Forward(seq, tpl)) => {
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);
}
Some(tpl_arg::Command::Send(atts, tpl)) => {
return tpl_handler::send(&mbox, atts, tpl, &mut printer, &mut imap, &mut smtp);
}
_ => (),
},
_ => (),

View file

@ -155,6 +155,16 @@ nmap gD <plug>(himalaya-msg-delete)
![gif](https://user-images.githubusercontent.com/10437171/110708795-84387900-81fb-11eb-8f8a-f7e7862e816d.gif)
| Function | Default binding |
| --- | --- |
| Add attachment | `ga` |
They can be customized:
```vim
nmap ga <plug>(himalaya-msg-add-attachment)
```
When you exit this special buffer, you will be prompted 4 choices:
- `Send`: sends the message

View file

@ -5,6 +5,7 @@ let s:plain_req = function("himalaya#request#plain")
let s:msg_id = 0
let s:draft = ""
let s:attachment_paths = []
function! himalaya#msg#list_with(account, mbox, page, should_throw)
let pos = getpos(".")
@ -254,6 +255,7 @@ endfunction
function! himalaya#msg#draft_handle()
try
let account = himalaya#account#curr()
let attachments = join(map(s:attachment_paths, "'--attachment '.v:val"), " ")
while 1
let choice = input("(s)end, (d)raft, (q)uit or (c)ancel? ")
let choice = tolower(choice)[0]
@ -261,15 +263,15 @@ function! himalaya#msg#draft_handle()
if choice == "s"
return s:cli(
\"--account %s send -- %s",
\[shellescape(account), shellescape(s:draft)],
\"--account %s template send %s -- %s",
\[shellescape(account), attachments, shellescape(s:draft)],
\"Sending message",
\0,
\)
elseif choice == "d"
return s:cli(
\"--account %s --mailbox Drafts save -- %s",
\[shellescape(account), shellescape(s:draft)],
\"--account %s --mailbox Drafts template save %s -- %s",
\[shellescape(account), attachments, shellescape(s:draft)],
\"Saving draft",
\0,
\)
@ -336,6 +338,21 @@ function! himalaya#msg#complete_contact(findstart, base)
endtry
endfunction
function! himalaya#msg#add_attachment()
try
let attachment_path = input("Attachment path: ", "", "file")
if empty(expand(glob(attachment_path)))
throw "The file does not exist"
endif
call add(s:attachment_paths, attachment_path)
redraw | call himalaya#shared#log#info("Attachment added!")
catch
if !empty(v:exception)
redraw | call himalaya#shared#log#err(v:exception)
endif
endtry
endfunction
" Utils
" https://newbedev.com/get-usable-window-width-in-vim-script

View file

@ -7,6 +7,10 @@ if exists("g:himalaya_complete_contact_cmd")
setlocal completefunc=himalaya#msg#complete_contact
endif
call himalaya#shared#bindings#define([
\["n", "ga", "msg#add_attachment"],
\])
augroup himalaya_write
autocmd! * <buffer>
autocmd BufWriteCmd <buffer> call himalaya#msg#draft_save()