1 parent
9003a50eb4
commit
2486856799
21 files changed
+538
-30
No files matched your search
+136
-1
@@ -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>>,
|
||||
|
||||
Reference in new issue
Block a user