Files
bichon/crates/core/src/import/mod.rs
T

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
})
}
}