improve msg arg/handlers + tpl + flag

This commit is contained in:
Clément DOUIN 2021-09-16 19:28:08 +02:00
parent bc5f9045ce
commit 54493540b4
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
17 changed files with 1201 additions and 1031 deletions

View file

@ -8,11 +8,11 @@ use log::{debug, trace};
use native_tls::{self, TlsConnector, TlsStream};
use std::{collections::HashSet, convert::TryFrom, iter::FromIterator, net::TcpStream};
use crate::{
domain::{
account::entity::Account, config::entity::Config, mbox::entity::Mbox, msg::entity::Msg,
},
flag::model::Flags,
use crate::domain::{
account::entity::Account,
config::entity::Config,
mbox::entity::Mbox,
msg::{entity::Msg, flag::entity::Flags},
};
type ImapSession = imap::Session<TlsStream<TcpStream>>;

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,7 @@ use mailparse;
use super::{attachment::Attachment, body::Body, headers::Headers};
use crate::{
domain::account::entity::Account,
flag::model::Flags,
domain::{account::entity::Account, msg::flag::entity::Flags},
ui::table::{Cell, Row, Table},
};

View file

@ -0,0 +1,81 @@
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::debug;
use crate::domain::msg;
type Uid<'a> = &'a str;
type Flags<'a> = &'a str;
/// Enumeration of all possible matches.
pub enum Match<'a> {
Set(Uid<'a>, Flags<'a>),
Add(Uid<'a>, Flags<'a>),
Remove(Uid<'a>, Flags<'a>),
}
/// Message flag arg matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Match<'a>>> {
if let Some(m) = m.subcommand_matches("set") {
debug!("set command matched");
let uid = m.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags = m.value_of("flags").unwrap();
debug!("flags: {}", flags);
return Ok(Some(Match::Set(uid, flags)));
}
if let Some(m) = m.subcommand_matches("add") {
debug!("add command matched");
let uid = m.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags = m.value_of("flags").unwrap();
debug!("flags: {}", flags);
return Ok(Some(Match::Add(uid, flags)));
}
if let Some(m) = m.subcommand_matches("remove") {
debug!("remove command matched");
let uid = m.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags = m.value_of("flags").unwrap();
debug!("flags: {}", flags);
return Ok(Some(Match::Remove(uid, flags)));
}
Ok(None)
}
/// Message flag arg.
fn flags_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("flags")
.help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11). Just write the flag name without the backslash. Example: --flags \"Seen Answered\"")
.value_name("FLAGS…")
.multiple(true)
.required(true)
}
/// Message flag subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("flags")
.about("Handles flags")
.subcommand(
SubCommand::with_name("set")
.about("Replaces all message flags")
.arg(msg::arg::uid_arg())
.arg(flags_arg()),
)
.subcommand(
SubCommand::with_name("add")
.about("Appends flags to a message")
.arg(msg::arg::uid_arg())
.arg(flags_arg()),
)
.subcommand(
SubCommand::with_name("remove")
.aliases(&["rm"])
.about("Removes flags from a message")
.arg(msg::arg::uid_arg())
.arg(flags_arg()),
)]
}

View file

@ -205,8 +205,7 @@ fn convert_to_static<'func>(flag: &'func Flag) -> Result<Flag<'static>, ()> {
#[cfg(test)]
mod tests {
use crate::flag::model::Flags;
use crate::domain::msg::flag::entity::Flags;
use imap::types::Flag;
use std::collections::HashSet;

View file

@ -0,0 +1,36 @@
use anyhow::Result;
use crate::domain::{imap::service::ImapServiceInterface, msg::flag::entity::Flags};
pub fn set<ImapService: ImapServiceInterface>(
uid: &str,
flags: &str,
imap: &mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.set_flags(uid, flags)?;
imap.logout()?;
Ok(())
}
pub fn add<ImapService: ImapServiceInterface>(
uid: &str,
flags: &str,
imap: &mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.add_flags(uid, flags)?;
imap.logout()?;
Ok(())
}
pub fn remove<ImapService: ImapServiceInterface>(
uid: &str,
flags: &str,
imap: &mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.remove_flags(uid, flags)?;
imap.logout()?;
Ok(())
}

View file

@ -0,0 +1,5 @@
//! Module related to messages flag.
pub mod arg;
pub mod entity;
pub mod handler;

430
src/domain/msg/handler.rs Normal file
View file

@ -0,0 +1,430 @@
use anyhow::{Context, Result};
use atty::Stream;
use imap::types::Flag;
use lettre::message::header::ContentTransferEncoding;
use log::{debug, error, trace};
use std::{
borrow::Cow,
convert::TryFrom,
fs,
io::{self, BufRead},
};
use url::Url;
use crate::{
domain::{
account::entity::Account,
imap::service::ImapServiceInterface,
mbox::entity::Mbox,
msg::{
body::Body,
entity::{Msg, Msgs},
},
smtp::service::SmtpServiceInterface,
},
input,
output::service::{OutputService, OutputServiceInterface},
};
use super::{entity::MsgSerialized, flag::entity::Flags, headers::Headers};
// TODO: move this function to the right folder
fn msg_interaction<ImapService: ImapServiceInterface, SmtpService: SmtpServiceInterface>(
output: &OutputService,
msg: &mut Msg,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<bool> {
// let the user change the body a little bit first, before opening the prompt
msg.edit_body()?;
loop {
match input::post_edit_choice() {
Ok(choice) => match choice {
input::PostEditChoice::Send => {
debug!("sending message…");
// prepare the msg to be send
let sendable = match msg.to_sendable_msg() {
Ok(sendable) => sendable,
// In general if an error occured, then this is normally
// due to a missing value of a header. So let's give the
// user another try and give him/her the chance to fix
// that :)
Err(err) => {
println!("{}", err);
println!("Please reedit your msg to make it to a sendable message!");
continue;
}
};
smtp.send(&sendable)?;
// TODO: Gmail sent mailboxes are called `[Gmail]/Sent`
// which creates a conflict, fix this!
// let the server know, that the user sent a msg
msg.flags.insert(Flag::Seen);
let mbox = Mbox::from("Sent");
imap.append_msg(&mbox, msg)?;
// remove the draft, since we sent it
input::remove_draft()?;
output.print("Message successfully sent")?;
break;
}
// edit the body of the msg
input::PostEditChoice::Edit => {
// Did something goes wrong when the user changed the
// content?
if let Err(err) = msg.edit_body() {
println!("[ERROR] {}", err);
println!(concat!(
"Please try to fix the problem by editing",
"the msg again."
));
}
}
input::PostEditChoice::LocalDraft => break,
input::PostEditChoice::RemoteDraft => {
debug!("saving to draft…");
msg.flags.insert(Flag::Seen);
let mbox = Mbox::from("Drafts");
match imap.append_msg(&mbox, msg) {
Ok(_) => {
input::remove_draft()?;
output.print("Message successfully saved to Drafts")?;
}
Err(err) => {
output.print("Cannot save draft to the server")?;
return Err(err.into());
}
};
break;
}
input::PostEditChoice::Discard => {
input::remove_draft()?;
break;
}
},
Err(err) => error!("{}", err),
}
}
Ok(true)
}
pub fn attachments<ImapService: ImapServiceInterface>(
uid: &str,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let msg = imap.get_msg(&uid)?;
let attachments = msg.attachments.clone();
debug!(
"{} attachment(s) found for message {}",
&attachments.len(),
&uid
);
// Iterate through all attachments and download them to the download
// directory of the account.
for attachment in &attachments {
let filepath = account.downloads_dir.join(&attachment.filename);
debug!("downloading {}…", &attachment.filename);
fs::write(&filepath, &attachment.body_raw)
.context(format!("cannot save attachment {:?}", filepath))?;
}
debug!(
"{} attachment(s) successfully downloaded",
&attachments.len()
);
output.print(format!(
"{} attachment(s) successfully downloaded",
&attachments.len()
))?;
imap.logout()?;
Ok(())
}
pub fn copy<ImapService: ImapServiceInterface>(
uid: &str,
mbox: Option<&str>,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let target = Mbox::try_from(mbox)?;
let mut msg = imap.get_msg(&uid)?;
// the message, which will be in the new mailbox doesn't need to be seen
msg.flags.insert(Flag::Seen);
imap.append_msg(&target, &mut msg)?;
debug!("message {} successfully copied to folder `{}`", uid, target);
output.print(format!(
"Message {} successfully copied to folder `{}`",
uid, target
))?;
imap.logout()?;
Ok(())
}
pub fn delete<ImapService: ImapServiceInterface>(
uid: &str,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let flags = vec![Flag::Seen, Flag::Deleted];
imap.add_flags(uid, Flags::from(flags))?;
imap.expunge()?;
debug!("message {} successfully deleted", uid);
output.print(format!("Message {} successfully deleted", uid))?;
imap.logout()?;
Ok(())
}
pub fn forward<ImapService: ImapServiceInterface, SmtpService: SmtpServiceInterface>(
uid: &str,
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
// prepare to forward it
msg.change_to_forwarding(&account);
attachments_paths
.iter()
.for_each(|path| msg.add_attachment(path));
debug!("found {} attachments", attachments_paths.len());
trace!("attachments: {:?}", attachments_paths);
// apply changes
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}
pub fn list<ImapService: ImapServiceInterface>(
page_size: Option<usize>,
page: usize,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
let msgs = imap.list_msgs(&page_size, &page)?;
let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches)?
} else {
Msgs::new()
};
trace!("messages: {:#?}", msgs);
output.print(msgs)?;
imap.logout()?;
Ok(())
}
pub fn mailto<ImapService: ImapServiceInterface, SmtpService: SmtpServiceInterface>(
url: &Url,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut cc = Vec::new();
let mut bcc = Vec::new();
let mut subject = Cow::default();
let mut body = Cow::default();
for (key, val) in url.query_pairs() {
match key.as_bytes() {
b"cc" => {
cc.push(val.into());
}
b"bcc" => {
bcc.push(val.into());
}
b"subject" => {
subject = val;
}
b"body" => {
body = val;
}
_ => (),
}
}
let headers = Headers {
from: vec![account.address()],
to: vec![url.path().to_string()],
encoding: ContentTransferEncoding::Base64,
bcc: Some(bcc),
cc: Some(cc),
signature: Some(account.signature.to_owned()),
subject: Some(subject.into()),
..Headers::default()
};
let mut msg = Msg::new_with_headers(&account, headers);
msg.body = Body::new_with_text(body);
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}
pub fn move_<ImapService: ImapServiceInterface>(
uid: &str,
mbox: Option<&str>,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let target = Mbox::try_from(mbox)?;
let mut msg = imap.get_msg(&uid)?;
// create the msg in the target-msgbox
msg.flags.insert(Flag::Seen);
imap.append_msg(&target, &mut msg)?;
debug!("message {} successfully moved to folder `{}`", uid, target);
output.print(format!(
"Message {} successfully moved to folder `{}`",
uid, target
))?;
// delete the msg in the old mailbox
let flags = vec![Flag::Seen, Flag::Deleted];
imap.add_flags(uid, Flags::from(flags))?;
imap.expunge()?;
imap.logout()?;
Ok(())
}
pub fn read<ImapService: ImapServiceInterface>(
uid: &str,
// TODO: use the mime to select the right body
_mime: String,
raw: bool,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let msg = imap.get_msg(&uid)?;
if raw {
output.print(msg.get_raw_as_string()?)?;
} else {
output.print(MsgSerialized::try_from(&msg)?)?;
}
imap.logout()?;
Ok(())
}
pub fn reply<ImapService: ImapServiceInterface, SmtpService: SmtpServiceInterface>(
uid: &str,
all: bool,
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
// Change the msg to a reply-msg.
msg.change_to_reply(&account, all)?;
// Apply the given attachments to the reply-msg.
attachments_paths
.iter()
.for_each(|path| msg.add_attachment(path));
debug!("found {} attachments", attachments_paths.len());
trace!("attachments: {:#?}", attachments_paths);
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}
pub fn save<ImapService: ImapServiceInterface>(
mbox: Option<&str>,
msg: &str,
imap: &mut ImapService,
) -> Result<()> {
let mbox = Mbox::try_from(mbox)?;
let mut msg = Msg::try_from(msg)?;
msg.flags.insert(Flag::Seen);
imap.append_msg(&mbox, &mut msg)?;
imap.logout()?;
Ok(())
}
pub fn search<ImapService: ImapServiceInterface>(
page_size: Option<usize>,
page: usize,
query: String,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
let msgs = imap.search_msgs(&query, &page_size, &page)?;
let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches)?
} else {
Msgs::new()
};
trace!("messages: {:?}", msgs);
output.print(msgs)?;
imap.logout()?;
Ok(())
}
pub fn send<ImapService: ImapServiceInterface, SmtpService: SmtpServiceInterface>(
msg: &str,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let msg = if atty::is(Stream::Stdin) || output.is_json() {
msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(|ln| ln.ok())
.map(|ln| ln.to_string())
.collect::<Vec<String>>()
.join("\r\n")
};
let mut msg = Msg::try_from(msg.as_str())?;
// send the message/msg
let sendable = msg.to_sendable_msg()?;
smtp.send(&sendable)?;
debug!("message sent!");
// add the message/msg to the Sent-Mailbox of the user
msg.flags.insert(Flag::Seen);
let mbox = Mbox::from("Sent");
imap.append_msg(&mbox, &mut msg)?;
imap.logout()?;
Ok(())
}
pub fn write<ImapService: ImapServiceInterface, SmtpService: SmtpServiceInterface>(
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut msg = Msg::new_with_headers(
&account,
Headers {
subject: Some(String::new()),
to: Vec::new(),
..Headers::default()
},
);
attachments_paths
.iter()
.for_each(|path| msg.add_attachment(path));
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}

View file

@ -35,3 +35,6 @@ pub mod headers;
/// This module is used in the `Msg` struct, which should represent the body of
/// a msg; The part where you're writing some text like `Dear Mr. LMAO`.
pub mod body;
pub mod flag;
pub mod handler;
pub mod tpl;

120
src/domain/msg/tpl/arg.rs Normal file
View file

@ -0,0 +1,120 @@
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::debug;
use crate::domain::msg::{self, arg::uid_arg};
type Uid<'a> = &'a str;
type All = bool;
/// Enumeration of all possible matches.
pub enum Match<'a> {
New,
Reply(Uid<'a>, All),
Forward(Uid<'a>),
}
/// Message template arg matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Match<'a>>> {
if let Some(_) = m.subcommand_matches("new") {
debug!("new command matched");
return Ok(Some(Match::New));
}
if let Some(m) = m.subcommand_matches("reply") {
debug!("reply command matched");
let uid = m.value_of("uid").unwrap();
debug!("uid: {}", uid);
let all = m.is_present("reply-all");
debug!("reply all: {}", all);
return Ok(Some(Match::Reply(uid, all)));
}
if let Some(m) = m.subcommand_matches("forward") {
debug!("forward command matched");
let uid = m.value_of("uid").unwrap();
debug!("uid: {}", uid);
return Ok(Some(Match::Forward(uid)));
}
Ok(None)
}
/// Message template args.
pub fn tpl_args<'a>() -> Vec<Arg<'a, 'a>> {
vec![
Arg::with_name("subject")
.help("Overrides the Subject header")
.short("s")
.long("subject")
.value_name("STRING"),
Arg::with_name("from")
.help("Overrides the From header")
.short("f")
.long("from")
.value_name("ADDR")
.multiple(true),
Arg::with_name("to")
.help("Overrides the To header")
.short("t")
.long("to")
.value_name("ADDR")
.multiple(true),
Arg::with_name("cc")
.help("Overrides the Cc header")
.short("c")
.long("cc")
.value_name("ADDR")
.multiple(true),
Arg::with_name("bcc")
.help("Overrides the Bcc header")
.short("b")
.long("bcc")
.value_name("ADDR")
.multiple(true),
Arg::with_name("header")
.help("Overrides a specific header")
.short("h")
.long("header")
.value_name("KEY: VAL")
.multiple(true),
Arg::with_name("body")
.help("Overrides the body")
.short("B")
.long("body")
.value_name("STRING"),
Arg::with_name("signature")
.help("Overrides the signature")
.short("S")
.long("signature")
.value_name("STRING"),
]
}
/// Message template subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("template")
.aliases(&["tpl"])
.about("Generates a message template")
.subcommand(
SubCommand::with_name("new")
.aliases(&["n"])
.about("Generates a new message template")
.args(&tpl_args()),
)
.subcommand(
SubCommand::with_name("reply")
.aliases(&["rep", "r"])
.about("Generates a reply message template")
.arg(uid_arg())
.arg(msg::arg::reply_all_arg())
.args(&tpl_args()),
)
.subcommand(
SubCommand::with_name("forward")
.aliases(&["fwd", "fw", "f"])
.about("Generates a forward message template")
.arg(uid_arg())
.args(&tpl_args()),
)]
}

View file

@ -0,0 +1,150 @@
use std::convert::TryFrom;
use anyhow::Result;
use log::trace;
use crate::{
domain::{
account::entity::Account,
imap::service::ImapServiceInterface,
msg::entity::{Msg, MsgSerialized},
},
output::service::OutputServiceInterface,
};
pub fn new<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let msg = Msg::new(&account);
// FIXME
// override_msg_with_args(&mut msg, &matches);
trace!("message: {:#?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
Ok(())
}
pub fn reply<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
all: bool,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
msg.change_to_reply(&account, all)?;
// FIXME
// override_msg_with_args(&mut msg, &matches);
trace!("Message: {:?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
Ok(())
}
pub fn forward<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
msg.change_to_forwarding(&account);
// FIXME
// override_msg_with_args(&mut msg, &matches);
trace!("Message: {:?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
Ok(())
}
// == Helper functions ==
// -- Template Subcommands --
// These functions are more used for the "template" subcommand
// fn override_msg_with_args(msg: &mut Msg) {
// // -- Collecting credentials --
// let from: Vec<String> = match matches.values_of("from") {
// Some(from) => from.map(|arg| arg.to_string()).collect(),
// None => msg.headers.from.clone(),
// };
// let to: Vec<String> = match matches.values_of("to") {
// Some(to) => to.map(|arg| arg.to_string()).collect(),
// None => Vec::new(),
// };
// let subject = matches
// .value_of("subject")
// .and_then(|subject| Some(subject.to_string()));
// let cc: Option<Vec<String>> = matches
// .values_of("cc")
// .and_then(|cc| Some(cc.map(|arg| arg.to_string()).collect()));
// let bcc: Option<Vec<String>> = matches
// .values_of("bcc")
// .and_then(|bcc| Some(bcc.map(|arg| arg.to_string()).collect()));
// let signature = matches
// .value_of("signature")
// .and_then(|signature| Some(signature.to_string()))
// .or(msg.headers.signature.clone());
// let custom_headers: Option<HashMap<String, Vec<String>>> = {
// if let Some(matched_headers) = matches.values_of("header") {
// let mut custom_headers: HashMap<String, Vec<String>> = HashMap::new();
// // collect the custom headers
// for header in matched_headers {
// let mut header = header.split(":");
// let key = header.next().unwrap_or_default();
// let val = header.next().unwrap_or_default().trim_start();
// debug!("overriden header: {}={}", key, val);
// custom_headers.insert(key.to_string(), vec![val.to_string()]);
// }
// Some(custom_headers)
// } else {
// None
// }
// };
// let body = {
// if atty::isnt(Stream::Stdin) {
// let body = io::stdin()
// .lock()
// .lines()
// .filter_map(|line| line.ok())
// .map(|line| line.to_string())
// .collect::<Vec<String>>()
// .join("\n");
// debug!("overriden body from stdin: {:?}", body);
// body
// } else if let Some(body) = matches.value_of("body") {
// debug!("overriden body: {:?}", body);
// body.to_string()
// } else {
// String::new()
// }
// };
// let body = Body::new_with_text(body);
// // -- Creating and printing --
// let headers = Headers {
// from,
// subject,
// to,
// cc,
// bcc,
// signature,
// custom_headers,
// ..msg.headers.clone()
// };
// msg.headers = headers;
// msg.body = body;
// }

View file

@ -0,0 +1,4 @@
//! Module related to messages template.
pub mod arg;
pub mod handler;

View file

@ -1,93 +0,0 @@
use anyhow::Result;
use clap;
use log::debug;
use crate::{
domain::{imap::service::ImapServiceInterface, msg::arg::uid_arg},
flag::model::Flags,
};
fn flags_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("flags")
.help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11). Just write the flag name without the backslash. Example: --flags \"Seen Answered\"")
.value_name("FLAGS…")
.multiple(true)
.required(true)
}
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![clap::SubCommand::with_name("flags")
.about("Handles flags")
.subcommand(
clap::SubCommand::with_name("set")
.about("Replaces all message flags")
.arg(uid_arg())
.arg(flags_arg()),
)
.subcommand(
clap::SubCommand::with_name("add")
.about("Appends flags to a message")
.arg(uid_arg())
.arg(flags_arg()),
)
.subcommand(
clap::SubCommand::with_name("remove")
.aliases(&["rm"])
.about("Removes flags from a message")
.arg(uid_arg())
.arg(flags_arg()),
)]
}
pub fn matches<ImapService: ImapServiceInterface>(
arg_matches: &clap::ArgMatches,
imap: &mut ImapService,
) -> Result<bool> {
if let Some(matches) = arg_matches.subcommand_matches("set") {
debug!("set command matched");
let uid = matches.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags = matches.value_of("flags").unwrap();
debug!("flags: {}", flags);
let flags = Flags::from(flags);
imap.set_flags(uid, flags)?;
imap.logout()?;
return Ok(true);
}
if let Some(matches) = arg_matches.subcommand_matches("add") {
debug!("add command matched");
let uid = matches.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags = matches.value_of("flags").unwrap();
debug!("flags: {}", flags);
let flags = Flags::from(flags);
imap.add_flags(uid, flags)?;
imap.logout()?;
return Ok(true);
}
if let Some(matches) = arg_matches.subcommand_matches("remove") {
debug!("remove command matched");
let uid = matches.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags = matches.value_of("flags").unwrap();
debug!("flags: {}", flags);
let flags = Flags::from(flags);
imap.remove_flags(uid, flags)?;
imap.logout()?;
return Ok(true);
}
debug!("nothing matched");
Ok(false)
}

View file

@ -1,2 +0,0 @@
pub mod arg;
pub mod model;

View file

@ -17,10 +17,6 @@ pub mod compl;
/// Everything which is related to the config files. For example the structure of your config file.
pub mod config;
/// A wrapper for representing a flag of a message or mailbox. For example the delete-flag or
/// read-flag.
pub mod flag;
/// Handles the input-interaction with the user. For example if you want to edit the body of your
/// message, his module takes care of the draft and calls your ~(neo)vim~ your favourite editor.
pub mod input;

View file

@ -2,6 +2,7 @@ use anyhow::Result;
use clap;
use env_logger;
use std::{convert::TryFrom, env};
use url::Url;
use himalaya::{
compl,
@ -14,7 +15,6 @@ use himalaya::{
msg,
smtp::service::SmtpService,
},
flag,
output::{cli::output_args, service::OutputService},
};
@ -26,11 +26,10 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
.args(&output_args())
.args(&config_args())
.arg(mbox::arg::source_arg())
.subcommands(flag::arg::subcmds())
.subcommands(compl::arg::subcmds())
.subcommands(imap::arg::subcmds())
.subcommands(mbox::arg::subcmds())
.subcommands(msg::arg::subcmds())
.subcommands(compl::arg::subcmds())
}
fn main() -> Result<()> {
@ -39,19 +38,18 @@ fn main() -> Result<()> {
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"),
);
// TODO: put in a `mailto` module
// let raw_args: Vec<String> = env::args().collect();
// if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
// let config = Config::new(None)?;
// let account = config.find_account_by_name(None)?.clone();
// let output = Output::new("plain");
// let mbox = "INBOX";
// let arg_matches = ArgMatches::default();
// let app = Ctx::new(config, output, mbox, arg_matches);
// let url = Url::parse(&raw_args[1])?;
// let smtp = domain::smtp::service::SMTPService::new(&app.account);
// return Ok(msg_matches_mailto(&app, &url, smtp)?);
// }
// Check mailto match BEFORE app initialization.
let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let mbox = Mbox::from("INBOX");
let config = Config::try_from(None)?;
let account = Account::try_from((&config, None))?;
let output = OutputService::from("plain");
let url = Url::parse(&raw_args[1])?;
let mut imap = ImapService::from((&account, &mbox));
let mut smtp = SmtpService::from(&account);
return msg::handler::mailto(&url, &account, &output, &mut imap, &mut smtp);
}
let app = create_app();
let m = app.get_matches();
@ -91,9 +89,69 @@ fn main() -> Result<()> {
_ => (),
}
// TODO: use same system as compl
let _matched = flag::arg::matches(&m, &mut imap)?
|| msg::arg::matches(&m, &mbox, &account, &output, &mut imap, &mut smtp)?;
// Check message matches.
match msg::arg::matches(&m)? {
Some(msg::arg::Match::Attachments(uid)) => {
return msg::handler::attachments(uid, &account, &output, &mut imap);
}
Some(msg::arg::Match::Copy(uid, mbox)) => {
return msg::handler::copy(uid, mbox, &output, &mut imap);
}
Some(msg::arg::Match::Delete(uid)) => {
return msg::handler::delete(uid, &output, &mut imap);
}
Some(msg::arg::Match::Forward(uid, paths)) => {
return msg::handler::forward(uid, paths, &account, &output, &mut imap, &mut smtp);
}
Some(msg::arg::Match::List(page_size, page)) => {
return msg::handler::list(page_size, page, &account, &output, &mut imap);
}
Some(msg::arg::Match::Move(uid, mbox)) => {
return msg::handler::move_(uid, mbox, &output, &mut imap);
}
Some(msg::arg::Match::Read(uid, mime, raw)) => {
return msg::handler::read(uid, mime, raw, &output, &mut imap);
}
Some(msg::arg::Match::Reply(uid, all, paths)) => {
return msg::handler::reply(uid, all, paths, &account, &output, &mut imap, &mut smtp);
}
Some(msg::arg::Match::Save(mbox, msg)) => {
return msg::handler::save(mbox, msg, &mut imap);
}
Some(msg::arg::Match::Search(query, page_size, page)) => {
return msg::handler::search(page_size, page, query, &account, &output, &mut imap);
}
Some(msg::arg::Match::Send(msg)) => {
return msg::handler::send(msg, &output, &mut imap, &mut smtp);
}
Some(msg::arg::Match::Write(paths)) => {
return msg::handler::write(paths, &account, &output, &mut imap, &mut smtp);
}
Some(msg::arg::Match::Flag(m)) => match m {
msg::flag::arg::Match::Set(uid, flags) => {
return msg::flag::handler::set(uid, flags, &mut imap);
}
msg::flag::arg::Match::Add(uid, flags) => {
return msg::flag::handler::add(uid, flags, &mut imap);
}
msg::flag::arg::Match::Remove(uid, flags) => {
return msg::flag::handler::remove(uid, flags, &mut imap);
}
},
Some(msg::arg::Match::Tpl(m)) => match m {
msg::tpl::arg::Match::New => {
return msg::tpl::handler::new(&account, &output, &mut imap);
}
msg::tpl::arg::Match::Reply(uid, all) => {
return msg::tpl::handler::reply(uid, all, &account, &output, &mut imap);
}
msg::tpl::arg::Match::Forward(uid) => {
return msg::tpl::handler::forward(uid, &account, &output, &mut imap);
}
},
_ => (),
}
Ok(())
}

View file

@ -12,6 +12,15 @@ pub enum OutputFmt {
Json,
}
impl From<&str> for OutputFmt {
fn from(fmt: &str) -> Self {
match fmt {
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
_ => Self::Plain,
}
}
}
impl TryFrom<Option<&str>> for OutputFmt {
type Error = Error;
@ -99,6 +108,15 @@ impl Default for OutputService {
}
}
impl From<&str> for OutputService {
fn from(fmt: &str) -> Self {
debug!("init output service");
debug!("output: `{:?}`", fmt);
let fmt = fmt.into();
Self { fmt }
}
}
impl TryFrom<Option<&str>> for OutputService {
type Error = Error;