1 parent
96a32ea8fb
commit
a643e7e3ab
106 files changed
+2223
-4669
No files matched your search
+10
-39
@@ -26,29 +26,26 @@ pub struct SendRequest {
|
||||
pub html_body: Option<String>,
|
||||
pub in_reply_to: Option<String>,
|
||||
pub references: Option<String>,
|
||||
/// If sending from a draft, include the draft ID to load attachments
|
||||
/// and clean up after send.
|
||||
|
||||
pub draft_id: Option<String>,
|
||||
/// Optional identity ID to use as the From address.
|
||||
/// If not provided, falls back to the session email.
|
||||
|
||||
pub from_identity_id: Option<i64>,
|
||||
/// Request an MDN/read receipt at the selected identity/account address.
|
||||
|
||||
#[serde(default)]
|
||||
pub request_mdn: bool,
|
||||
/// Message priority: high, normal, or low.
|
||||
|
||||
#[serde(default = "default_priority")]
|
||||
pub priority: String,
|
||||
/// OpenPGP mode: none, sign, encrypt, or sign-encrypt. When omitted,
|
||||
/// identity defaults are used.
|
||||
|
||||
pub pgp_mode: Option<String>,
|
||||
/// Override identity default for legacy inline OpenPGP. PGP/MIME is the default.
|
||||
|
||||
pub pgp_inline: Option<bool>,
|
||||
/// Passphrase for the selected private key. Never persisted.
|
||||
|
||||
pub pgp_passphrase: Option<String>,
|
||||
/// Attach the sender's public OpenPGP key as application/pgp-keys.
|
||||
|
||||
#[serde(default)]
|
||||
pub pgp_attach_public_key: bool,
|
||||
/// Attach public OpenPGP keys for visible To/Cc recipients. Bcc is intentionally excluded.
|
||||
|
||||
#[serde(default)]
|
||||
pub pgp_attach_recipient_keys: bool,
|
||||
}
|
||||
@@ -70,10 +67,6 @@ pub struct SendResponse {
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
/// Handler for `POST /api/messages/send`.
|
||||
///
|
||||
/// Validates the request, sends the message via SMTP, and appends a copy
|
||||
/// to the IMAP "Sent" folder (best-effort).
|
||||
pub async fn send_message_handler(
|
||||
Extension(session): Extension<SessionState>,
|
||||
Extension(config): Extension<Arc<AppConfig>>,
|
||||
@@ -85,14 +78,12 @@ pub async fn send_message_handler(
|
||||
db::drafts::validate_draft_id(draft_id).map_err(AppError::BadRequest)?;
|
||||
}
|
||||
|
||||
// Validate: at least one recipient.
|
||||
if req.to.is_empty() && req.cc.is_empty() && req.bcc.is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"At least one recipient is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate: subject or body must be non-empty.
|
||||
let has_subject = !req.subject.trim().is_empty();
|
||||
let has_text = req
|
||||
.text_body
|
||||
@@ -115,10 +106,6 @@ pub async fn send_message_handler(
|
||||
));
|
||||
}
|
||||
|
||||
// Build SMTP credentials from the authenticated account session.
|
||||
// Server endpoints are resolved from RustBox configuration at login and stored in the session.
|
||||
// Normal login falls back from an unset smtp_host to the IMAP host, but reject
|
||||
// an empty endpoint defensively for stale/legacy sessions.
|
||||
if session.smtp_host.trim().is_empty() {
|
||||
return Err(AppError::ServiceUnavailable(
|
||||
"SMTP server is not configured for this account".to_string(),
|
||||
@@ -133,7 +120,6 @@ pub async fn send_message_handler(
|
||||
password: session.password.clone(),
|
||||
};
|
||||
|
||||
// Resolve identity and OpenPGP options before the first network await.
|
||||
let conn = db::pool::open_user_db(&config.data_dir, &session.user_hash)
|
||||
.map_err(|e| AppError::InternalError(format!("Failed to open database: {e}")))?;
|
||||
let selected_identity = if let Some(identity_id) = req.from_identity_id {
|
||||
@@ -231,8 +217,6 @@ pub async fn send_message_handler(
|
||||
)?;
|
||||
drop(conn);
|
||||
|
||||
// Validate every envelope/header mailbox before touching SMTP. Lettre then
|
||||
// performs the actual SMTPUTF8 capability negotiation during delivery.
|
||||
validate_mailbox("From", &from_address)?;
|
||||
for address in &req.to {
|
||||
validate_mailbox("To", address)?;
|
||||
@@ -250,7 +234,6 @@ pub async fn send_message_handler(
|
||||
validate_mailbox("read receipt", &receipt_address)?;
|
||||
}
|
||||
|
||||
// Load attachments from draft if draft_id is provided.
|
||||
let mut attachments = if let Some(ref draft_id) = req.draft_id {
|
||||
load_draft_attachments(&config.data_dir, &session.user_hash, draft_id)?
|
||||
} else {
|
||||
@@ -283,7 +266,6 @@ pub async fn send_message_handler(
|
||||
});
|
||||
}
|
||||
|
||||
// Build the sendable message.
|
||||
let message = SendableMessage {
|
||||
from: from_address,
|
||||
to: req.to,
|
||||
@@ -306,8 +288,6 @@ pub async fn send_message_handler(
|
||||
max_message_size_bytes: config.max_message_size_bytes(),
|
||||
};
|
||||
|
||||
// Send via SMTP. The exact RFC822 bytes returned here are also appended
|
||||
// to IMAP Sent so PGP/MIME signatures and encryption remain identical.
|
||||
let sent = smtp_client
|
||||
.send_message(&smtp_creds, &message)
|
||||
.await
|
||||
@@ -323,8 +303,6 @@ pub async fn send_message_handler(
|
||||
})?;
|
||||
let message_id = sent.message_id.clone();
|
||||
|
||||
// Best-effort: append a copy to the "Sent" folder via the account's IMAP server.
|
||||
// Don't fail the SMTP send if this step fails.
|
||||
let imap_creds = ImapCredentials {
|
||||
host: session.imap_host.clone(),
|
||||
port: session.imap_port,
|
||||
@@ -364,8 +342,6 @@ pub async fn send_message_handler(
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the remote Drafts snapshot only after SMTP delivery succeeds.
|
||||
// The UID is verified against the stored UIDVALIDITY before deletion.
|
||||
if let Some(ref draft_id) = req.draft_id {
|
||||
if let Ok(conn) = db::pool::open_user_db(&config.data_dir, &session.user_hash)
|
||||
&& let Ok(Some(draft)) = db::drafts::get_draft(&conn, draft_id)
|
||||
@@ -387,7 +363,6 @@ pub async fn send_message_handler(
|
||||
.into_response())
|
||||
}
|
||||
|
||||
/// Load attachment data from disk for a given draft.
|
||||
fn load_draft_attachments(
|
||||
data_dir: &str,
|
||||
user_hash: &str,
|
||||
@@ -417,16 +392,13 @@ fn load_draft_attachments(
|
||||
Ok(attachments)
|
||||
}
|
||||
|
||||
/// Clean up draft record and attachment files from disk after successful send.
|
||||
async fn cleanup_draft(data_dir: &str, user_hash: &str, draft_id: &str) {
|
||||
// Delete draft from DB (cascade deletes attachment records).
|
||||
if let Ok(conn) = db::pool::open_user_db(data_dir, user_hash)
|
||||
&& let Err(e) = db::drafts::delete_draft(&conn, draft_id)
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to delete draft after send");
|
||||
}
|
||||
|
||||
// Remove the attachment directory from disk.
|
||||
let att_dir = Path::new(data_dir)
|
||||
.join(user_hash)
|
||||
.join("attachments")
|
||||
@@ -581,7 +553,6 @@ fn build_pgp_send_config(
|
||||
}
|
||||
}
|
||||
|
||||
// Include the sender's public key when available so the Sent copy can be decrypted later.
|
||||
if let Some(key) = db::pgp::find_key_internal_by_email(conn, sender_email)
|
||||
.map_err(AppError::InternalError)?
|
||||
{
|
||||
@@ -591,7 +562,7 @@ fn build_pgp_send_config(
|
||||
}
|
||||
}
|
||||
|
||||
let _ = (data_dir, user_hash); // kept in the signature for future key-agent storage without API churn
|
||||
let _ = (data_dir, user_hash);
|
||||
Ok(Some(PgpSendConfig {
|
||||
mode,
|
||||
inline,
|
||||
|
||||
Reference in new issue
Block a user