ui: add mailcap support

jmap
Manos Pitsidianakis 2019-11-11 22:20:16 +02:00
parent 9cd00cf53a
commit dce1c39b48
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
5 changed files with 428 additions and 28 deletions

77
meli.1
View File

@ -24,7 +24,7 @@
.Nm meli
.Nd Meli Mail User Agent. meli is the Greek word for honey.
.Sh SYNOPSIS
.Nm meli
.Nm
.Op Fl -help | h
.Op Fl -version | v
.Op Fl -create-config Op Ar path
@ -45,7 +45,9 @@ if given, or at
Start meli with given configuration file.
.El
.Sh STARTING WITH meli
When launched for the first time, meli will search for its configuration directory,
When launched for the first time,
.Nm
will search for its configuration directory,
.Pa $XDG_CONFIG_HOME/meli/ Ns
\&. If it doesn't exist, you will be asked if you want to create one along with a sample configuration. The sample configuration
.Pa $XDG_CONFIG_HOME/meli/config
@ -83,6 +85,21 @@ section of your configuration.
^^ (`-=-=-=-=-`)
`-=-=-=-=-` ^^
.Ed
.Sh VIEWING MAIL
Open attachments by typing their index in the attachments list and then
.Cm a Ns
\&.
.Ns
.Nm
will attempt to open text inside its pager and other content via
.Cm xdg-open Ns
\&. Press
.Cm m
instead to use the mailcap entry for the MIME type of the attachment, if any. See
.Sx FILES
for the location of the mailcap files and
.Xr mailcap 5
for their syntax.
.Sh COMPOSING
To send mail, press
.Cm m
@ -112,7 +129,9 @@ will send your message by piping it into a binary of your choosing (see
.Cm close
and select 'save as draft'.
.Pp
If there is no Draft or Sent folder, meli tries first saving mail in your INBOX and then at any other folder. On complete failure to save your draft or sent message it will be saved in your
If there is no Draft or Sent folder,
.Nm
tries first saving mail in your INBOX and then at any other folder. On complete failure to save your draft or sent message it will be saved in your
.Em tmp
directory instead and you will be notified of its location.
.Pp
@ -120,7 +139,8 @@ To open a draft for editing later, select your draft in the mail listing and pre
.Cm e Ns
\&.
.Sh SEARCH
meli, if built with sqlite3, includes the ability to perform full text search on the following fields: From, To, Cc, Bcc, In-Reply-To, References, Subject and Date. The message body (in plain text human readable form) and the flags can also be queried. To create the sqlite3 index issue command
.Nm Ns
, if built with sqlite3, includes the ability to perform full text search on the following fields: From, To, Cc, Bcc, In-Reply-To, References, Subject and Date. The message body (in plain text human readable form) and the flags can also be queried. To create the sqlite3 index issue command
.Ic index Ar ACCOUNT_NAME Ns \&.
To search in the message body type your keywords without any special formatting.
@ -150,7 +170,9 @@ To prevent downloading all your messages from your IMAP server, don't set
.Em cache_type
to
.Em sqlite3 Ns
\&. meli will relay your queries to the IMAP server. Expect a delay between query and response. Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
\&.
.Nm
will relay your queries to the IMAP server. Expect a delay between query and response. Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
.Sh EXECUTE mode
Commands are issued in EXECUTE mode, by default started with the space character and exited with Escape key.
.Pp
@ -198,7 +220,7 @@ delete folder
.El
.Pp
envelope view commands:
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST" -offset indent
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST"
.It Cm pipe Ar EXECUTABLE Ar ARGS
pipe pager contents to binary
.It Cm list-post
@ -211,7 +233,7 @@ open list archive with
.El
.Pp
composing mail commands:
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST" -offset indent
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST"
.It Ic add-attachment Ar PATH
in composer, add
.Ar PATH
@ -223,7 +245,7 @@ toggle between signing and not signing this message. If the gpg invocation fails
.El
.Pp
generic commands:
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST" -offset indent
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST"
.It Cm open-in-tab
opens envelope view in new tab
.It Ic close
@ -239,7 +261,7 @@ print environment variable
.El
.Sh SHORTCUTS
Non-complete list of shortcuts and their default values.
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST" -offset indent
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST"
.It Cm open_thread
\&'\\n'
.It Cm exit_thread
@ -275,7 +297,7 @@ PageDown
.It Cm select
\&'v'
.El
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST" -offset indent
.Bl -tag -width "rename-folder ACCOUNT FOLDER_PATH_SRC FOLDER_PATH_DEST"
.It Cm `
toggles hiding of sidebar in mail listings
.It Cm \&?
@ -296,6 +318,11 @@ opens the
.Ar n Ns
th
attachment.
.It Ar n Ns Cm m
opens the
.Ar n Ns
th
attachment according to its mailcap entry.
.It Cm v
(un)selects mail entries in mail listings
.El
@ -314,7 +341,8 @@ Specifies the editor to use
Override the configuration file
.El
.Sh FILES
meli uses the following parts of the XDG standard:
.Nm
uses the following parts of the XDG standard:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Ev XDG_CONFIG_HOME
defaults to
@ -343,16 +371,37 @@ Internal data used by meli.
.It Pa $XDG_DATA_HOME/meli/meli.log
Operation log.
.It Pa /tmp/meli/*
Temporary files generated by meli.
Temporary files generated by
.Nm Ns
\&.
.El
.Pp
Mailcap entries are searched for in the following files, in this order:
.Pp
.Bl -enum -compact -offset indent
.It
.Pa $XDG_CONFIG_HOME/meli/mailcap
.It
.Pa $XDG_CONFIG_HOME/.mailcap
.It
.Pa $HOME/.mailcap
.It
.Pa /etc/mailcap
.It
.Pa /usr/etc/mailcap
.It
.Pa /usr/local/etc/mailcap
.El
.Sh SEE ALSO
.Xr meli.conf 5 ,
.Xr xdg-open 1 ,
.Xr meli.conf 5
.Xr mailcap 5
.Sh CONFORMING TO
XDG Standard
.Aq https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
, maildir
.Aq https://cr.yp.to/proto/maildir.html
.Aq https://cr.yp.to/proto/maildir.html Ns
, IMAPv4rev1 RFC3501.
.Sh AUTHORS
Copyright 2017-2019
.An Manos Pitsidianakis Aq epilys@nessuent.xyz

View File

@ -642,6 +642,27 @@ impl Attachment {
into_raw_helper(self, &mut ret);
ret
}
pub fn parameters(&self) -> Vec<(&[u8], &[u8])> {
let mut ret = Vec::new();
let (headers, _) = match parser::attachment(&self.raw).to_full_result() {
Ok(v) => v,
Err(_) => return ret,
};
for (name, value) in headers {
if name.eq_ignore_ascii_case(b"content-type") {
match parser::content_type(value).to_full_result() {
Ok((_, _, params)) => {
ret = params;
}
_ => {}
}
break;
}
}
ret
}
}
pub fn interpret_format_flowed(_t: &str) -> String {

View File

@ -701,8 +701,12 @@ impl Component for MailView {
}
}
let shortcuts = &self.get_shortcuts(context)[MailView::DESCRIPTION];
match *event {
UIEvent::Input(Key::Char('c')) if !self.mode.is_contact_selector() => {
UIEvent::Input(ref key)
if !self.mode.is_contact_selector()
&& *key == shortcuts["add_addresses_to_contacts"] =>
{
let account = &mut context.accounts[self.coordinates.0];
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
@ -746,25 +750,87 @@ impl Component for MailView {
)));
return true;
}
UIEvent::Input(Key::Alt('r'))
if self.mode == ViewMode::Normal || self.mode == ViewMode::Subview =>
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
&& *key == shortcuts["view_raw_source"] =>
{
self.mode = ViewMode::Raw;
self.set_dirty();
return true;
}
UIEvent::Input(Key::Char('r'))
if self.mode.is_attachment()
UIEvent::Input(ref key)
if (self.mode.is_attachment()
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Url
|| self.mode == ViewMode::Raw =>
|| self.mode == ViewMode::Raw)
&& *key == shortcuts["return_to_normal_view"] =>
{
self.mode = ViewMode::Normal;
self.set_dirty();
return true;
}
UIEvent::Input(Key::Char('a'))
if !self.cmd_buf.is_empty()
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
&& !self.cmd_buf.is_empty()
&& *key == shortcuts["open_mailcap"] =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
{
let account = &mut context.accounts[self.coordinates.0];
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
let op = account.operation(envelope.hash());
let attachments = match envelope.body(op) {
Ok(body) => body.attachments(),
Err(e) => {
context.replies.push_back(UIEvent::Notification(
Some("Failed to open e-mail".to_string()),
e.to_string(),
Some(NotificationType::ERROR),
));
log(
format!(
"Failed to open envelope {}: {}",
envelope.message_id_display(),
e.to_string()
),
ERROR,
);
return true;
}
};
drop(envelope);
drop(account);
if let Some(u) = attachments.get(lidx) {
if let Ok(()) = crate::mailcap::MailcapEntry::execute(u, context) {
self.set_dirty();
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"no mailcap entry found for {}",
u.content_type()
)),
));
}
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Attachment `{}` not found.",
lidx
)),
));
}
return true;
}
}
UIEvent::Input(ref key)
if *key == shortcuts["open_attachment"]
&& !self.cmd_buf.is_empty()
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
@ -895,13 +961,15 @@ impl Component for MailView {
}
};
}
UIEvent::Input(Key::Char('h')) => {
UIEvent::Input(ref key) if *key == shortcuts["toggle_expand_headers"] => {
self.expand_headers = !self.expand_headers;
self.dirty = true;
return true;
}
UIEvent::Input(Key::Char('g'))
if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
UIEvent::Input(ref key)
if !self.cmd_buf.is_empty()
&& self.mode == ViewMode::Url
&& *key == shortcuts["go_to_url"] =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
@ -943,15 +1011,24 @@ impl Component for MailView {
}
};
Command::new("xdg-open")
if let Err(e) = Command::new("xdg-open")
.arg(url)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start xdg_open");
{
context.replies.push_back(UIEvent::Notification(
Some("Failed to launch xdg-open".to_string()),
e.to_string(),
Some(NotificationType::ERROR),
));
}
return true;
}
UIEvent::Input(Key::Char('u')) => {
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
&& *key == shortcuts["toggle_url_mode"] =>
{
match self.mode {
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
@ -1220,12 +1297,14 @@ impl Component for MailView {
our_map.insert("return_to_normal_view", Key::Char('r'));
}
our_map.insert("open_attachment", Key::Char('a'));
our_map.insert("open_mailcap", Key::Char('m'));
if self.mode == ViewMode::Url {
our_map.insert("go_to_url", Key::Char('g'));
}
if self.mode == ViewMode::Normal || self.mode == ViewMode::Url {
our_map.insert("toggle_url_mode", Key::Char('u'));
}
our_map.insert("toggle_expand_headers", Key::Char('h'));
map.insert(MailView::DESCRIPTION.to_string(), our_map);
map

View File

@ -74,6 +74,8 @@ pub mod sqlite3;
pub mod cache;
pub mod mailcap;
pub use crate::username::*;
pub mod username {
use libc;

249
ui/src/mailcap.rs 100644
View File

@ -0,0 +1,249 @@
use crate::split_command;
use crate::state::Context;
use crate::types::{create_temp_file, ForkType, UIEvent};
use fnv::FnvHashMap;
use melib::attachments::decode;
use melib::{email::Attachment, MeliError, Result};
use std::io::Read;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
pub struct MailcapEntry {
command: String,
/* Pass to pager */
copiousoutput: bool,
}
trait GlobMatch {
fn matches_glob(&self, s: &str) -> bool;
}
impl GlobMatch for str {
fn matches_glob(&self, s: &str) -> bool {
let parts = self.split("*");
let mut ptr = 0;
let mut part_no = 0;
for p in parts {
if ptr >= s.len() {
return false;
}
if part_no > 0 {
while !&s[ptr..].starts_with(p) {
ptr += 1;
if ptr >= s.len() {
return false;
}
}
}
if !&s[ptr..].starts_with(p) {
return false;
}
ptr += p.len();
part_no += 1;
}
true
}
}
impl MailcapEntry {
pub fn execute(a: &Attachment, context: &mut Context) -> Result<()> {
/* lookup order:
* $XDG_CONFIG_HOME/meli/mailcap:$XDG_CONFIG_HOME/.mailcap:$HOME/.mailcap:/etc/mailcap:/usr/etc/mailcap:/usr/local/etc/mailcap
*/
let xdg_dirs =
xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?;
let mut mailcap_path = xdg_dirs
.place_config_file("mailcap")
.map_err(|e| MeliError::new(e.to_string()))?;
if !mailcap_path.exists() {
mailcap_path = xdg::BaseDirectories::new()
.map_err(|e| MeliError::new(e.to_string()))?
.place_config_file("mailcap")?;
if !mailcap_path.exists() {
if let Ok(home) = std::env::var("HOME") {
mailcap_path = PathBuf::from(format!("{}/.mailcap", home));
}
if !mailcap_path.exists() {
mailcap_path = PathBuf::from("/etc/mailcap");
if !mailcap_path.exists() {
mailcap_path = PathBuf::from("/usr/etc/mailcap");
if !mailcap_path.exists() {
mailcap_path = PathBuf::from("/usr/local/etc/mailcap");
}
if !mailcap_path.exists() {
return Err(MeliError::new("No mailcap file found."));
}
}
}
}
}
let mut hash_map = FnvHashMap::default();
let mut content = String::new();
std::fs::File::open(mailcap_path.as_path())?.read_to_string(&mut content)?;
let content_type = a.content_type().to_string();
let mut result = None;
let mut lines_iter = content.lines();
while let Some(l) = lines_iter.next() {
let l = l.trim();
if l.starts_with("#") {
continue;
}
if l.is_empty() {
continue;
}
if l.ends_with("\\") {
let l = format!("{}{}", &l[..l.len() - 2], lines_iter.next().unwrap());
let mut parts_iter = l.split(";");
let key = parts_iter.next().unwrap();
let cmd = parts_iter.next().unwrap();
//let flags = parts_iter.next().unwrap();
if key.starts_with(&content_type) || key.matches_glob(&content_type) {
let mut copiousoutput = false;
while let Some(flag) = parts_iter.next() {
if flag.trim() == "copiousoutput" {
copiousoutput = true;
} else {
debug!("unknown mailcap flag: {}", flag);
}
}
result = Some(MailcapEntry {
command: cmd.to_string(),
copiousoutput,
});
break;
}
hash_map.insert(key.to_string(), cmd.to_string());
} else {
let mut parts_iter = l.split(";");
let key = parts_iter.next().unwrap();
let cmd = parts_iter.next().unwrap();
//let flags = parts_iter.next().unwrap();
if key.starts_with(&content_type) || key.matches_glob(&content_type) {
let mut copiousoutput = false;
while let Some(flag) = parts_iter.next() {
if flag.trim() == "copiousoutput" {
copiousoutput = true;
} else {
debug!("unknown mailcap flag: {}", flag);
}
}
result = Some(MailcapEntry {
command: cmd.to_string(),
copiousoutput,
});
break;
}
hash_map.insert(key.to_string(), cmd.to_string());
}
}
match result {
None => Err(MeliError::new("Not found".to_string())),
Some(MailcapEntry {
command,
copiousoutput,
}) => {
let parts = split_command!(command);
let (cmd, args) = (parts[0], &parts[1..]);
let mut f = None;
let mut needs_stdin = true;
let params = a.parameters();
/* TODO: See mailcap(5)
* - replace "\%" with "%" and unescape other blackslash uses.
* - "%n" and "%F".
* - test=xxx field.
*/
let args = args
.iter()
.map(|arg| match *arg {
"%s" => {
needs_stdin = false;
let _f = create_temp_file(&decode(a, None), None, None, true);
let p = _f.path().display().to_string();
f = Some(_f);
p
}
"%t" => a.content_type().to_string(),
param if param.starts_with("%{") && param.ends_with("}") => {
let param = &param["%{".len()..param.len() - 1];
if let Some(v) = params.iter().find(|(k, _)| *k == param.as_bytes()) {
String::from_utf8_lossy(v.1).into()
} else if param == "charset" {
String::from("utf-8")
} else {
String::new()
}
}
a => a.to_string(),
})
.collect::<Vec<String>>();
{
context.input_kill();
}
if copiousoutput {
let out = if needs_stdin {
let mut child = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.as_mut().unwrap().write_all(&decode(a, None))?;
child.wait_with_output()?.stdout
} else {
let child = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.wait_with_output()?.stdout
};
let pager_cmd = if let Ok(v) = std::env::var("PAGER") {
std::borrow::Cow::from(v)
} else {
std::borrow::Cow::from("less")
};
let mut pager = Command::new(pager_cmd.as_ref())
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.spawn()?;
pager.stdin.as_mut().unwrap().write_all(&out)?;
debug!(pager.wait_with_output()?.stdout);
} else {
if needs_stdin {
let mut child = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.spawn()?;
child.stdin.as_mut().unwrap().write_all(&decode(a, None))?;
debug!(child.wait_with_output()?.stdout);
} else {
let child = Command::new(cmd)
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.spawn()?;
debug!(child.wait_with_output()?.stdout);
}
}
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
context.restore_input();
Ok(())
}
}
}
}