feat: support restoring single message to IMAP #77

This commit is contained in:
rustmailer
2025-12-31 22:56:06 +08:00
parent 1c58b516dd
commit a6216c2ce6
33 changed files with 618 additions and 25 deletions
+16 -7
View File
@@ -79,12 +79,26 @@ impl ImapExecutor {
Ok(result)
}
pub async fn append(
&self,
mailbox_name: impl AsRef<str>,
flags: Option<&str>,
internaldate: Option<&str>,
content: impl AsRef<[u8]>,
) -> BichonResult<()> {
let mut session = self.get_connection().await?;
session
.append(mailbox_name, flags, internaldate, content)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))
}
pub async fn fetch_new_mail(
&self,
account: &AccountModel,
mailbox: &MailBox,
start_uid: u64,
before: Option<&str>
before: Option<&str>,
) -> BichonResult<()> {
assert!(start_uid > 0, "start_uid must be greater than 0");
@@ -93,12 +107,7 @@ impl ImapExecutor {
None => format!("UID {start_uid}:*"),
};
let uid_list = self
.uid_search(
&mailbox.encoded_name(),
&query,
)
.await?;
let uid_list = self.uid_search(&mailbox.encoded_name(), &query).await?;
let len = uid_list.len();
if len == 0 {
+104
View File
@@ -0,0 +1,104 @@
use crate::{
encode_mailbox_name,
modules::{
account::migration::{AccountModel, AccountType},
context::executors::MAIL_CONTEXT,
error::{code::ErrorCode, BichonResult},
indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
},
raise_error,
};
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
const MAX_RESTORE_COUNT: usize = 100;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
pub struct RestoreMessagesRequest {
/// Message IDs to restore (max 100)
pub message_ids: Vec<u64>,
}
pub async fn restore_emails(account_id: u64, message_ids: Vec<u64>) -> BichonResult<()> {
if message_ids.len() > MAX_RESTORE_COUNT {
return Err(raise_error!(
format!(
"Too many messages to restore: {} (max {})",
message_ids.len(),
MAX_RESTORE_COUNT
),
ErrorCode::InvalidParameter
));
}
let account = AccountModel::check_account_exists(account_id).await?;
if !matches!(account.account_type, AccountType::IMAP) {
return Err(raise_error!(
"Account type is not IMAP".into(),
ErrorCode::Incompatible
));
}
let executor = MAIL_CONTEXT.imap(account.id).await?;
let mut failed = Vec::new();
for message_id in message_ids {
let result: BichonResult<()> = async {
let envelope = ENVELOPE_INDEX_MANAGER
.get_envelope_by_id(account_id, message_id)
.await?
.ok_or_else(|| {
raise_error!(
format!(
"Envelope not found: account_id={} message_id={}",
account_id, message_id
),
ErrorCode::ResourceNotFound
)
})?;
let eml = EML_INDEX_MANAGER
.get(account_id, message_id)
.await?
.ok_or_else(|| {
raise_error!(
format!(
"Email record not found: account_id={} id={}",
account_id, message_id
),
ErrorCode::ResourceNotFound
)
})?;
if let Some(mailbox_name) = envelope.mailbox_name {
executor
.append(encode_mailbox_name!(&mailbox_name), None, None, &eml)
.await?;
}
Ok(())
}
.await;
if let Err(err) = result {
failed.push(message_id);
tracing::warn!(
account_id = account_id,
message_id = message_id,
error = ?err,
"Failed to restore email"
);
}
}
if !failed.is_empty() {
tracing::info!(
account_id = account_id,
failed_count = failed.len(),
failed_message_ids = ?failed,
"Restore emails finished with partial failures"
);
}
Ok(())
}
+1 -1
View File
@@ -16,7 +16,7 @@
// 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 append;
pub mod content;
pub mod delete;
pub mod list;
+21
View File
@@ -21,6 +21,8 @@ use crate::modules::common::auth::ClientContext;
use crate::modules::indexer::envelope::Envelope;
use crate::modules::indexer::manager::EML_INDEX_MANAGER;
use crate::modules::indexer::manager::ENVELOPE_INDEX_MANAGER;
use crate::modules::message::append::restore_emails;
use crate::modules::message::append::RestoreMessagesRequest;
use crate::modules::message::content::{retrieve_email_content, FullMessageContent};
use crate::modules::message::delete::delete_messages_impl;
use crate::modules::message::list::{get_thread_messages, list_messages_impl};
@@ -227,6 +229,25 @@ impl MessageApi {
Ok(attachment)
}
#[oai(
path = "/restore-messages/:account_id",
method = "post",
operation_id = "restore_messages"
)]
async fn restore_messages(
&self,
account_id: Path<u64>,
/// Message IDs to restore.
payload: Json<RestoreMessagesRequest>,
context: ClientContext,
) -> ApiResult<()> {
let account_id = account_id.0;
context
.require_permission(Some(account_id), Permission::DATA_EXPORT_BATCH)
.await?;
Ok(restore_emails(account_id, payload.0.message_ids).await?)
}
/// Downloads a specific attachment from an email. Requires `name` query parameter.
#[oai(
path = "/download-attachment/:account_id/:message_id",