mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-16 20:57:53 +08:00
373 lines
10 KiB
Rust
373 lines
10 KiB
Rust
//! Module related to message handling.
|
|
//!
|
|
//! This module gathers all message commands.
|
|
|
|
use anyhow::{Context, Result};
|
|
use atty::Stream;
|
|
use log::{debug, info, trace};
|
|
use mailparse::addrparse;
|
|
use std::{
|
|
borrow::Cow,
|
|
convert::TryInto,
|
|
fs,
|
|
io::{self, BufRead},
|
|
};
|
|
use url::Url;
|
|
|
|
use crate::{
|
|
backends::Backend,
|
|
config::{AccountConfig, DEFAULT_SENT_FOLDER},
|
|
msg::{Msg, Part, Parts, TextPlainPart},
|
|
output::{PrintTableOpts, PrinterService},
|
|
smtp::SmtpService,
|
|
};
|
|
|
|
/// Downloads all message attachments to the user account downloads directory.
|
|
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
seq: &str,
|
|
mbox: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
let attachments = backend.get_msg(mbox, seq)?.attachments();
|
|
let attachments_len = attachments.len();
|
|
debug!(
|
|
r#"{} attachment(s) found for message "{}""#,
|
|
attachments_len, seq
|
|
);
|
|
|
|
for attachment in attachments {
|
|
let file_path = config.get_download_file_path(&attachment.filename)?;
|
|
debug!("downloading {}…", attachment.filename);
|
|
fs::write(&file_path, &attachment.content)
|
|
.context(format!("cannot download attachment {:?}", file_path))?;
|
|
}
|
|
|
|
printer.print(format!(
|
|
"{} attachment(s) successfully downloaded to {:?}",
|
|
attachments_len, config.downloads_dir
|
|
))
|
|
}
|
|
|
|
/// Copy a message from a mailbox to another.
|
|
pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
seq: &str,
|
|
mbox_src: &str,
|
|
mbox_dst: &str,
|
|
printer: &mut P,
|
|
backend: Box<&mut B>,
|
|
) -> Result<()> {
|
|
backend.copy_msg(mbox_src, mbox_dst, seq)?;
|
|
printer.print(format!(
|
|
r#"Message {} successfully copied to folder "{}""#,
|
|
seq, mbox_dst
|
|
))
|
|
}
|
|
|
|
/// Delete messages matching the given sequence range.
|
|
pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
seq: &str,
|
|
mbox: &str,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
backend.del_msg(mbox, seq)?;
|
|
printer.print(format!(r#"Message(s) {} successfully deleted"#, seq))
|
|
}
|
|
|
|
/// Forward the given message UID from the selected mailbox.
|
|
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|
seq: &str,
|
|
attachments_paths: Vec<&str>,
|
|
encrypt: bool,
|
|
mbox: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
smtp: &mut S,
|
|
) -> Result<()> {
|
|
backend
|
|
.get_msg(mbox, seq)?
|
|
.into_forward(config)?
|
|
.add_attachments(attachments_paths)?
|
|
.encrypt(encrypt)
|
|
.edit_with_editor(config, printer, backend, smtp)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// List paginated messages from the selected mailbox.
|
|
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
max_width: Option<usize>,
|
|
page_size: Option<usize>,
|
|
page: usize,
|
|
mbox: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
imap: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
let page_size = page_size.unwrap_or(config.default_page_size);
|
|
debug!("page size: {}", page_size);
|
|
let msgs = imap.get_envelopes(mbox, page_size, page)?;
|
|
trace!("envelopes: {:?}", msgs);
|
|
printer.print_table(
|
|
msgs,
|
|
PrintTableOpts {
|
|
format: &config.format,
|
|
max_width,
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Parses and edits a message from a [mailto] URL string.
|
|
///
|
|
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
|
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|
url: &Url,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
smtp: &mut S,
|
|
) -> Result<()> {
|
|
info!("entering mailto command handler");
|
|
|
|
let to = addrparse(url.path())?;
|
|
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.to_string());
|
|
}
|
|
b"bcc" => {
|
|
bcc.push(val.to_string());
|
|
}
|
|
b"subject" => {
|
|
subject = val;
|
|
}
|
|
b"body" => {
|
|
body = val;
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
let msg = Msg {
|
|
from: Some(vec![config.address()?].into()),
|
|
to: if to.is_empty() { None } else { Some(to) },
|
|
cc: if cc.is_empty() {
|
|
None
|
|
} else {
|
|
Some(addrparse(&cc.join(","))?)
|
|
},
|
|
bcc: if bcc.is_empty() {
|
|
None
|
|
} else {
|
|
Some(addrparse(&bcc.join(","))?)
|
|
},
|
|
subject: subject.into(),
|
|
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
|
content: body.into(),
|
|
})]),
|
|
..Msg::default()
|
|
};
|
|
trace!("message: {:?}", msg);
|
|
|
|
msg.edit_with_editor(config, printer, backend, smtp)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Move a message from a mailbox to another.
|
|
pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
seq: &str,
|
|
mbox_src: &str,
|
|
mbox_dst: &str,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
backend.move_msg(mbox_src, mbox_dst, seq)?;
|
|
printer.print(format!(
|
|
r#"Message {} successfully moved to folder "{}""#,
|
|
seq, mbox_dst
|
|
))
|
|
}
|
|
|
|
/// Read a message by its sequence number.
|
|
pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
seq: &str,
|
|
text_mime: &str,
|
|
raw: bool,
|
|
mbox: &str,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
let msg = backend.get_msg(mbox, seq)?;
|
|
let msg = if raw {
|
|
// Emails don't always have valid utf8. Using "lossy" to display what we can.
|
|
String::from_utf8_lossy(&msg.raw).into_owned()
|
|
} else {
|
|
msg.fold_text_parts(text_mime)
|
|
};
|
|
|
|
printer.print(msg)
|
|
}
|
|
|
|
/// Reply to the given message UID.
|
|
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|
seq: &str,
|
|
all: bool,
|
|
attachments_paths: Vec<&str>,
|
|
encrypt: bool,
|
|
mbox: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
smtp: &mut S,
|
|
) -> Result<()> {
|
|
backend
|
|
.get_msg(mbox, seq)?
|
|
.into_reply(all, config)?
|
|
.add_attachments(attachments_paths)?
|
|
.encrypt(encrypt)
|
|
.edit_with_editor(config, printer, backend, smtp)?
|
|
.add_flags(mbox, seq, "replied")
|
|
}
|
|
|
|
/// Saves a raw message to the targetted mailbox.
|
|
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
mbox: &str,
|
|
raw_msg: &str,
|
|
printer: &mut P,
|
|
backend: Box<&mut B>,
|
|
) -> Result<()> {
|
|
info!("entering save message handler");
|
|
|
|
debug!("mailbox: {}", mbox);
|
|
|
|
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()
|
|
.lock()
|
|
.lines()
|
|
.filter_map(Result::ok)
|
|
.collect::<Vec<String>>()
|
|
.join("\r\n")
|
|
};
|
|
backend.add_msg(mbox, raw_msg.as_bytes(), "seen")?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Paginate messages from the selected mailbox matching the specified query.
|
|
pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
query: String,
|
|
max_width: Option<usize>,
|
|
page_size: Option<usize>,
|
|
page: usize,
|
|
mbox: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
let page_size = page_size.unwrap_or(config.default_page_size);
|
|
debug!("page size: {}", page_size);
|
|
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
|
|
trace!("messages: {:#?}", msgs);
|
|
printer.print_table(
|
|
msgs,
|
|
PrintTableOpts {
|
|
format: &config.format,
|
|
max_width,
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria.
|
|
pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|
sort: String,
|
|
query: String,
|
|
max_width: Option<usize>,
|
|
page_size: Option<usize>,
|
|
page: usize,
|
|
mbox: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
) -> Result<()> {
|
|
let page_size = page_size.unwrap_or(config.default_page_size);
|
|
debug!("page size: {}", page_size);
|
|
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
|
|
trace!("envelopes: {:#?}", msgs);
|
|
printer.print_table(
|
|
msgs,
|
|
PrintTableOpts {
|
|
format: &config.format,
|
|
max_width,
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Send a raw message.
|
|
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|
raw_msg: &str,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&mut B>,
|
|
smtp: &mut S,
|
|
) -> Result<()> {
|
|
info!("entering send message handler");
|
|
|
|
let is_tty = atty::is(Stream::Stdin);
|
|
debug!("is tty: {}", is_tty);
|
|
let is_json = printer.is_json();
|
|
debug!("is json: {}", is_json);
|
|
|
|
let sent_folder = config
|
|
.mailboxes
|
|
.get("sent")
|
|
.map(|s| s.as_str())
|
|
.unwrap_or(DEFAULT_SENT_FOLDER);
|
|
debug!("sent folder: {:?}", sent_folder);
|
|
|
|
let raw_msg = if is_tty || is_json {
|
|
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
|
} else {
|
|
io::stdin()
|
|
.lock()
|
|
.lines()
|
|
.filter_map(Result::ok)
|
|
.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);
|
|
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
|
|
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Compose a new message.
|
|
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|
attachments_paths: Vec<&str>,
|
|
encrypt: bool,
|
|
config: &AccountConfig,
|
|
printer: &mut P,
|
|
backend: Box<&'a mut B>,
|
|
smtp: &mut S,
|
|
) -> Result<()> {
|
|
Msg::default()
|
|
.add_attachments(attachments_paths)?
|
|
.encrypt(encrypt)
|
|
.edit_with_editor(config, printer, backend, smtp)?;
|
|
Ok(())
|
|
}
|