mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 13:17:55 +08:00
425 lines
12 KiB
Rust
425 lines
12 KiB
Rust
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::{
|
|
config::entity::Account,
|
|
domain::{
|
|
imap::service::ImapServiceInterface,
|
|
mbox::entity::Mbox,
|
|
msg::{
|
|
self,
|
|
body::entity::Body,
|
|
entity::{Msg, MsgSerialized, Msgs},
|
|
flag::entity::Flags,
|
|
header::entity::Headers,
|
|
},
|
|
smtp::service::SmtpServiceInterface,
|
|
},
|
|
output::service::{OutputService, OutputServiceInterface},
|
|
ui::choice::{self, PostEditChoice},
|
|
};
|
|
|
|
// 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 choice::post_edit() {
|
|
Ok(choice) => match choice {
|
|
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
|
|
msg::utils::remove_draft()?;
|
|
output.print("Message successfully sent")?;
|
|
break;
|
|
}
|
|
// edit the body of the msg
|
|
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."
|
|
));
|
|
}
|
|
}
|
|
PostEditChoice::LocalDraft => break,
|
|
PostEditChoice::RemoteDraft => {
|
|
debug!("saving to draft…");
|
|
|
|
msg.flags.insert(Flag::Seen);
|
|
|
|
let mbox = Mbox::from("Drafts");
|
|
match imap.append_msg(&mbox, msg) {
|
|
Ok(_) => {
|
|
msg::utils::remove_draft()?;
|
|
output.print("Message successfully saved to Drafts")?;
|
|
}
|
|
Err(err) => {
|
|
output.print("Cannot save draft to the server")?;
|
|
return Err(err.into());
|
|
}
|
|
};
|
|
break;
|
|
}
|
|
PostEditChoice::Discard => {
|
|
msg::utils::remove_draft()?;
|
|
break;
|
|
}
|
|
},
|
|
Err(err) => error!("{}", err),
|
|
}
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Download all attachments from the given message UID to the user account downloads directory.
|
|
pub fn attachments<OutputService: OutputServiceInterface, 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
|
|
);
|
|
|
|
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 download attachment {:?}", filepath))?;
|
|
}
|
|
|
|
output.print(format!(
|
|
"{} attachment(s) successfully downloaded to {:?}",
|
|
attachments.len(),
|
|
account.downloads_dir
|
|
))?;
|
|
|
|
imap.logout()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Copy the given message UID to the targetted mailbox.
|
|
pub fn copy<OutputService: OutputServiceInterface, 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)?;
|
|
msg.flags.insert(Flag::Seen);
|
|
imap.append_msg(&target, &mut msg)?;
|
|
output.print(format!(
|
|
r#"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),
|
|
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(())
|
|
}
|