v0.0.5
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 12:06:44 +01:00
1 parent 96a32ea8fb
commit a643e7e3ab
106 files changed
+2223 -4669

No files matched your search

+10 -39
View File
@@ -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,