diff --git a/Cargo.toml b/Cargo.toml index 43a62165..0da12aab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,10 +69,11 @@ debug = false members = ["melib", "testing", ] [features] -default = ["sqlite3", "notmuch", "regexp"] +default = ["sqlite3", "notmuch", "regexp", "smtp"] notmuch = ["melib/notmuch_backend", ] jmap = ["melib/jmap_backend",] sqlite3 = ["melib/sqlite3"] +smtp = ["melib/smtp"] regexp = ["pcre2"] cli-docs = [] svgscreenshot = ["svg_crate"] diff --git a/melib/src/logging.rs b/melib/src/logging.rs index 17aaee34..0df5ad53 100644 --- a/melib/src/logging.rs +++ b/melib/src/logging.rs @@ -79,7 +79,7 @@ thread_local!(static LOG: Arc> = Arc::new(Mutex::new({ }})) ); -pub fn log(val: String, level: LoggingLevel) { +pub fn log>(val: S, level: LoggingLevel) { LOG.with(|f| { let mut b = f.lock().unwrap(); if level <= b.level { @@ -91,7 +91,7 @@ pub fn log(val: String, level: LoggingLevel) { b.dest.write_all(b" [").unwrap(); b.dest.write_all(level.to_string().as_bytes()).unwrap(); b.dest.write_all(b"]: ").unwrap(); - b.dest.write_all(val.as_bytes()).unwrap(); + b.dest.write_all(val.as_ref().as_bytes()).unwrap(); b.dest.write_all(b"\n").unwrap(); b.dest.flush().unwrap(); } diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 1c0e2045..df2aab71 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -24,7 +24,7 @@ use melib::list_management; use melib::Draft; use crate::conf::accounts::JobRequest; -use crate::jobs::{oneshot, JobId}; +use crate::jobs::{JobChannel, JobId, JoinHandle}; use crate::terminal::embed::EmbedGrid; use nix::sys::wait::WaitStatus; use std::str::FromStr; @@ -66,7 +66,7 @@ impl std::ops::DerefMut for EmbedStatus { #[derive(Debug)] pub struct Composer { reply_context: Option<(MailboxHash, EnvelopeHash)>, - reply_bytes_request: Option<(JobId, oneshot::Receiver>>)>, + reply_bytes_request: Option<(JobId, JobChannel>)>, account_cursor: usize, cursor: Cursor, @@ -120,6 +120,7 @@ enum ViewMode { Embed, SelectRecipients(UIDialog
), Send(UIConfirmationDialog), + WaitingForSendResult(UIDialog, JoinHandle, JobId, JobChannel<()>), } impl ViewMode { @@ -612,6 +613,10 @@ impl Component for Composer { /* Let user choose whether to quit with/without saving or cancel */ s.draw(grid, center_area(area, s.content.size()), context); } + ViewMode::WaitingForSendResult(ref mut s, _, _, _) => { + /* Let user choose whether to wait for success or cancel */ + s.draw(grid, center_area(area, s.content.size()), context); + } } self.dirty = false; self.draw_attachments(grid, attachment_area, context); @@ -673,28 +678,61 @@ impl Component for Composer { { if let Some(true) = result.downcast_ref::() { self.update_draft(); - if send_draft( + match send_draft( self.sign_mail, context, self.account_cursor, self.draft.clone(), SpecialUsageMailbox::Sent, Flag::SEEN, + false, ) { - context - .replies - .push_back(UIEvent::Action(Tab(Kill(self.id)))); - } else { - save_draft( - self.draft.clone().finalise().unwrap().as_bytes(), - context, - SpecialUsageMailbox::Drafts, - Flag::SEEN | Flag::DRAFT, - self.account_cursor, - ); + Ok(Some((job_id, handle, chan))) => { + self.mode = ViewMode::WaitingForSendResult( + UIDialog::new( + "Waiting for confirmation.. The tab will close automatically on successful submission.", + vec![ + ('c', "force close tab".to_string()), + ('n', "close this message and return to edit mode".to_string()), + ], + true, + Some(Box::new(move |id: ComponentId, results: &[char]| { + Some(UIEvent::FinishedUIDialog( + id, + Box::new(results.get(0).map(|c| *c).unwrap_or('c')), + )) + })), + context, + ), handle, job_id, chan); + } + Ok(None) => { + context.replies.push_back(UIEvent::Notification( + Some("Sent.".into()), + String::new(), + Some(NotificationType::INFO), + )); + context + .replies + .push_back(UIEvent::Action(Tab(Kill(self.id)))); + } + Err(err) => { + context.replies.push_back(UIEvent::Notification( + None, + err.to_string(), + Some(NotificationType::ERROR), + )); + save_draft( + self.draft.clone().finalise().unwrap().as_bytes(), + context, + SpecialUsageMailbox::Drafts, + Flag::SEEN | Flag::DRAFT, + self.account_cursor, + ); + self.mode = ViewMode::Edit; + } } } - self.mode = ViewMode::Edit; + self.set_dirty(true); return true; } (ViewMode::Send(ref mut selector), _) => { @@ -753,6 +791,59 @@ impl Component for Composer { return true; } } + ( + ViewMode::WaitingForSendResult(ref selector, _, _, _), + UIEvent::FinishedUIDialog(id, result), + ) if selector.id() == *id => { + if let Some(key) = result.downcast_mut::() { + match key { + 'c' => { + context + .replies + .push_back(UIEvent::Action(Tab(Kill(self.id)))); + return true; + } + 'n' => { + self.set_dirty(true); + if let ViewMode::WaitingForSendResult(_, handle, job_id, chan) = + std::mem::replace(&mut self.mode, ViewMode::Edit) + { + context.accounts[self.account_cursor].active_jobs.insert( + job_id, + JobRequest::SendMessageBackground(handle, chan), + ); + } + } + _ => {} + } + } + return true; + } + ( + ViewMode::WaitingForSendResult(_, _, ref our_job_id, ref mut chan), + UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)), + ) if *our_job_id == *job_id => { + let result = chan.try_recv().unwrap(); + if let Some(Err(err)) = result { + self.mode = ViewMode::Edit; + context.replies.push_back(UIEvent::Notification( + None, + err.to_string(), + Some(NotificationType::ERROR), + )); + self.set_dirty(true); + } else { + context + .replies + .push_back(UIEvent::Action(Tab(Kill(self.id)))); + } + return true; + } + (ViewMode::WaitingForSendResult(ref mut selector, _, _, _), _) => { + if selector.process_event(event, context) { + return true; + } + } _ => {} } if self.cursor == Cursor::Headers @@ -1279,76 +1370,58 @@ pub fn send_draft( mut draft: Draft, mailbox_type: SpecialUsageMailbox, flags: Flag, -) -> bool { - use std::io::Write; - use std::process::{Command, Stdio}; + complete_in_background: bool, +) -> Result)>> { let format_flowed = *mailbox_acc_settings!(context[account_cursor].composing.format_flowed); - let command = mailbox_acc_settings!(context[account_cursor].composing.mailer_command); - if command.is_empty() { - context.replies.push_back(UIEvent::Notification( - None, - String::from("mailer_command configuration value is empty"), - Some(NotificationType::ERROR), - )); - return false; - } - let bytes; - let mut msmtp = Command::new("sh") - .args(&["-c", command]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to start mailer command"); - { - let stdin = msmtp.stdin.as_mut().expect("failed to open stdin"); - if sign_mail.is_true() { - let mut content_type = ContentType::default(); - if format_flowed { - if let ContentType::Text { - ref mut parameters, .. - } = content_type - { - parameters.push((b"format".to_vec(), b"flowed".to_vec())); - } + if sign_mail.is_true() { + let mut content_type = ContentType::default(); + if format_flowed { + if let ContentType::Text { + ref mut parameters, .. + } = content_type + { + parameters.push((b"format".to_vec(), b"flowed".to_vec())); } + } - let mut body: AttachmentBuilder = Attachment::new( - content_type, + let mut body: AttachmentBuilder = Attachment::new( + content_type, + Default::default(), + std::mem::replace(&mut draft.body, String::new()).into_bytes(), + ) + .into(); + if !draft.attachments.is_empty() { + let mut parts = std::mem::replace(&mut draft.attachments, Vec::new()); + parts.insert(0, body); + let boundary = ContentType::make_boundary(&parts); + body = Attachment::new( + ContentType::Multipart { + boundary: boundary.into_bytes(), + kind: MultipartType::Mixed, + parts: parts.into_iter().map(|a| a.into()).collect::>(), + }, Default::default(), - std::mem::replace(&mut draft.body, String::new()).into_bytes(), + Vec::new(), ) .into(); - if !draft.attachments.is_empty() { - let mut parts = std::mem::replace(&mut draft.attachments, Vec::new()); - parts.insert(0, body); - let boundary = ContentType::make_boundary(&parts); - body = Attachment::new( - ContentType::Multipart { - boundary: boundary.into_bytes(), - kind: MultipartType::Mixed, - parts: parts.into_iter().map(|a| a.into()).collect::>(), - }, - Default::default(), - Vec::new(), - ) - .into(); - } - let output = crate::components::mail::pgp::sign( - body.into(), - mailbox_acc_settings!(context[account_cursor].pgp.gpg_binary) - .as_ref() - .map(|s| s.as_str()), - mailbox_acc_settings!(context[account_cursor].pgp.key) - .as_ref() - .map(|s| s.as_str()), - ); - if let Err(e) = &output { - debug!("{:?} could not sign draft msg", e); + } + let output = crate::components::mail::pgp::sign( + body.into(), + mailbox_acc_settings!(context[account_cursor].pgp.gpg_binary) + .as_ref() + .map(|s| s.as_str()), + mailbox_acc_settings!(context[account_cursor].pgp.key) + .as_ref() + .map(|s| s.as_str()), + ); + match output { + Err(err) => { + debug!("{:?} could not sign draft msg", err); log( format!( "Could not sign draft in account `{}`: {}.", context.accounts[account_cursor].name(), - e.to_string() + err.to_string() ), ERROR, ); @@ -1357,62 +1430,38 @@ pub fn send_draft( "Could not sign draft in account `{}`.", context.accounts[account_cursor].name() )), - e.to_string(), + err.to_string(), Some(NotificationType::ERROR), )); - return false; + return Err(err); } - draft.attachments.push(output.unwrap()); - } else { - let mut content_type = ContentType::default(); - if format_flowed { - if let ContentType::Text { - ref mut parameters, .. - } = content_type - { - parameters.push((b"format".to_vec(), b"flowed".to_vec())); - } - - let body: AttachmentBuilder = Attachment::new( - content_type, - Default::default(), - std::mem::replace(&mut draft.body, String::new()).into_bytes(), - ) - .into(); - draft.attachments.insert(0, body); + Ok(output) => { + draft.attachments.push(output); } } - bytes = draft.finalise().unwrap(); - stdin - .write_all(bytes.as_bytes()) - .expect("Failed to write to stdin"); - } - let output = msmtp.wait().expect("Failed to wait on mailer"); - if output.success() { - context.replies.push_back(UIEvent::Notification( - Some("Sent.".into()), - String::new(), - None, - )); } else { - let error_message = if let Some(exit_code) = output.code() { - format!( - "Could not send e-mail using `{}`: Process exited with {}", - command, exit_code + let mut content_type = ContentType::default(); + if format_flowed { + if let ContentType::Text { + ref mut parameters, .. + } = content_type + { + parameters.push((b"format".to_vec(), b"flowed".to_vec())); + } + + let body: AttachmentBuilder = Attachment::new( + content_type, + Default::default(), + std::mem::replace(&mut draft.body, String::new()).into_bytes(), ) - } else { - format!( - "Could not send e-mail using `{}`: Process was killed by signal", - command - ) - }; - context.replies.push_back(UIEvent::Notification( - Some("Message not sent.".into()), - error_message.clone(), - Some(NotificationType::ERROR), - )); - log(error_message, ERROR); + .into(); + draft.attachments.insert(0, body); + } } + let bytes = draft.finalise().unwrap(); + let send_mail = mailbox_acc_settings!(context[account_cursor].composing.send_mail).clone(); + let ret = + context.accounts[account_cursor].send(bytes.clone(), send_mail, complete_in_background); save_draft( bytes.as_bytes(), context, @@ -1420,7 +1469,7 @@ pub fn send_draft( flags, account_cursor, ); - true + ret } pub fn save_draft( diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 8f8e670d..930c4c5f 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -1495,14 +1495,23 @@ impl Component for MailView { * on its own */ drop(detect); drop(envelope); - return super::compose::send_draft( + if let Err(err) = super::compose::send_draft( ToggleFlag::False, context, self.coordinates.0, draft, SpecialUsageMailbox::Sent, Flag::SEEN, - ); + true, + ) { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Couldn't send unsubscribe e-mail: {}", + err + )), + )); + } + return true; } } list_management::ListAction::Url(url) => { diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs index fd155a94..db7b718d 100644 --- a/src/conf/accounts.rs +++ b/src/conf/accounts.rs @@ -24,7 +24,7 @@ */ use super::{AccountConf, FileMailboxConf}; -use crate::jobs::{JobExecutor, JobId, JoinHandle}; +use crate::jobs::{JobChannel, JobExecutor, JobId, JoinHandle}; use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext}; use melib::backends::{ AccountHash, BackendOp, Backends, MailBackend, Mailbox, MailboxHash, NotifyFn, ReadOnlyOp, @@ -173,7 +173,8 @@ pub enum JobRequest { Refresh(MailboxHash, JoinHandle, oneshot::Receiver>), SetFlags(EnvelopeHash, JoinHandle, oneshot::Receiver>), SaveMessage(MailboxHash, JoinHandle, oneshot::Receiver>), - SendMessage(JoinHandle, oneshot::Receiver>), + SendMessage, + SendMessageBackground(JoinHandle, JobChannel<()>), CopyTo(MailboxHash, JoinHandle, oneshot::Receiver>>), DeleteMessage(EnvelopeHash, JoinHandle, oneshot::Receiver>), CreateMailbox( @@ -215,7 +216,10 @@ impl core::fmt::Debug for JobRequest { write!(f, "JobRequest::SetMailboxSubscription") } JobRequest::Watch(_) => write!(f, "JobRequest::Watch"), - JobRequest::SendMessage(_, _) => write!(f, "JobRequest::SendMessage"), + JobRequest::SendMessage => write!(f, "JobRequest::SendMessage"), + JobRequest::SendMessageBackground(_, _) => { + write!(f, "JobRequest::SendMessageBackground") + } } } } @@ -1221,6 +1225,81 @@ impl Account { Ok(()) } + pub fn send( + &mut self, + message: String, + send_mail: crate::conf::composing::SendMail, + complete_in_background: bool, + ) -> Result)>> { + use crate::conf::composing::SendMail; + use std::io::Write; + use std::process::{Command, Stdio}; + debug!(&send_mail); + match send_mail { + SendMail::ShellCommand(ref command) => { + if command.is_empty() { + return Err(MeliError::new( + "send_mail shell command configuration value is empty", + )); + } + let mut msmtp = Command::new("sh") + .args(&["-c", command]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to start mailer command"); + { + let stdin = msmtp.stdin.as_mut().expect("failed to open stdin"); + stdin + .write_all(message.as_bytes()) + .expect("Failed to write to stdin"); + } + let output = msmtp.wait().expect("Failed to wait on mailer"); + if output.success() { + melib::log("Message sent.", melib::LoggingLevel::TRACE); + } else { + let error_message = if let Some(exit_code) = output.code() { + format!( + "Could not send e-mail using `{}`: Process exited with {}", + command, exit_code + ) + } else { + format!( + "Could not send e-mail using `{}`: Process was killed by signal", + command + ) + }; + melib::log(&error_message, melib::LoggingLevel::ERROR); + return Err( + MeliError::new(error_message.clone()).set_summary("Message not sent.") + ); + } + Ok(None) + } + #[cfg(feature = "smtp")] + SendMail::Smtp(conf) => { + let (chan, handle, job_id) = self.job_executor.spawn_specialized(async move { + let mut smtp_connection = + melib::smtp::SmtpConnection::new_connection(conf).await?; + smtp_connection.mail_transaction(&message).await + }); + self.sender + .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( + StatusEvent::NewJob(job_id), + ))) + .unwrap(); + if complete_in_background { + self.active_jobs + .insert(job_id, JobRequest::SendMessageBackground(handle, chan)); + return Ok(None); + } else { + self.active_jobs.insert(job_id, JobRequest::SendMessage); + } + Ok(Some((job_id, handle, chan))) + } + } + } + pub fn delete( &mut self, env_hash: EnvelopeHash, @@ -1678,6 +1757,30 @@ impl Account { .expect("Could not send event on main channel"); } } + JobRequest::SendMessage => { + self.sender + .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( + StatusEvent::JobFinished(*job_id), + ))) + .unwrap(); + } + JobRequest::SendMessageBackground(_, mut chan) => { + let r = chan.try_recv().unwrap(); + if let Some(Err(err)) = r { + self.sender + .send(ThreadEvent::UIEvent(UIEvent::Notification( + Some("Could not send message".to_string()), + err.to_string(), + Some(crate::types::NotificationType::ERROR), + ))) + .expect("Could not send event on main channel"); + } + self.sender + .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( + StatusEvent::JobFinished(*job_id), + ))) + .unwrap(); + } JobRequest::CopyTo(mailbox_hash, _, mut chan) => { if let Err(err) = chan .try_recv() diff --git a/src/conf/composing.rs b/src/conf/composing.rs index 79101544..0d7e2576 100644 --- a/src/conf/composing.rs +++ b/src/conf/composing.rs @@ -28,8 +28,7 @@ use std::collections::HashMap; pub struct ComposingSettings { /// A command to pipe new emails to /// Required - #[serde(alias = "mailer-command", alias = "mailer-cmd", alias = "mailer_cmd")] - pub mailer_command: String, + pub send_mail: SendMail, /// Command to launch editor. Can have arguments. Draft filename is given as the last argument. If it's missing, the environment variable $EDITOR is looked up. #[serde( default = "none", @@ -54,7 +53,7 @@ pub struct ComposingSettings { impl Default for ComposingSettings { fn default() -> Self { ComposingSettings { - mailer_command: String::new(), + send_mail: SendMail::ShellCommand("/bin/false".into()), editor_command: None, embed: false, format_flowed: true, @@ -62,3 +61,11 @@ impl Default for ComposingSettings { } } } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum SendMail { + #[cfg(feature = "smtp")] + Smtp(melib::smtp::SmtpServerConf), + ShellCommand(String), +} diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index aeb00c5b..1d5672e9 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -202,9 +202,8 @@ impl Default for ShortcutsOverride { pub struct ComposingSettingsOverride { #[doc = " A command to pipe new emails to"] #[doc = " Required"] - #[serde(alias = "mailer-command", alias = "mailer-cmd", alias = "mailer_cmd")] #[serde(default)] - pub mailer_command: Option, + pub send_mail: Option, #[doc = " Command to launch editor. Can have arguments. Draft filename is given as the last argument. If it's missing, the environment variable $EDITOR is looked up."] #[serde(alias = "editor-command", alias = "editor-cmd", alias = "editor_cmd")] #[serde(default)] @@ -226,7 +225,7 @@ pub struct ComposingSettingsOverride { impl Default for ComposingSettingsOverride { fn default() -> Self { ComposingSettingsOverride { - mailer_command: None, + send_mail: None, editor_command: None, embed: None, format_flowed: None, diff --git a/src/jobs.rs b/src/jobs.rs index 82e72591..5804b6b2 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -77,7 +77,7 @@ macro_rules! uuid_hash_type { } impl $n { - fn new() -> Self { + pub fn new() -> Self { $n(Uuid::new_v4()) } pub fn null() -> Self { @@ -216,6 +216,8 @@ impl JobExecutor { } } +pub type JobChannel = oneshot::Receiver>; + ///// Spawns a future on the executor. //fn spawn(future: F) -> JoinHandle //where diff --git a/src/types.rs b/src/types.rs index df56e965..83263015 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,6 +38,7 @@ pub use self::helpers::*; use super::execute::Action; use super::jobs::JobId; use super::terminal::*; +use crate::components::{Component, ComponentId}; use melib::backends::{AccountHash, MailboxHash}; use melib::{EnvelopeHash, RefreshEvent, ThreadHash}; @@ -125,8 +126,8 @@ pub enum UIEvent { EnvelopeRemove(EnvelopeHash, ThreadHash), Contacts(ContactEvent), Compose(ComposeEvent), - FinishedUIDialog(crate::components::ComponentId, UIMessage), - GlobalUIDialog(Box), + FinishedUIDialog(ComponentId, UIMessage), + GlobalUIDialog(Box), Timer(u8), }