Add smtp client support for sending mail in UI

`mailer_command` was removed, and a new setting `send_mail` was added.

Its possible values are a string, consisting of a shell command to
execute, or settings to configure an smtp server connection. The
configuration I used for testing this is:

  [composing]
  send_mail = { hostname = "smtp.mail.tld", port = 587, auth = { type = "auto", username = "yoshi", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/msmtp/yoshi.gpg" } }, security = { type = "STARTTLS" } }

For local smtp server:
  [composing]
  send_mail = { hostname = "localhost", port = 25, auth = { type = "none" }, security = { type = "none" } }
master
Manos Pitsidianakis 2020-07-15 14:38:43 +03:00
parent ddafde7b37
commit 77dc1d74bf
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
9 changed files with 312 additions and 141 deletions

View File

@ -69,10 +69,11 @@ debug = false
members = ["melib", "testing", ] members = ["melib", "testing", ]
[features] [features]
default = ["sqlite3", "notmuch", "regexp"] default = ["sqlite3", "notmuch", "regexp", "smtp"]
notmuch = ["melib/notmuch_backend", ] notmuch = ["melib/notmuch_backend", ]
jmap = ["melib/jmap_backend",] jmap = ["melib/jmap_backend",]
sqlite3 = ["melib/sqlite3"] sqlite3 = ["melib/sqlite3"]
smtp = ["melib/smtp"]
regexp = ["pcre2"] regexp = ["pcre2"]
cli-docs = [] cli-docs = []
svgscreenshot = ["svg_crate"] svgscreenshot = ["svg_crate"]

View File

@ -79,7 +79,7 @@ thread_local!(static LOG: Arc<Mutex<LoggingBackend>> = Arc::new(Mutex::new({
}})) }}))
); );
pub fn log(val: String, level: LoggingLevel) { pub fn log<S: AsRef<str>>(val: S, level: LoggingLevel) {
LOG.with(|f| { LOG.with(|f| {
let mut b = f.lock().unwrap(); let mut b = f.lock().unwrap();
if level <= b.level { 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(b" [").unwrap();
b.dest.write_all(level.to_string().as_bytes()).unwrap(); b.dest.write_all(level.to_string().as_bytes()).unwrap();
b.dest.write_all(b"]: ").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.write_all(b"\n").unwrap();
b.dest.flush().unwrap(); b.dest.flush().unwrap();
} }

View File

@ -24,7 +24,7 @@ use melib::list_management;
use melib::Draft; use melib::Draft;
use crate::conf::accounts::JobRequest; use crate::conf::accounts::JobRequest;
use crate::jobs::{oneshot, JobId}; use crate::jobs::{JobChannel, JobId, JoinHandle};
use crate::terminal::embed::EmbedGrid; use crate::terminal::embed::EmbedGrid;
use nix::sys::wait::WaitStatus; use nix::sys::wait::WaitStatus;
use std::str::FromStr; use std::str::FromStr;
@ -66,7 +66,7 @@ impl std::ops::DerefMut for EmbedStatus {
#[derive(Debug)] #[derive(Debug)]
pub struct Composer { pub struct Composer {
reply_context: Option<(MailboxHash, EnvelopeHash)>, reply_context: Option<(MailboxHash, EnvelopeHash)>,
reply_bytes_request: Option<(JobId, oneshot::Receiver<Result<Vec<u8>>>)>, reply_bytes_request: Option<(JobId, JobChannel<Vec<u8>>)>,
account_cursor: usize, account_cursor: usize,
cursor: Cursor, cursor: Cursor,
@ -120,6 +120,7 @@ enum ViewMode {
Embed, Embed,
SelectRecipients(UIDialog<Address>), SelectRecipients(UIDialog<Address>),
Send(UIConfirmationDialog), Send(UIConfirmationDialog),
WaitingForSendResult(UIDialog<char>, JoinHandle, JobId, JobChannel<()>),
} }
impl ViewMode { impl ViewMode {
@ -612,6 +613,10 @@ impl Component for Composer {
/* Let user choose whether to quit with/without saving or cancel */ /* Let user choose whether to quit with/without saving or cancel */
s.draw(grid, center_area(area, s.content.size()), context); 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.dirty = false;
self.draw_attachments(grid, attachment_area, context); self.draw_attachments(grid, attachment_area, context);
@ -673,28 +678,61 @@ impl Component for Composer {
{ {
if let Some(true) = result.downcast_ref::<bool>() { if let Some(true) = result.downcast_ref::<bool>() {
self.update_draft(); self.update_draft();
if send_draft( match send_draft(
self.sign_mail, self.sign_mail,
context, context,
self.account_cursor, self.account_cursor,
self.draft.clone(), self.draft.clone(),
SpecialUsageMailbox::Sent, SpecialUsageMailbox::Sent,
Flag::SEEN, Flag::SEEN,
false,
) { ) {
context Ok(Some((job_id, handle, chan))) => {
.replies self.mode = ViewMode::WaitingForSendResult(
.push_back(UIEvent::Action(Tab(Kill(self.id)))); UIDialog::new(
} else { "Waiting for confirmation.. The tab will close automatically on successful submission.",
save_draft( vec![
self.draft.clone().finalise().unwrap().as_bytes(), ('c', "force close tab".to_string()),
context, ('n', "close this message and return to edit mode".to_string()),
SpecialUsageMailbox::Drafts, ],
Flag::SEEN | Flag::DRAFT, true,
self.account_cursor, 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; return true;
} }
(ViewMode::Send(ref mut selector), _) => { (ViewMode::Send(ref mut selector), _) => {
@ -753,6 +791,59 @@ impl Component for Composer {
return true; return true;
} }
} }
(
ViewMode::WaitingForSendResult(ref selector, _, _, _),
UIEvent::FinishedUIDialog(id, result),
) if selector.id() == *id => {
if let Some(key) = result.downcast_mut::<char>() {
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 if self.cursor == Cursor::Headers
@ -1279,76 +1370,58 @@ pub fn send_draft(
mut draft: Draft, mut draft: Draft,
mailbox_type: SpecialUsageMailbox, mailbox_type: SpecialUsageMailbox,
flags: Flag, flags: Flag,
) -> bool { complete_in_background: bool,
use std::io::Write; ) -> Result<Option<(JobId, JoinHandle, JobChannel<()>)>> {
use std::process::{Command, Stdio};
let format_flowed = *mailbox_acc_settings!(context[account_cursor].composing.format_flowed); let format_flowed = *mailbox_acc_settings!(context[account_cursor].composing.format_flowed);
let command = mailbox_acc_settings!(context[account_cursor].composing.mailer_command); if sign_mail.is_true() {
if command.is_empty() { let mut content_type = ContentType::default();
context.replies.push_back(UIEvent::Notification( if format_flowed {
None, if let ContentType::Text {
String::from("mailer_command configuration value is empty"), ref mut parameters, ..
Some(NotificationType::ERROR), } = content_type
)); {
return false; parameters.push((b"format".to_vec(), b"flowed".to_vec()));
}
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()));
}
} }
}
let mut body: AttachmentBuilder = Attachment::new( let mut body: AttachmentBuilder = Attachment::new(
content_type, 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::<Vec<_>>(),
},
Default::default(), Default::default(),
std::mem::replace(&mut draft.body, String::new()).into_bytes(), Vec::new(),
) )
.into(); .into();
if !draft.attachments.is_empty() { }
let mut parts = std::mem::replace(&mut draft.attachments, Vec::new()); let output = crate::components::mail::pgp::sign(
parts.insert(0, body); body.into(),
let boundary = ContentType::make_boundary(&parts); mailbox_acc_settings!(context[account_cursor].pgp.gpg_binary)
body = Attachment::new( .as_ref()
ContentType::Multipart { .map(|s| s.as_str()),
boundary: boundary.into_bytes(), mailbox_acc_settings!(context[account_cursor].pgp.key)
kind: MultipartType::Mixed, .as_ref()
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(), .map(|s| s.as_str()),
}, );
Default::default(), match output {
Vec::new(), Err(err) => {
) debug!("{:?} could not sign draft msg", err);
.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);
log( log(
format!( format!(
"Could not sign draft in account `{}`: {}.", "Could not sign draft in account `{}`: {}.",
context.accounts[account_cursor].name(), context.accounts[account_cursor].name(),
e.to_string() err.to_string()
), ),
ERROR, ERROR,
); );
@ -1357,62 +1430,38 @@ pub fn send_draft(
"Could not sign draft in account `{}`.", "Could not sign draft in account `{}`.",
context.accounts[account_cursor].name() context.accounts[account_cursor].name()
)), )),
e.to_string(), err.to_string(),
Some(NotificationType::ERROR), Some(NotificationType::ERROR),
)); ));
return false; return Err(err);
} }
draft.attachments.push(output.unwrap()); Ok(output) => {
} else { draft.attachments.push(output);
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);
} }
} }
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 { } else {
let error_message = if let Some(exit_code) = output.code() { let mut content_type = ContentType::default();
format!( if format_flowed {
"Could not send e-mail using `{}`: Process exited with {}", if let ContentType::Text {
command, exit_code 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 { .into();
format!( draft.attachments.insert(0, body);
"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);
} }
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( save_draft(
bytes.as_bytes(), bytes.as_bytes(),
context, context,
@ -1420,7 +1469,7 @@ pub fn send_draft(
flags, flags,
account_cursor, account_cursor,
); );
true ret
} }
pub fn save_draft( pub fn save_draft(

View File

@ -1495,14 +1495,23 @@ impl Component for MailView {
* on its own */ * on its own */
drop(detect); drop(detect);
drop(envelope); drop(envelope);
return super::compose::send_draft( if let Err(err) = super::compose::send_draft(
ToggleFlag::False, ToggleFlag::False,
context, context,
self.coordinates.0, self.coordinates.0,
draft, draft,
SpecialUsageMailbox::Sent, SpecialUsageMailbox::Sent,
Flag::SEEN, 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) => { list_management::ListAction::Url(url) => {

View File

@ -24,7 +24,7 @@
*/ */
use super::{AccountConf, FileMailboxConf}; 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::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
use melib::backends::{ use melib::backends::{
AccountHash, BackendOp, Backends, MailBackend, Mailbox, MailboxHash, NotifyFn, ReadOnlyOp, AccountHash, BackendOp, Backends, MailBackend, Mailbox, MailboxHash, NotifyFn, ReadOnlyOp,
@ -173,7 +173,8 @@ pub enum JobRequest {
Refresh(MailboxHash, JoinHandle, oneshot::Receiver<Result<()>>), Refresh(MailboxHash, JoinHandle, oneshot::Receiver<Result<()>>),
SetFlags(EnvelopeHash, JoinHandle, oneshot::Receiver<Result<()>>), SetFlags(EnvelopeHash, JoinHandle, oneshot::Receiver<Result<()>>),
SaveMessage(MailboxHash, JoinHandle, oneshot::Receiver<Result<()>>), SaveMessage(MailboxHash, JoinHandle, oneshot::Receiver<Result<()>>),
SendMessage(JoinHandle, oneshot::Receiver<Result<()>>), SendMessage,
SendMessageBackground(JoinHandle, JobChannel<()>),
CopyTo(MailboxHash, JoinHandle, oneshot::Receiver<Result<Vec<u8>>>), CopyTo(MailboxHash, JoinHandle, oneshot::Receiver<Result<Vec<u8>>>),
DeleteMessage(EnvelopeHash, JoinHandle, oneshot::Receiver<Result<()>>), DeleteMessage(EnvelopeHash, JoinHandle, oneshot::Receiver<Result<()>>),
CreateMailbox( CreateMailbox(
@ -215,7 +216,10 @@ impl core::fmt::Debug for JobRequest {
write!(f, "JobRequest::SetMailboxSubscription") write!(f, "JobRequest::SetMailboxSubscription")
} }
JobRequest::Watch(_) => write!(f, "JobRequest::Watch"), 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(()) Ok(())
} }
pub fn send(
&mut self,
message: String,
send_mail: crate::conf::composing::SendMail,
complete_in_background: bool,
) -> Result<Option<(JobId, JoinHandle, JobChannel<()>)>> {
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( pub fn delete(
&mut self, &mut self,
env_hash: EnvelopeHash, env_hash: EnvelopeHash,
@ -1678,6 +1757,30 @@ impl Account {
.expect("Could not send event on main channel"); .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) => { JobRequest::CopyTo(mailbox_hash, _, mut chan) => {
if let Err(err) = chan if let Err(err) = chan
.try_recv() .try_recv()

View File

@ -28,8 +28,7 @@ use std::collections::HashMap;
pub struct ComposingSettings { pub struct ComposingSettings {
/// A command to pipe new emails to /// A command to pipe new emails to
/// Required /// Required
#[serde(alias = "mailer-command", alias = "mailer-cmd", alias = "mailer_cmd")] pub send_mail: SendMail,
pub mailer_command: String,
/// 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. /// 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( #[serde(
default = "none", default = "none",
@ -54,7 +53,7 @@ pub struct ComposingSettings {
impl Default for ComposingSettings { impl Default for ComposingSettings {
fn default() -> Self { fn default() -> Self {
ComposingSettings { ComposingSettings {
mailer_command: String::new(), send_mail: SendMail::ShellCommand("/bin/false".into()),
editor_command: None, editor_command: None,
embed: false, embed: false,
format_flowed: true, 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),
}

View File

@ -202,9 +202,8 @@ impl Default for ShortcutsOverride {
pub struct ComposingSettingsOverride { pub struct ComposingSettingsOverride {
#[doc = " A command to pipe new emails to"] #[doc = " A command to pipe new emails to"]
#[doc = " Required"] #[doc = " Required"]
#[serde(alias = "mailer-command", alias = "mailer-cmd", alias = "mailer_cmd")]
#[serde(default)] #[serde(default)]
pub mailer_command: Option<String>, pub send_mail: Option<SendMail>,
#[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."] #[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(alias = "editor-command", alias = "editor-cmd", alias = "editor_cmd")]
#[serde(default)] #[serde(default)]
@ -226,7 +225,7 @@ pub struct ComposingSettingsOverride {
impl Default for ComposingSettingsOverride { impl Default for ComposingSettingsOverride {
fn default() -> Self { fn default() -> Self {
ComposingSettingsOverride { ComposingSettingsOverride {
mailer_command: None, send_mail: None,
editor_command: None, editor_command: None,
embed: None, embed: None,
format_flowed: None, format_flowed: None,

View File

@ -77,7 +77,7 @@ macro_rules! uuid_hash_type {
} }
impl $n { impl $n {
fn new() -> Self { pub fn new() -> Self {
$n(Uuid::new_v4()) $n(Uuid::new_v4())
} }
pub fn null() -> Self { pub fn null() -> Self {
@ -216,6 +216,8 @@ impl JobExecutor {
} }
} }
pub type JobChannel<T> = oneshot::Receiver<Result<T>>;
///// Spawns a future on the executor. ///// Spawns a future on the executor.
//fn spawn<F, R>(future: F) -> JoinHandle<R> //fn spawn<F, R>(future: F) -> JoinHandle<R>
//where //where

View File

@ -38,6 +38,7 @@ pub use self::helpers::*;
use super::execute::Action; use super::execute::Action;
use super::jobs::JobId; use super::jobs::JobId;
use super::terminal::*; use super::terminal::*;
use crate::components::{Component, ComponentId};
use melib::backends::{AccountHash, MailboxHash}; use melib::backends::{AccountHash, MailboxHash};
use melib::{EnvelopeHash, RefreshEvent, ThreadHash}; use melib::{EnvelopeHash, RefreshEvent, ThreadHash};
@ -125,8 +126,8 @@ pub enum UIEvent {
EnvelopeRemove(EnvelopeHash, ThreadHash), EnvelopeRemove(EnvelopeHash, ThreadHash),
Contacts(ContactEvent), Contacts(ContactEvent),
Compose(ComposeEvent), Compose(ComposeEvent),
FinishedUIDialog(crate::components::ComponentId, UIMessage), FinishedUIDialog(ComponentId, UIMessage),
GlobalUIDialog(Box<dyn crate::components::Component>), GlobalUIDialog(Box<dyn Component>),
Timer(u8), Timer(u8),
} }