use anyhow::{Result, bail};
use std::collections::HashMap;
use crate::app::App;
use crate::cli::{ComposeArgs, ForwardArgs, ReplyArgs, SendArgs};
use crate::helpers::{
append_signature_html, append_signature_text, build_send_attachments,
ensure_reply_account_matches, format_forwarded_body, format_sender, forward_subject,
generate_message_id, normalize_email, normalize_emails, now_timestamp, read_optional_content,
reply_all_recipients, reply_headers_for_message, reply_recipients, reply_subject,
};
use crate::http::fetch_sent_detail;
use crate::models::{MessageRecord, ReplyHeaders, ResolvedCompose, SendEmailRequest, SentEmail};
use crate::output::print_success_or;
impl App {
pub fn send(&self, args: SendArgs) -> Result<()> {
let reply_to_msg = args.compose.reply_to_msg;
let mut compose = self.resolve_compose(args.compose)?;
let reply_context = if let Some(msg_id) = reply_to_msg {
let target = self.get_message(msg_id)?;
let headers = reply_headers_for_message(&target);
if compose.subject.is_empty() {
compose.subject = reply_subject(&target.subject);
}
if compose.to.is_empty() {
compose.to = reply_recipients(&target)?;
}
Some((target.id, headers))
} else {
None
};
let message = self.send_compose(compose, reply_context)?;
print_success_or(self.format, &message, |message| {
println!(
"sent message {} from {} to {}",
message.id,
message.account_email,
message.to.join(", ")
);
});
Ok(())
}
pub fn reply(&self, args: ReplyArgs) -> Result<()> {
let target = self.get_message(args.message_id)?;
let account = match args.account {
Some(account) => self.get_account(&normalize_email(&account))?,
None => self.get_account(&target.account_email)?,
};
ensure_reply_account_matches(&target, &account)?;
let (to, cc) = if args.all {
reply_all_recipients(&target, &account.email)
} else {
(reply_recipients(&target)?, Vec::new())
};
if to.is_empty() {
bail!("no recipients for reply (all addresses resolved to self)");
}
let subject = reply_subject(&target.subject);
let compose = ResolvedCompose {
account,
to,
cc,
bcc: Vec::new(),
subject,
text: read_optional_content(args.text, args.text_file)?,
html: read_optional_content(args.html, args.html_file)?,
attachments: args.attachments,
};
let headers = reply_headers_for_message(&target);
let message = self.send_compose(compose, Some((target.id, headers)))?;
print_success_or(self.format, &message, |message| {
println!("replied with message {}", message.id);
});
Ok(())
}
pub fn forward(&self, args: ForwardArgs) -> Result<()> {
let target = self.get_message(args.message_id)?;
let account = match args.account {
Some(account) => self.get_account(&normalize_email(&account))?,
None => self.get_account(&target.account_email)?,
};
let subject = forward_subject(&target.subject);
let (text, html) = format_forwarded_body(args.text.as_deref(), &target);
let compose = ResolvedCompose {
account,
to: normalize_emails(&args.to),
cc: normalize_emails(&args.cc),
bcc: normalize_emails(&args.bcc),
subject,
text,
html,
attachments: Vec::new(),
};
let message = self.send_compose(compose, None)?;
print_success_or(self.format, &message, |message| {
println!("forwarded message {} as {}", args.message_id, message.id);
});
Ok(())
}
pub fn resolve_compose(&self, compose: ComposeArgs) -> Result<ResolvedCompose> {
let account = match compose.account {
Some(account) => self.get_account(&normalize_email(&account))?,
None => self.default_account()?,
};
let text = read_optional_content(compose.text, compose.text_file)?;
let html = read_optional_content(compose.html, compose.html_file)?;
if text.is_none() && html.is_none() {
bail!("one of --text/--text-file or --html/--html-file is required");
}
Ok(ResolvedCompose {
account,
to: normalize_emails(&compose.to),
cc: normalize_emails(&compose.cc),
bcc: normalize_emails(&compose.bcc),
subject: compose.subject,
text,
html,
attachments: compose.attachments,
})
}
pub fn send_compose(
&self,
compose: ResolvedCompose,
reply_context: Option<(i64, ReplyHeaders)>,
) -> Result<MessageRecord> {
let client = self.client_for_profile(&compose.account.profile_name)?;
let mut text = compose.text.clone();
let mut html = compose.html.clone();
if !compose.account.signature.trim().is_empty() {
if text.is_some() {
text = Some(append_signature_text(
text.as_deref(),
&compose.account.signature,
));
}
if html.is_some() {
html = Some(append_signature_html(
html.as_deref(),
&compose.account.signature,
));
}
}
let message_id = generate_message_id(&compose.account.email);
let mut custom_headers = HashMap::new();
custom_headers.insert("Message-ID".to_string(), message_id.clone());
if let Some((_, ref reply)) = reply_context {
if let Some(in_reply_to) = reply.in_reply_to.as_deref() {
custom_headers.insert("In-Reply-To".to_string(), in_reply_to.to_string());
}
if !reply.references.is_empty() {
custom_headers.insert("References".to_string(), reply.references.join(" "));
}
}
let headers = if custom_headers.is_empty() {
None
} else {
Some(custom_headers)
};
let request = SendEmailRequest {
from: format_sender(
compose.account.display_name.as_deref(),
&compose.account.email,
),
to: compose.to.clone(),
cc: compose.cc.clone(),
bcc: compose.bcc.clone(),
subject: compose.subject.clone(),
text,
html,
headers,
attachments: build_send_attachments(&compose.attachments)?,
};
let idempotency_key = self.outbox_send(&request, &compose.account.email)?;
match client.send_email(&request, &idempotency_key) {
Ok(response) => {
self.outbox_mark_sent(&idempotency_key)?;
let detail =
fetch_sent_detail(&client, &response.id).unwrap_or_else(|| SentEmail {
id: response.id.clone(),
from: Some(request.from.clone()),
to: request.to.clone(),
cc: request.cc.clone(),
bcc: request.bcc.clone(),
reply_to: Vec::new(),
subject: Some(request.subject.clone()),
created_at: Some(now_timestamp()),
last_event: Some("sent".to_string()),
html: request.html.clone(),
text: request.text.clone(),
});
let reply_headers = reply_context.map(|(_, reply)| reply);
self.store_sent_message(&compose.account, detail, reply_headers, Some(message_id))
}
Err(err) => {
self.outbox_mark_failed(&idempotency_key, &err.to_string())?;
Err(err)
}
}
}
}