Merge pull request #67 from mmaudet/feat/envelope-endpoint-and-api-improvements

feat(api): Add envelope endpoint and improve API documentation
This commit is contained in:
rustmailer
2025-12-30 22:32:26 +08:00
committed by GitHub
4 changed files with 97 additions and 23 deletions

View File

@@ -808,6 +808,48 @@ impl EnvelopeIndexManager {
})
}
pub async fn get_envelope_by_id(
&self,
account_id: u64,
message_id: u64,
) -> BichonResult<Option<Envelope>> {
let searcher = self.create_searcher()?;
let f = SchemaTools::envelope_fields();
let query = BooleanQuery::new(vec![
(
Occur::Must,
Box::new(TermQuery::new(
Term::from_field_u64(f.f_account_id, account_id),
IndexRecordOption::Basic,
)),
),
(
Occur::Must,
Box::new(TermQuery::new(
Term::from_field_u64(f.f_id, message_id),
IndexRecordOption::Basic,
)),
),
]);
let docs: Vec<(f32, DocAddress)> = searcher
.search(&query, &TopDocs::with_limit(1))
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
if let Some((_, doc_address)) = docs.first() {
let doc: TantivyDocument = searcher
.doc_async(*doc_address)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
let envelope = Envelope::from_tantivy_doc(&doc).await?;
Ok(Some(envelope))
} else {
Ok(None)
}
}
pub async fn top_10_largest_emails(
&self,
accounts: &Option<HashSet<u64>>,

View File

@@ -66,7 +66,7 @@ impl MessageApi {
Ok(delete_messages_impl(request).await?)
}
/// Lists messages in a specified mailbox for the given account.
/// Lists messages in a mailbox. Requires `mailbox_id`, `page`, and `page_size` query parameters.
#[oai(
path = "/list-messages/:account_id",
method = "get",
@@ -74,7 +74,9 @@ impl MessageApi {
)]
async fn list_messages(
&self,
/// The ID of the account.
account_id: Path<u64>,
/// The ID of the mailbox to list messages from.
mailbox_id: Query<u64>,
page: Query<u64>,
page_size: Query<u64>,
@@ -90,7 +92,8 @@ impl MessageApi {
))
}
/// Lists messages in a specified mailbox for the given account.
/// Searches messages across all mailboxes using various filter criteria.
/// The search filters are provided in the request body.
#[oai(
path = "/search-messages",
method = "post",
@@ -112,7 +115,7 @@ impl MessageApi {
Ok(Json(search_messages_impl(authorized_ids, payload.0).await?))
}
/// Get thread's envelopes in a specified mailbox for the given account.
/// Retrieves all messages belonging to a specific thread. Requires `thread_id`, `page`, and `page_size` query parameters.
#[oai(
path = "/get-thread-messages/:account_id",
method = "get",
@@ -140,9 +143,9 @@ impl MessageApi {
))
}
/// Fetches the content of a specific email for the given account.
/// Fetches the content of a specific email.
#[oai(
path = "/message-content/:account_id",
path = "/message-content/:account_id/:message_id",
method = "get",
operation_id = "fetch_message_content"
)]
@@ -151,7 +154,7 @@ impl MessageApi {
/// The ID of the account.
account_id: Path<u64>,
/// The ID of the message to fetch.
message_id: Query<u64>,
message_id: Path<u64>,
context: ClientContext,
) -> ApiResult<Json<FullMessageContent>> {
let account_id = account_id.0;
@@ -161,9 +164,40 @@ impl MessageApi {
Ok(Json(retrieve_email_content(account_id, message_id.0).await?))
}
/// Fetches the full content of a specific email for the given account.
/// Retrieves the envelope (metadata) of a specific message.
#[oai(
path = "/download-message/:account_id",
path = "/envelope/:account_id/:message_id",
method = "get",
operation_id = "get_envelope"
)]
async fn get_envelope(
&self,
/// The ID of the account.
account_id: Path<u64>,
/// The ID of the message.
message_id: Path<u64>,
context: ClientContext,
) -> ApiResult<Json<Envelope>> {
let account_id = account_id.0;
context.require_account_access(account_id)?;
let envelope = ENVELOPE_INDEX_MANAGER
.get_envelope_by_id(account_id, message_id.0)
.await?
.ok_or_else(|| {
raise_error!(
format!(
"Envelope not found: account_id={} message_id={}",
account_id, message_id.0
),
ErrorCode::ResourceNotFound
)
})?;
Ok(Json(envelope))
}
/// Downloads the raw EML file of a specific email.
#[oai(
path = "/download-message/:account_id/:message_id",
method = "get",
operation_id = "download_message"
)]
@@ -172,7 +206,7 @@ impl MessageApi {
/// The ID of the account.
account_id: Path<u64>,
/// The ID of the message to download.
message_id: Query<u64>,
message_id: Path<u64>,
context: ClientContext,
) -> ApiResult<Attachment<Body>> {
let account_id = account_id.0;
@@ -180,8 +214,7 @@ impl MessageApi {
context
.require_permission(Some(account_id), Permission::DATA_RAW_DOWNLOAD)
.await?;
let message_id = message_id.0;
let reader = EML_INDEX_MANAGER.get_reader(account_id, message_id).await?;
let reader = EML_INDEX_MANAGER.get_reader(account_id, message_id.0).await?;
let body = Body::from_async_read(reader);
let attachment = Attachment::new(body)
.attachment_type(AttachmentType::Attachment)
@@ -189,9 +222,9 @@ impl MessageApi {
Ok(attachment)
}
/// Downloads a specific attachment by filename.
/// Downloads a specific attachment from an email. Requires `name` query parameter.
#[oai(
path = "/download-attachment/:account_id",
path = "/download-attachment/:account_id/:message_id",
method = "get",
operation_id = "download_attachment"
)]
@@ -200,7 +233,7 @@ impl MessageApi {
/// The ID of the account.
account_id: Path<u64>,
/// The ID of the message containing the attachment.
message_id: Query<u64>,
message_id: Path<u64>,
/// The filename of the attachment to download.
name: Query<String>,
context: ClientContext,
@@ -210,10 +243,9 @@ impl MessageApi {
context
.require_permission(Some(account_id), Permission::DATA_READ)
.await?;
let message_id = message_id.0;
let name = name.0.trim();
let reader = EML_INDEX_MANAGER
.get_attachment(account_id, message_id, name)
.get_attachment(account_id, message_id.0, name)
.await?;
let body = Body::from_async_read(reader);
let attachment = Attachment::new(body)

View File

@@ -86,7 +86,7 @@ impl SystemApi {
#[oai(path = "/proxy/:id", method = "delete", operation_id = "remove_proxy")]
async fn remove_proxy(
&self,
/// The name of the OAuth2 configuration to retrieve
/// The ID of the proxy configuration to delete.
id: Path<u64>,
context: ClientContext,
) -> ApiResult<()> {
@@ -96,11 +96,11 @@ impl SystemApi {
Ok(Proxy::delete(id.0).await?)
}
/// Retrieve a specific proxy configuration by ID
/// Retrieve a specific proxy configuration by ID. Requires root permission.
#[oai(path = "/proxy/:id", method = "get", operation_id = "get_proxy")]
async fn get_proxy(
&self,
/// The name of the OAuth2 configuration to retrieve
/// The ID of the proxy configuration to retrieve.
id: Path<u64>,
context: ClientContext,
) -> ApiResult<Json<Proxy>> {

View File

@@ -48,7 +48,7 @@ export const get_thread_messages = async (accountId: number, thread_id: number,
}
export const download_attachment = async (accountId: number, id: number, attachmentFileName: string) => {
const response = await axiosInstance.get(`/api/v1/download-attachment/${accountId}?message_id=${id}&name=${attachmentFileName}`, { responseType: 'blob' });
const response = await axiosInstance.get(`/api/v1/download-attachment/${accountId}/${id}?name=${attachmentFileName}`, { responseType: 'blob' });
const blob = new Blob([response.data]);
saveAs(blob, attachmentFileName);
};
@@ -83,7 +83,7 @@ export const getContent = (messageContent: MessageContentResponse): string | nul
};
export const load_message = async (accountId: number, id: number) => {
const response = await axiosInstance.get<MessageContentResponse>(`/api/v1/message-content/${accountId}?message_id=${id}`);
const response = await axiosInstance.get<MessageContentResponse>(`/api/v1/message-content/${accountId}/${id}`);
return response.data;
};
@@ -93,7 +93,7 @@ export const delete_messages = async (payload: Record<string, number[]>) => {
};
export const download_message = async (accountId: number, id: number) => {
const response = await axiosInstance.get(`/api/v1/download-message/${accountId}?message_id=${id}`, { responseType: 'blob' });
const response = await axiosInstance.get(`/api/v1/download-message/${accountId}/${id}`, { responseType: 'blob' });
const blob = new Blob([response.data]);
saveAs(blob, `${id}.eml`);
};
};