initial commit

This commit is contained in:
rustmailer
2025-11-19 02:14:37 +08:00
commit 1a8f95117e
355 changed files with 54089 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
//
// Copyright (c) 2025 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 crate::base64_encode;
use crate::modules::account::migration::AccountModel;
use crate::modules::error::code::ErrorCode;
use crate::modules::indexer::manager::EML_INDEX_MANAGER;
use crate::{modules::error::BichonResult, raise_error};
use mail_parser::{MessageParser, MimeHeaders};
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
/// Represents metadata of an attachment in a Gmail message.
///
/// This struct stores information required to identify, download,
/// and render an attachment, including inline images embedded
/// in HTML emails.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
pub struct AttachmentInfo {
/// MIME content type of the attachment (e.g., `image/png`, `application/pdf`).
pub file_type: String,
/// Whether the attachment is marked as inline (true) or a regular file (false).
pub inline: bool,
/// Original filename of the attachment, if provided.
pub filename: String,
/// Size of the attachment in bytes.
pub size: usize,
pub content_id: Option<String>,
}
/// Represents the content of an email message in both plain text and HTML formats.
///
/// This struct contains optional fields for plain text and HTML versions of
/// the email message body. At least one of them may be present.
///
/// # Fields
///
/// - `plain`: The plain text version of the message, if available.
/// - `html`: The HTML version of the message, if available.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
pub struct FullMessageContent {
/// Optional plain text version of the message.
pub text: Option<String>,
/// Optional HTML version of the message.
pub html: Option<String>,
// all Attachments include inline attachments
pub attachments: Option<Vec<AttachmentInfo>>,
}
pub async fn retrieve_email_content(
account_id: u64,
id: u64,
) -> BichonResult<FullMessageContent> {
AccountModel::check_account_active(account_id).await?;
let eml = EML_INDEX_MANAGER
.get(account_id, id)
.await?
.ok_or_else(|| {
raise_error!(
format!(
"Email record not found: account_id={} id={}",
account_id, id
),
ErrorCode::ResourceNotFound
)
})?;
let message = MessageParser::default().parse(&eml).ok_or_else(|| {
raise_error!(
format!(
"Failed to parse EML data (id={}) — the message may be corrupted.",
id
),
ErrorCode::InternalError
)
})?;
let mut html: Option<String> = message.body_html(0).map(|cow| cow.into_owned());
let text: Option<String> = message.body_text(0).map(|cow| cow.into_owned());
let mut attachments = Vec::new();
for attachment in message.attachments() {
let content_type = attachment.content_type().ok_or_else(|| {
raise_error!(
format!("Attachment is missing Content-Type (email id={})", id),
ErrorCode::InternalError
)
})?;
let filename = attachment
.attachment_name()
.map(|name| name.to_string())
.unwrap_or_else(|| format!("email{}_attachment{}", id, attachment.raw_body_offset()));
let disposition = attachment.content_disposition();
let file_type = format!(
"{}/{}",
content_type.c_type.as_ref(),
content_type.c_subtype.as_deref().unwrap_or("")
);
let inline = disposition.map(|d| d.is_inline()).unwrap_or(false);
if inline {
if let Some(html1) = html.as_deref() {
if let Some(cid) = attachment.content_id() {
if html1.contains(cid) {
let data = attachment.contents();
let base64_encoded = base64_encode!(data);
let html_content = html1.replace(
&format!("cid:{}", cid),
&format!("data:{};base64,{}", file_type, base64_encoded),
);
html = Some(html_content);
}
}
}
}
attachments.push(AttachmentInfo {
filename,
size: attachment.len(),
inline,
file_type,
content_id: attachment.content_id().map(Into::into),
});
}
Ok(FullMessageContent {
text,
html,
attachments: Some(attachments),
})
}
+31
View File
@@ -0,0 +1,31 @@
//
// Copyright (c) 2025 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 crate::modules::error::BichonResult;
use crate::modules::indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER};
use std::collections::HashMap;
pub async fn delete_messages_impl(request: HashMap<u64, Vec<u64>>) -> BichonResult<()> {
EML_INDEX_MANAGER
.delete_email_multi_account(&request)
.await?;
ENVELOPE_INDEX_MANAGER
.delete_envelopes_multi_account(&request)
.await
}
+69
View File
@@ -0,0 +1,69 @@
//
// Copyright (c) 2025 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 crate::{
modules::{
account::migration::AccountModel,
error::{code::ErrorCode, BichonResult},
indexer::{envelope::Envelope, manager::ENVELOPE_INDEX_MANAGER},
rest::response::DataPage,
},
raise_error,
};
pub async fn list_messages_impl(
account_id: u64,
mailbox_id: u64,
page: u64,
page_size: u64,
) -> BichonResult<DataPage<Envelope>> {
AccountModel::check_account_active(account_id).await?;
validate_pagination_params(page, page_size)?;
ENVELOPE_INDEX_MANAGER
.list_mailbox_envelopes(account_id, mailbox_id, page, page_size, true)
.await
}
fn validate_pagination_params(page: u64, page_size: u64) -> BichonResult<()> {
if page == 0 || page_size == 0 {
return Err(raise_error!(
"Both page and page_size must be greater than 0.".into(),
ErrorCode::InvalidParameter
));
}
if page_size > 500 {
return Err(raise_error!(
"The page_size exceeds the maximum allowed limit of 500.".into(),
ErrorCode::InvalidParameter
));
}
Ok(())
}
pub async fn get_thread_messages(
account_id: u64,
thread_id: u64,
page: u64,
page_size: u64,
) -> BichonResult<DataPage<Envelope>> {
AccountModel::check_account_active(account_id).await?;
ENVELOPE_INDEX_MANAGER
.list_thread_envelopes(account_id, thread_id, page, page_size, true)
.await
}
+24
View File
@@ -0,0 +1,24 @@
//
// Copyright (c) 2025 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/>.
pub mod content;
pub mod delete;
pub mod list;
pub mod search;
pub mod tags;
+80
View File
@@ -0,0 +1,80 @@
//
// Copyright (c) 2025 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::{
modules::{
error::{code::ErrorCode, BichonResult},
indexer::{envelope::Envelope, manager::ENVELOPE_INDEX_MANAGER},
rest::response::DataPage,
},
raise_error,
};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct SearchFilter {
pub text: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub cc: Option<String>,
pub bcc: Option<String>,
pub since: Option<i64>,
pub before: Option<i64>,
pub account_id: Option<u64>,
pub mailbox_id: Option<u64>,
pub min_size: Option<u64>,
pub max_size: Option<u64>,
pub message_id: Option<String>,
pub has_attachment: Option<bool>,
pub attachment_name: Option<String>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct SearchRequest {
filter: SearchFilter,
page: u64,
page_size: u64,
}
impl SearchRequest {
pub fn validate(&self) -> BichonResult<()> {
if self.page == 0 || self.page_size == 0 {
return Err(raise_error!(
"Both page and page_size must be greater than 0.".into(),
ErrorCode::InvalidParameter
));
}
if self.page_size > 500 {
return Err(raise_error!(
"The page_size exceeds the maximum allowed limit of 500.".into(),
ErrorCode::InvalidParameter
));
}
Ok(())
}
}
pub async fn search_messages_impl(request: SearchRequest) -> BichonResult<DataPage<Envelope>> {
request.validate()?;
ENVELOPE_INDEX_MANAGER
.search(request.filter, request.page, request.page_size, true)
.await
}
+35
View File
@@ -0,0 +1,35 @@
//
// Copyright (c) 2025 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 std::collections::HashMap;
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct UpdateTagsRequest {
pub updates: HashMap<u64, Vec<u64>>, // account_id -> envelope_ids
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct TagCount {
pub tag: String,
pub count: u64,
}