mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
185 lines
6.9 KiB
Rust
185 lines
6.9 KiB
Rust
//
|
|
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
|
//
|
|
// This file is part of the Bichon Email Archiving Project
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
//use poem_openapi::Object;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
base64_decode_url_safe,
|
|
{
|
|
account::migration::{AccountModel, AccountType},
|
|
cache::imap::mailbox::{Attribute, AttributeEnum, MailBox},
|
|
envelope::extractor::extract_envelope_from_eml,
|
|
error::{BichonResult, code::ErrorCode},
|
|
utils::create_hash,
|
|
},
|
|
raise_error,
|
|
};
|
|
|
|
/// Skip individual emails larger than this after decoding (100 MB).
|
|
const MAX_SINGLE_EML_BYTES: usize = 100 * 1024 * 1024;
|
|
|
|
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "web-api", derive(poem_openapi::Object))]
|
|
pub struct BatchEmlRequest {
|
|
pub account_id: u64,
|
|
pub mail_folder: String,
|
|
/// A list of emails in base64-encoded format. Each element represents one .eml file.
|
|
pub emls: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "web-api", derive(poem_openapi::Object))]
|
|
pub struct FailedEmlDetail {
|
|
/// The 0-based index of the failed EML in the request list
|
|
pub index: usize,
|
|
/// The error message that caused the import to fail
|
|
pub error_message: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "web-api", derive(poem_openapi::Object))]
|
|
pub struct BatchEmlResult {
|
|
/// Total number of emails processed
|
|
pub total: usize,
|
|
/// Number of emails successfully imported
|
|
pub success: usize,
|
|
/// Number of emails failed to import
|
|
pub failed: usize,
|
|
/// A list of details for failed imports
|
|
pub failed_details: Vec<FailedEmlDetail>,
|
|
}
|
|
|
|
pub struct ImportEmls;
|
|
|
|
impl ImportEmls {
|
|
pub async fn do_import(mut request: BatchEmlRequest) -> BichonResult<BatchEmlResult> {
|
|
let account = AccountModel::check_account_exists(request.account_id)?;
|
|
|
|
if !account.enabled {
|
|
return Err(raise_error!("The account is disabled and cannot be used for this operation.".into(), ErrorCode::InvalidParameter));
|
|
}
|
|
|
|
let mailbox_id = match account.account_type {
|
|
AccountType::IMAP => {
|
|
let all_mailboxes = MailBox::list_all(account.id)?;
|
|
let mailbox = all_mailboxes.into_iter().find(|m| m.name == request.mail_folder);
|
|
|
|
match mailbox {
|
|
Some(mailbox) => mailbox.id,
|
|
None => return Err(raise_error!(
|
|
format!("Mail folder '{}' not found for account ID {}. The target folder must exist before importing.",
|
|
request.mail_folder,
|
|
request.account_id).into(),
|
|
ErrorCode::ResourceNotFound
|
|
)),
|
|
}
|
|
},
|
|
AccountType::NoSync => {
|
|
let mailbox = MailBox {
|
|
id: create_hash(request.account_id, &request.mail_folder),
|
|
account_id: request.account_id,
|
|
name: request.mail_folder.clone(),
|
|
delimiter: Some("/".to_string()),
|
|
attributes: vec![Attribute {
|
|
attr: AttributeEnum::Extension,
|
|
extension: Some("CreatedByBichon".into()),
|
|
}],
|
|
exists: 0,
|
|
unseen: None,
|
|
uid_next: None,
|
|
uid_validity: None,
|
|
};
|
|
let mailbox_id = mailbox.id;
|
|
// Upsert the mailbox, creating it if it doesn't exist
|
|
MailBox::batch_upsert(&[mailbox])?;
|
|
mailbox_id
|
|
},
|
|
};
|
|
|
|
let account_id = account.id;
|
|
let mut success_count = 0;
|
|
let mut failed_details: Vec<FailedEmlDetail> = Vec::new(); // Store failure details
|
|
|
|
let total = request.emls.len();
|
|
let mut index: usize = 0;
|
|
while let Some(eml_base64) = request.emls.pop() {
|
|
let decoded = match base64_decode_url_safe!(eml_base64.as_bytes()) {
|
|
Ok(bytes) => bytes,
|
|
Err(e) => {
|
|
let error_msg =
|
|
format!("Failed to decode base64 EML at index {}: {:?}", index, e);
|
|
tracing::error!("{}", error_msg);
|
|
failed_details.push(FailedEmlDetail {
|
|
index,
|
|
error_message: error_msg,
|
|
});
|
|
index += 1;
|
|
continue;
|
|
}
|
|
};
|
|
// eml_base64 string dropped here — frees base64 memory before parsing
|
|
|
|
if decoded.len() > MAX_SINGLE_EML_BYTES {
|
|
let size_mb = decoded.len() as f64 / 1024.0 / 1024.0;
|
|
let error_msg = format!(
|
|
"Email at index {} is {:.1} MB (limit 50 MB). Skipping.",
|
|
index, size_mb,
|
|
);
|
|
tracing::warn!("{}", error_msg);
|
|
failed_details.push(FailedEmlDetail {
|
|
index,
|
|
error_message: error_msg,
|
|
});
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
match extract_envelope_from_eml(&decoded, account_id, mailbox_id).await {
|
|
Ok(_) => {
|
|
success_count += 1;
|
|
},
|
|
Err(e) => {
|
|
let error_msg = format!(
|
|
"Failed to extract envelope from EML at index {}: {:?}",
|
|
index, e
|
|
);
|
|
tracing::error!("{}", error_msg);
|
|
failed_details.push(FailedEmlDetail {
|
|
index,
|
|
error_message: error_msg,
|
|
});
|
|
index += 1;
|
|
continue;
|
|
}
|
|
};
|
|
index += 1;
|
|
}
|
|
|
|
let failed_count = failed_details.len();
|
|
|
|
Ok(BatchEmlResult {
|
|
total,
|
|
success: success_count,
|
|
failed: failed_count,
|
|
failed_details, // Return the list of failure details
|
|
})
|
|
}
|
|
}
|