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

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-03 16:07:58 +01:00
1 parent 9003a50eb4
commit 2486856799
21 files changed
+538 -30

No files matched your search

+136 -1
View File
@@ -4,13 +4,15 @@ use axum::extract::Multipart;
use axum::extract::Path as AxumPath;
use axum::response::{IntoResponse, Response};
use axum::{Extension, Json};
use serde::Serialize;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::auth::session::SessionState;
use crate::config::AppConfig;
use crate::db;
use crate::error::AppError;
use crate::imap::client::{ImapClient, ImapCredentials};
use crate::storage::{self, BlobStorage, attachment_key};
#[derive(Debug, Serialize)]
@@ -26,6 +28,12 @@ struct DeleteResponse {
status: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AttachMessageSourceRequest {
folder: String,
uid: u32,
}
pub async fn upload_attachment(
Extension(session): Extension<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
@@ -174,6 +182,133 @@ pub async fn upload_attachment(
.into_response())
}
pub async fn attach_message_source(
Extension(session): Extension<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Extension(storage): Extension<Arc<dyn BlobStorage>>,
Extension(imap_client): Extension<Arc<dyn ImapClient>>,
AxumPath(draft_id): AxumPath<String>,
Json(req): Json<AttachMessageSourceRequest>,
) -> Result<Response, AppError> {
db::drafts::validate_draft_id(&draft_id).map_err(AppError::BadRequest)?;
if req.folder.trim().is_empty() || req.uid == 0 {
return Err(AppError::BadRequest(
"Message folder and UID are required".to_string(),
));
}
let creds = ImapCredentials {
host: session.imap_host.clone(),
port: session.imap_port,
tls: session.imap_tls,
tls_verify: session.imap_tls_verify,
email: session.email.clone(),
password: session.password.clone(),
};
let raw = imap_client
.fetch_raw_message(&creds, &req.folder, req.uid)
.await
.map_err(|e| AppError::ServiceUnavailable(format!("IMAP error: {e}")))?;
if raw.is_empty() {
return Err(AppError::BadRequest("Message source is empty".to_string()));
}
if raw.len() > config.max_attachment_size_bytes() {
return Err(AppError::BadRequest(format!(
"Message source exceeds maximum attachment size of {} MiB",
config.max_attachment_size_mb
)));
}
let conn = db::pool::open_user_db(&config.data_dir, &session.user_hash)
.map_err(|e| AppError::InternalError(format!("Failed to open database: {e}")))?;
if db::drafts::get_draft(&conn, &draft_id)
.map_err(AppError::InternalError)?
.is_none()
{
db::drafts::upsert_draft(
&conn, &draft_id, "", "", "", "", "", None, None, None, false, "normal", "html",
"none", false, false, false,
)
.map_err(AppError::InternalError)?;
}
let filename = format!("message-{}.eml", req.uid);
let content_type = "message/rfc822".to_string();
let att_id = uuid::Uuid::new_v4().to_string();
let key =
attachment_key(&session.user_hash, &draft_id, &att_id).map_err(AppError::InternalError)?;
let size = raw.len() as i64;
let checksum = format!("{:x}", Sha256::digest(&raw));
let mut writer = storage
.begin_upload(&key)
.await
.map_err(|e| AppError::ServiceUnavailable(format!("Storage unavailable: {e}")))?;
if let Err(error) = writer.write(Bytes::from(raw)).await {
if writer.abort().await.is_err()
&& let Err(cleanup_error) =
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
{
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue generated attachment cleanup");
}
return Err(AppError::ServiceUnavailable(format!(
"Failed to store message attachment: {error}"
)));
}
let stat = match writer.finish().await {
Ok(stat) => stat,
Err(error) => {
if let Err(cleanup_error) =
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
{
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue failed generated attachment cleanup");
}
return Err(AppError::ServiceUnavailable(format!(
"Failed to finalize message attachment: {error}"
)));
}
};
if stat.size != size as u64 {
if let Err(cleanup_error) =
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
{
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue invalid generated attachment cleanup");
}
return Err(AppError::InternalError(
"Message attachment size verification failed".to_string(),
));
}
let attachment = db::drafts::NewDraftAttachment {
id: &att_id,
draft_id: &draft_id,
filename: &filename,
content_type: &content_type,
size,
storage_key: &key,
checksum_sha256: Some(&checksum),
};
if let Err(error) = db::drafts::add_draft_attachment(&conn, &attachment) {
if let Err(cleanup_error) =
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
{
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue unattached generated blob cleanup");
}
return Err(AppError::InternalError(error));
}
Ok(Json(serde_json::json!({
"attachments": [UploadResponse {
id: att_id,
filename,
content_type,
size,
}],
}))
.into_response())
}
pub async fn get_attachment_content(
Extension(session): Extension<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,