mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat(api): Add envelope endpoint and improve API documentation
- Add GET /envelope/{account_id}/{message_id} endpoint to retrieve message envelope (metadata)
- Add get_envelope_by_id method to ENVELOPE_INDEX_MANAGER for querying single envelope
- Move message_id from query parameter to path parameter for clearer API paths:
- /message-content/{account_id}/{message_id}
- /download-message/{account_id}/{message_id}
- /download-attachment/{account_id}/{message_id}
- /envelope/{account_id}/{message_id}
- Fix API documentation descriptions to be more accurate:
- search_messages: Now correctly describes search functionality
- get_thread_messages: Mentions thread_id requirement
- proxy endpoints: Fixed copy-paste errors from OAuth2 docs
- Update frontend API client to use new path-based URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
c05a8944ef
commit
70db81dc03
Generated
+1
-1
@@ -424,7 +424,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bichon"
|
name = "bichon"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"async-imap",
|
"async-imap",
|
||||||
|
|||||||
@@ -741,6 +741,47 @@ 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) -> BichonResult<Vec<LargestEmail>> {
|
pub async fn top_10_largest_emails(&self) -> BichonResult<Vec<LargestEmail>> {
|
||||||
self.reader
|
self.reader
|
||||||
.reload()
|
.reload()
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ use crate::modules::rest::response::DataPage;
|
|||||||
use crate::modules::rest::ApiResult;
|
use crate::modules::rest::ApiResult;
|
||||||
use crate::modules::rest::ErrorCode;
|
use crate::modules::rest::ErrorCode;
|
||||||
use crate::raise_error;
|
use crate::raise_error;
|
||||||
use poem::web::Path;
|
|
||||||
use poem::Body;
|
use poem::Body;
|
||||||
use poem_openapi::param::Query;
|
use poem_openapi::param::{Path, Query};
|
||||||
use poem_openapi::payload::{Attachment, AttachmentType, Json};
|
use poem_openapi::payload::{Attachment, AttachmentType, Json};
|
||||||
use poem_openapi::OpenApi;
|
use poem_openapi::OpenApi;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -63,7 +62,7 @@ impl MessageApi {
|
|||||||
Ok(delete_messages_impl(request).await?)
|
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(
|
#[oai(
|
||||||
path = "/list-messages/:account_id",
|
path = "/list-messages/:account_id",
|
||||||
method = "get",
|
method = "get",
|
||||||
@@ -71,7 +70,9 @@ impl MessageApi {
|
|||||||
)]
|
)]
|
||||||
async fn list_messages(
|
async fn list_messages(
|
||||||
&self,
|
&self,
|
||||||
|
/// The ID of the account.
|
||||||
account_id: Path<u64>,
|
account_id: Path<u64>,
|
||||||
|
/// The ID of the mailbox to list messages from.
|
||||||
mailbox_id: Query<u64>,
|
mailbox_id: Query<u64>,
|
||||||
page: Query<u64>,
|
page: Query<u64>,
|
||||||
page_size: Query<u64>,
|
page_size: Query<u64>,
|
||||||
@@ -85,7 +86,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(
|
#[oai(
|
||||||
path = "/search-messages",
|
path = "/search-messages",
|
||||||
method = "post",
|
method = "post",
|
||||||
@@ -100,7 +102,7 @@ impl MessageApi {
|
|||||||
Ok(Json(search_messages_impl(payload.0).await?))
|
Ok(Json(search_messages_impl(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(
|
#[oai(
|
||||||
path = "/get-thread-messages/:account_id",
|
path = "/get-thread-messages/:account_id",
|
||||||
method = "get",
|
method = "get",
|
||||||
@@ -126,67 +128,105 @@ impl MessageApi {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the content of a specific email for the given account.
|
/// Fetches the content of a specific email.
|
||||||
#[oai(
|
#[oai(
|
||||||
path = "/message-content/:account_id",
|
path = "/message-content/:account_id/:message_id",
|
||||||
method = "get",
|
method = "get",
|
||||||
operation_id = "fetch_message_content"
|
operation_id = "fetch_message_content"
|
||||||
)]
|
)]
|
||||||
async fn fetch_message_content(
|
async fn fetch_message_content(
|
||||||
&self,
|
&self,
|
||||||
|
/// The ID of the account.
|
||||||
account_id: Path<u64>,
|
account_id: Path<u64>,
|
||||||
id: Query<u64>,
|
/// The ID of the message to fetch.
|
||||||
|
message_id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Json<FullMessageContent>> {
|
) -> ApiResult<Json<FullMessageContent>> {
|
||||||
let account_id = account_id.0;
|
let account_id = account_id.0;
|
||||||
context.require_account_access(account_id)?;
|
context.require_account_access(account_id)?;
|
||||||
Ok(Json(retrieve_email_content(account_id, id.0).await?))
|
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(
|
#[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",
|
method = "get",
|
||||||
operation_id = "download_message"
|
operation_id = "download_message"
|
||||||
)]
|
)]
|
||||||
async fn download_message(
|
async fn download_message(
|
||||||
&self,
|
&self,
|
||||||
|
/// The ID of the account.
|
||||||
account_id: Path<u64>,
|
account_id: Path<u64>,
|
||||||
id: Query<u64>,
|
/// The ID of the message to download.
|
||||||
|
message_id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Attachment<Body>> {
|
) -> ApiResult<Attachment<Body>> {
|
||||||
let account_id = account_id.0;
|
let account_id = account_id.0;
|
||||||
AccountModel::check_account_exists(account_id).await?;
|
AccountModel::check_account_exists(account_id).await?;
|
||||||
context.require_account_access(account_id)?;
|
context.require_account_access(account_id)?;
|
||||||
let id = id.0;
|
let message_id = message_id.0;
|
||||||
let reader = EML_INDEX_MANAGER.get_reader(account_id, id).await?;
|
let reader = EML_INDEX_MANAGER.get_reader(account_id, message_id).await?;
|
||||||
let body = Body::from_async_read(reader);
|
let body = Body::from_async_read(reader);
|
||||||
let attachment = Attachment::new(body)
|
let attachment = Attachment::new(body)
|
||||||
.attachment_type(AttachmentType::Attachment)
|
.attachment_type(AttachmentType::Attachment)
|
||||||
.filename(format!("{id}.eml"));
|
.filename(format!("{message_id}.eml"));
|
||||||
Ok(attachment)
|
Ok(attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads a specific attachment by filename.
|
/// Downloads a specific attachment from an email. Requires `name` query parameter.
|
||||||
#[oai(
|
#[oai(
|
||||||
path = "/download-attachment/:account_id",
|
path = "/download-attachment/:account_id/:message_id",
|
||||||
method = "get",
|
method = "get",
|
||||||
operation_id = "download_attachment"
|
operation_id = "download_attachment"
|
||||||
)]
|
)]
|
||||||
async fn download_attachment(
|
async fn download_attachment(
|
||||||
&self,
|
&self,
|
||||||
|
/// The ID of the account.
|
||||||
account_id: Path<u64>,
|
account_id: Path<u64>,
|
||||||
id: Query<u64>,
|
/// The ID of the message containing the attachment.
|
||||||
|
message_id: Path<u64>,
|
||||||
|
/// The filename of the attachment to download.
|
||||||
name: Query<String>,
|
name: Query<String>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Attachment<Body>> {
|
) -> ApiResult<Attachment<Body>> {
|
||||||
let account_id = account_id.0;
|
let account_id = account_id.0;
|
||||||
AccountModel::check_account_exists(account_id).await?;
|
AccountModel::check_account_exists(account_id).await?;
|
||||||
context.require_account_access(account_id)?;
|
context.require_account_access(account_id)?;
|
||||||
let email_id = id.0;
|
let message_id = message_id.0;
|
||||||
let name = name.0.trim();
|
let name = name.0.trim();
|
||||||
let reader = EML_INDEX_MANAGER
|
let reader = EML_INDEX_MANAGER
|
||||||
.get_attachment(account_id, email_id, name)
|
.get_attachment(account_id, message_id, name)
|
||||||
.await?;
|
.await?;
|
||||||
let body = Body::from_async_read(reader);
|
let body = Body::from_async_read(reader);
|
||||||
let attachment = Attachment::new(body)
|
let attachment = Attachment::new(body)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ impl SystemApi {
|
|||||||
#[oai(path = "/proxy/:id", method = "delete", operation_id = "remove_proxy")]
|
#[oai(path = "/proxy/:id", method = "delete", operation_id = "remove_proxy")]
|
||||||
async fn remove_proxy(
|
async fn remove_proxy(
|
||||||
&self,
|
&self,
|
||||||
/// The name of the OAuth2 configuration to retrieve
|
/// The ID of the proxy configuration to delete.
|
||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
@@ -86,11 +86,11 @@ impl SystemApi {
|
|||||||
Ok(Proxy::delete(id.0).await?)
|
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")]
|
#[oai(path = "/proxy/:id", method = "get", operation_id = "get_proxy")]
|
||||||
async fn get_proxy(
|
async fn get_proxy(
|
||||||
&self,
|
&self,
|
||||||
/// The name of the OAuth2 configuration to retrieve
|
/// The ID of the proxy configuration to retrieve.
|
||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Json<Proxy>> {
|
) -> ApiResult<Json<Proxy>> {
|
||||||
|
|||||||
@@ -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) => {
|
export const download_attachment = async (accountId: number, id: number, attachmentFileName: string) => {
|
||||||
const response = await axiosInstance.get(`/api/v1/download-attachment/${accountId}?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]);
|
const blob = new Blob([response.data]);
|
||||||
saveAs(blob, attachmentFileName);
|
saveAs(blob, attachmentFileName);
|
||||||
};
|
};
|
||||||
@@ -83,7 +83,7 @@ export const getContent = (messageContent: MessageContentResponse): string | nul
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const load_message = async (accountId: number, id: number) => {
|
export const load_message = async (accountId: number, id: number) => {
|
||||||
const response = await axiosInstance.get<MessageContentResponse>(`/api/v1/message-content/${accountId}?id=${id}`);
|
const response = await axiosInstance.get<MessageContentResponse>(`/api/v1/message-content/${accountId}/${id}`);
|
||||||
return response.data;
|
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) => {
|
export const download_message = async (accountId: number, id: number) => {
|
||||||
const response = await axiosInstance.get(`/api/v1/download-message/${accountId}?id=${id}`, { responseType: 'blob' });
|
const response = await axiosInstance.get(`/api/v1/download-message/${accountId}/${id}`, { responseType: 'blob' });
|
||||||
const blob = new Blob([response.data]);
|
const blob = new Blob([response.data]);
|
||||||
saveAs(blob, `${id}.eml`);
|
saveAs(blob, `${id}.eml`);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user