mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// 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::common::auth::ClientContext;
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::modules::token::payload::AccessTokenUpdateRequest;
|
||||
use crate::modules::token::root::set_root_password;
|
||||
use crate::modules::{
|
||||
token::payload::AccessTokenCreateRequest,
|
||||
token::{root::reset_root_token, AccessToken},
|
||||
};
|
||||
use poem_openapi::payload::PlainText;
|
||||
use poem_openapi::{param::Path, payload::Json, OpenApi};
|
||||
|
||||
pub struct AccessTokenApi;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::AccessToken")]
|
||||
impl AccessTokenApi {
|
||||
/// Lists all access tokens in the system.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
#[oai(
|
||||
path = "/access-token-list",
|
||||
method = "get",
|
||||
operation_id = "list_access_tokens"
|
||||
)]
|
||||
async fn list_access_tokens(
|
||||
&self,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<Vec<AccessToken>>> {
|
||||
context.require_root()?;
|
||||
Ok(Json(AccessToken::list_all().await?))
|
||||
}
|
||||
|
||||
/// Lists access tokens for a specific account.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
#[oai(
|
||||
path = "/access-token-list/:account_id",
|
||||
method = "get",
|
||||
operation_id = "list_account_access_tokens"
|
||||
)]
|
||||
async fn list_account_access_tokens(
|
||||
&self,
|
||||
/// The ID of the account whose tokens are to be retrieved.
|
||||
account_id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<Vec<AccessToken>>> {
|
||||
context.require_root()?;
|
||||
Ok(Json(AccessToken::list_account_tokens(account_id.0).await?))
|
||||
}
|
||||
/// Deletes a specific access token.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
#[oai(
|
||||
path = "/access-token/:token",
|
||||
method = "delete",
|
||||
operation_id = "remove_access_token"
|
||||
)]
|
||||
async fn remove_access_token(
|
||||
&self,
|
||||
/// The access token to be deleted
|
||||
token: Path<String>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(AccessToken::delete(token.0.trim()).await?)
|
||||
}
|
||||
|
||||
/// Creates a new access token.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
#[oai(
|
||||
path = "/access-token",
|
||||
method = "post",
|
||||
operation_id = "create_access_token"
|
||||
)]
|
||||
async fn create_access_token(
|
||||
&self,
|
||||
context: ClientContext,
|
||||
/// The request payload
|
||||
payload: Json<AccessTokenCreateRequest>,
|
||||
) -> ApiResult<PlainText<String>> {
|
||||
context.require_root()?;
|
||||
Ok(PlainText(AccessToken::create(payload.0).await?))
|
||||
}
|
||||
|
||||
/// Updates an existing access token.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
#[oai(
|
||||
path = "/access-token/:token",
|
||||
method = "post",
|
||||
operation_id = "update_access_token"
|
||||
)]
|
||||
async fn update_access_token(
|
||||
&self,
|
||||
context: ClientContext,
|
||||
/// The access token to be updated.
|
||||
token: Path<String>,
|
||||
/// The request payload.
|
||||
payload: Json<AccessTokenUpdateRequest>,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(AccessToken::update(token.0.trim(), payload.0).await?)
|
||||
}
|
||||
|
||||
/// Regenerates the root access token.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
#[oai(
|
||||
path = "/reset-root-token",
|
||||
method = "post",
|
||||
operation_id = "regenerate_root_token"
|
||||
)]
|
||||
async fn regenerate_root_token(&self, context: ClientContext) -> ApiResult<PlainText<String>> {
|
||||
context.require_root()?;
|
||||
Ok(PlainText(reset_root_token().await?))
|
||||
}
|
||||
|
||||
/// Reset the Root user's password.
|
||||
///
|
||||
/// Only callable by an already authenticated Root user.
|
||||
/// This endpoint updates the Root password to `password_str`
|
||||
/// and regenerates the `root_token`, invalidating any previous token.
|
||||
#[oai(
|
||||
path = "/reset-root-password",
|
||||
method = "post",
|
||||
operation_id = "reset_root_password"
|
||||
)]
|
||||
async fn reset_root_password(
|
||||
&self,
|
||||
password_str: PlainText<String>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(set_root_password(password_str.0.trim()).await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// 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::BTreeSet;
|
||||
|
||||
use crate::modules::account::payload::{
|
||||
filter_accessible_accounts, AccountCreateRequest, AccountUpdateRequest, MinimalAccount,
|
||||
};
|
||||
use crate::modules::account::state::AccountRunningState;
|
||||
use crate::modules::account::migration::AccountModel;
|
||||
use crate::modules::common::auth::ClientContext;
|
||||
use crate::modules::common::paginated::paginate_vec;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::response::DataPage;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::modules::token::{AccessToken, AccountInfo};
|
||||
use crate::raise_error;
|
||||
use poem::web::Path;
|
||||
use poem_openapi::param::Query;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::OpenApi;
|
||||
|
||||
pub struct AccountApi;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Account")]
|
||||
impl AccountApi {
|
||||
/// Get account details by account ID
|
||||
#[oai(
|
||||
path = "/account/:account_id",
|
||||
method = "get",
|
||||
operation_id = "get_account"
|
||||
)]
|
||||
async fn get_account(
|
||||
&self,
|
||||
/// The account ID to retrieve
|
||||
account_id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<AccountModel>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
Ok(Json(AccountModel::get(account_id).await?))
|
||||
}
|
||||
|
||||
/// Delete an account by ID - WARNING: This permanently removes the account and all associated resources
|
||||
#[oai(
|
||||
path = "/account/:account_id",
|
||||
method = "delete",
|
||||
operation_id = "remove_account"
|
||||
)]
|
||||
async fn remove_account(
|
||||
&self,
|
||||
/// The account ID to delete
|
||||
account_id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
Ok(AccountModel::delete(account_id).await?)
|
||||
}
|
||||
|
||||
/// Create a new account
|
||||
#[oai(path = "/account", method = "post", operation_id = "create_account")]
|
||||
async fn create_account(
|
||||
&self,
|
||||
/// Account creation request payload
|
||||
payload: Json<AccountCreateRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<AccountModel>> {
|
||||
let account = AccountModel::create_account(payload.0).await?;
|
||||
if let Some(access_token) = &context.access_token {
|
||||
let account_info = AccountInfo {
|
||||
id: account.id,
|
||||
email: account.email.clone(),
|
||||
};
|
||||
AccessToken::grant_account_access(&access_token.token, account_info).await?;
|
||||
}
|
||||
Ok(Json(account))
|
||||
}
|
||||
|
||||
/// Update an existing account
|
||||
#[oai(
|
||||
path = "/account/:account_id",
|
||||
method = "post",
|
||||
operation_id = "update_account"
|
||||
)]
|
||||
async fn update_account(
|
||||
&self,
|
||||
/// The account ID to update
|
||||
account_id: Path<u64>,
|
||||
/// Account update request payload
|
||||
payload: Json<AccountUpdateRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
Ok(AccountModel::update(account_id, payload.0, true).await?)
|
||||
}
|
||||
|
||||
/// List accounts with optional pagination parameters
|
||||
#[oai(
|
||||
path = "/accounts",
|
||||
method = "get",
|
||||
operation_id = "list_accounts"
|
||||
)]
|
||||
async fn list_accounts(
|
||||
&self,
|
||||
/// Optional. The page number to retrieve (starting from 1).
|
||||
page: Query<Option<u64>>,
|
||||
/// Optional. The number of items per page.
|
||||
page_size: Query<Option<u64>>,
|
||||
/// Optional. Whether to sort the list in descending order.
|
||||
desc: Query<Option<bool>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<AccountModel>>> {
|
||||
let accessible_accounts = context.accessible_accounts()?;
|
||||
|
||||
if accessible_accounts.is_none() {
|
||||
return Ok(Json(
|
||||
AccountModel::paginate_list(page.0, page_size.0, desc.0).await?,
|
||||
));
|
||||
}
|
||||
|
||||
let all_accounts = AccountModel::list_all().await?;
|
||||
let allowed_ids: BTreeSet<u64> =
|
||||
accessible_accounts.unwrap().iter().map(|a| a.id).collect();
|
||||
|
||||
let mut filtered_accounts: Vec<AccountModel> = all_accounts
|
||||
.into_iter()
|
||||
.filter(|acct| allowed_ids.contains(&acct.id))
|
||||
.collect();
|
||||
|
||||
let sort_desc = desc.0.unwrap_or(true);
|
||||
filtered_accounts.sort_by(|a, b| {
|
||||
if sort_desc {
|
||||
b.created_at.cmp(&a.created_at)
|
||||
} else {
|
||||
a.created_at.cmp(&b.created_at)
|
||||
}
|
||||
});
|
||||
let page_data =
|
||||
paginate_vec(&filtered_accounts, page.0, page_size.0).map(DataPage::from)?;
|
||||
Ok(Json(page_data))
|
||||
}
|
||||
|
||||
/// Get the running state of an account
|
||||
#[oai(
|
||||
path = "/account-state/:account_id",
|
||||
method = "get",
|
||||
operation_id = "account_state"
|
||||
)]
|
||||
async fn account_state(
|
||||
&self,
|
||||
/// The account ID to check state for
|
||||
account_id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<AccountRunningState>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
let state = AccountRunningState::get(account_id).await?.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"account running state is not found".into(),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})?;
|
||||
Ok(Json(state))
|
||||
}
|
||||
|
||||
/// Get a minimal list of active accounts for use in selectors when creating account-related resources
|
||||
///
|
||||
/// This endpoint provides a lightweight list of accounts containing only essential information (id and name).
|
||||
/// It's primarily designed for UI selectors/dropdowns when creating or associating resources with accounts.
|
||||
#[oai(
|
||||
path = "/minimal-account-list",
|
||||
method = "get",
|
||||
operation_id = "minimal_accounts_list"
|
||||
)]
|
||||
async fn minimal_accounts_list(
|
||||
&self,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<Vec<MinimalAccount>>> {
|
||||
let accessible_accounts = context.accessible_accounts()?;
|
||||
|
||||
let minimal_list = AccountModel::minimal_list().await?;
|
||||
let result = match accessible_accounts {
|
||||
Some(set) => filter_accessible_accounts(&minimal_list, set),
|
||||
None => minimal_list,
|
||||
};
|
||||
Ok(Json(result))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// 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::autoconfig::entity::MailServerConfig;
|
||||
use crate::modules::autoconfig::load::resolve_autoconfig;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::raise_error;
|
||||
use poem::web::Path;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::OpenApi;
|
||||
|
||||
pub struct AutoConfigApi;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::AutoConfig")]
|
||||
impl AutoConfigApi {
|
||||
/// Retrieve mail server configuration for a given email address
|
||||
#[oai(
|
||||
path = "/autoconfig/:email_address",
|
||||
method = "get",
|
||||
operation_id = "autoconfig"
|
||||
)]
|
||||
async fn autoconfig(
|
||||
&self,
|
||||
/// The email address to lookup configuration for
|
||||
email_address: Path<String>
|
||||
) -> ApiResult<Json<MailServerConfig>> {
|
||||
let result = resolve_autoconfig(email_address.0.trim())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"Unable to find account configuration information in the backend.".into(),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// 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::cache::imap::mailbox::MailBox;
|
||||
use crate::modules::common::auth::ClientContext;
|
||||
use crate::modules::mailbox::list::get_account_mailboxes;
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use poem::web::Path;
|
||||
use poem_openapi::param::Query;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::OpenApi;
|
||||
|
||||
pub struct MailBoxApi;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Mailbox")]
|
||||
impl MailBoxApi {
|
||||
/// Returns all available mailboxes for the given account.
|
||||
///
|
||||
/// - For IMAP/SMTP accounts, this corresponds to folders/mailboxes.
|
||||
/// - For Gmail API accounts, this corresponds to labels visible via the
|
||||
/// `list messages` API (serving as mailbox equivalents).
|
||||
///
|
||||
/// Both account types support two modes:
|
||||
/// - Using the local cache of mailboxes/labels.
|
||||
/// - Querying the remote service directly for the latest state.
|
||||
#[oai(
|
||||
path = "/list-mailboxes/:account_id",
|
||||
method = "get",
|
||||
operation_id = "list_mailboxes"
|
||||
)]
|
||||
async fn list_mailboxes(
|
||||
&self,
|
||||
/// The unique identifier of the account.
|
||||
account_id: Path<u64>,
|
||||
remote: Query<Option<bool>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<Vec<MailBox>>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
let remote = remote.0.unwrap_or(false);
|
||||
Ok(Json(get_account_mailboxes(account_id, remote).await?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// 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::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::content::{retrieve_email_content, FullMessageContent};
|
||||
use crate::modules::message::delete::delete_messages_impl;
|
||||
use crate::modules::message::list::{get_thread_messages, list_messages_impl};
|
||||
use crate::modules::message::search::{search_messages_impl, SearchRequest};
|
||||
use crate::modules::message::tags::TagCount;
|
||||
use crate::modules::message::tags::UpdateTagsRequest;
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::response::DataPage;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::modules::rest::ErrorCode;
|
||||
use crate::raise_error;
|
||||
use poem::web::Path;
|
||||
use poem::Body;
|
||||
use poem_openapi::param::Query;
|
||||
use poem_openapi::payload::{Attachment, AttachmentType, Json};
|
||||
use poem_openapi::OpenApi;
|
||||
use std::collections::HashMap;
|
||||
use tantivy::schema::Facet;
|
||||
|
||||
pub struct MessageApi;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Message")]
|
||||
impl MessageApi {
|
||||
/// Deletes messages from a mailbox or moves them to the trash for the specified account.
|
||||
#[oai(
|
||||
path = "/delete-messages",
|
||||
method = "post",
|
||||
operation_id = "delete_messages"
|
||||
)]
|
||||
async fn delete_messages(
|
||||
&self,
|
||||
/// specifying the mailbox and messages to delete.
|
||||
payload: Json<HashMap<u64, Vec<u64>>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
let request = payload.0;
|
||||
for account_id in request.keys() {
|
||||
context.require_account_access(*account_id)?;
|
||||
}
|
||||
Ok(delete_messages_impl(request).await?)
|
||||
}
|
||||
|
||||
/// Lists messages in a specified mailbox for the given account.
|
||||
#[oai(
|
||||
path = "/list-messages/:account_id",
|
||||
method = "get",
|
||||
operation_id = "list_messages"
|
||||
)]
|
||||
async fn list_messages(
|
||||
&self,
|
||||
account_id: Path<u64>,
|
||||
mailbox_id: Query<u64>,
|
||||
page: Query<u64>,
|
||||
page_size: Query<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<Envelope>>> {
|
||||
let account_id = account_id.0;
|
||||
let mailbox_id = mailbox_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
Ok(Json(
|
||||
list_messages_impl(account_id, mailbox_id, page.0, page_size.0).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Lists messages in a specified mailbox for the given account.
|
||||
#[oai(
|
||||
path = "/search-messages",
|
||||
method = "post",
|
||||
operation_id = "search_messages"
|
||||
)]
|
||||
async fn search_messages(
|
||||
&self,
|
||||
payload: Json<SearchRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<Envelope>>> {
|
||||
context.require_root()?;
|
||||
Ok(Json(search_messages_impl(payload.0).await?))
|
||||
}
|
||||
|
||||
/// Get thread's envelopes in a specified mailbox for the given account.
|
||||
#[oai(
|
||||
path = "/get-thread-messages/:account_id",
|
||||
method = "get",
|
||||
operation_id = "get_thread_messages"
|
||||
)]
|
||||
async fn get_thread_messages(
|
||||
&self,
|
||||
/// The ID of the account owning the mailbox.
|
||||
account_id: Path<u64>,
|
||||
// Thread ID
|
||||
thread_id: Query<u64>,
|
||||
/// The page number for pagination (1-based).
|
||||
page: Query<u64>,
|
||||
/// The number of messages per page.
|
||||
page_size: Query<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<Envelope>>> {
|
||||
let account_id = account_id.0;
|
||||
let thread_id = thread_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
Ok(Json(
|
||||
get_thread_messages(account_id, thread_id, page.0, page_size.0).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Fetches the content of a specific email for the given account.
|
||||
#[oai(
|
||||
path = "/message-content/:account_id",
|
||||
method = "get",
|
||||
operation_id = "fetch_message_content"
|
||||
)]
|
||||
async fn fetch_message_content(
|
||||
&self,
|
||||
account_id: Path<u64>,
|
||||
id: Query<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<FullMessageContent>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
Ok(Json(retrieve_email_content(account_id, id.0).await?))
|
||||
}
|
||||
|
||||
/// Fetches the full content of a specific email for the given account.
|
||||
#[oai(
|
||||
path = "/download-message/:account_id",
|
||||
method = "get",
|
||||
operation_id = "download_message"
|
||||
)]
|
||||
async fn download_message(
|
||||
&self,
|
||||
account_id: Path<u64>,
|
||||
id: Query<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Attachment<Body>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
let id = id.0;
|
||||
let reader = EML_INDEX_MANAGER.get_reader(account_id, id).await?;
|
||||
let body = Body::from_async_read(reader);
|
||||
let attachment = Attachment::new(body)
|
||||
.attachment_type(AttachmentType::Attachment)
|
||||
.filename(format!("{id}.eml"));
|
||||
Ok(attachment)
|
||||
}
|
||||
|
||||
/// Downloads a specific attachment by filename.
|
||||
#[oai(
|
||||
path = "/download-attachment/:account_id",
|
||||
method = "get",
|
||||
operation_id = "download_attachment"
|
||||
)]
|
||||
async fn download_attachment(
|
||||
&self,
|
||||
account_id: Path<u64>,
|
||||
id: Query<u64>,
|
||||
name: Query<String>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Attachment<Body>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
let email_id = id.0;
|
||||
let name = name.0.trim();
|
||||
let reader = EML_INDEX_MANAGER
|
||||
.get_attachment(account_id, email_id, name)
|
||||
.await?;
|
||||
let body = Body::from_async_read(reader);
|
||||
let attachment = Attachment::new(body)
|
||||
.attachment_type(AttachmentType::Attachment)
|
||||
.filename(name);
|
||||
Ok(attachment)
|
||||
}
|
||||
/// Returns all facets in the index along with their document counts.
|
||||
#[oai(path = "/all-tags", method = "get", operation_id = "get_all_tags")]
|
||||
async fn get_all_tags(&self) -> ApiResult<Json<Vec<TagCount>>> {
|
||||
Ok(Json(ENVELOPE_INDEX_MANAGER.get_all_tags().await?))
|
||||
}
|
||||
|
||||
/// Adds or removes facet tags for multiple emails across accounts.
|
||||
#[oai(
|
||||
path = "/update-tags",
|
||||
method = "post",
|
||||
operation_id = "update_envelope_tags"
|
||||
)]
|
||||
async fn update_envelope_tags(&self, req: Json<UpdateTagsRequest>) -> ApiResult<()> {
|
||||
let req = req.0;
|
||||
for tag in &req.tags {
|
||||
Facet::from_text(tag)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?;
|
||||
}
|
||||
ENVELOPE_INDEX_MANAGER
|
||||
.update_envelope_tags(req.updates, req.tags)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// 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 access_token::AccessTokenApi;
|
||||
use account::AccountApi;
|
||||
use auto_config::AutoConfigApi;
|
||||
use mailbox::MailBoxApi;
|
||||
use message::MessageApi;
|
||||
use oauth2::OAuth2Api;
|
||||
use poem_openapi::{OpenApiService, Tags};
|
||||
use system::SystemApi;
|
||||
|
||||
use crate::bichon_version;
|
||||
|
||||
pub mod access_token;
|
||||
pub mod account;
|
||||
pub mod auto_config;
|
||||
pub mod mailbox;
|
||||
pub mod message;
|
||||
pub mod oauth2;
|
||||
pub mod system;
|
||||
|
||||
#[derive(Tags)]
|
||||
pub enum ApiTags {
|
||||
AccessToken,
|
||||
AutoConfig,
|
||||
Account,
|
||||
Mailbox,
|
||||
OAuth2,
|
||||
Message,
|
||||
System,
|
||||
}
|
||||
|
||||
type RustMailOpenApi = (
|
||||
AccessTokenApi,
|
||||
AutoConfigApi,
|
||||
AccountApi,
|
||||
SystemApi,
|
||||
MailBoxApi,
|
||||
OAuth2Api,
|
||||
MessageApi,
|
||||
);
|
||||
|
||||
pub fn create_openapi_service() -> OpenApiService<RustMailOpenApi, ()> {
|
||||
OpenApiService::new(
|
||||
(
|
||||
AccessTokenApi,
|
||||
AutoConfigApi,
|
||||
AccountApi,
|
||||
SystemApi,
|
||||
MailBoxApi,
|
||||
OAuth2Api,
|
||||
MessageApi,
|
||||
),
|
||||
"BichonApi",
|
||||
bichon_version!(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// 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::common::auth::ClientContext;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::oauth2::entity::{OAuth2, OAuth2CreateRequest, OAuth2UpdateRequest};
|
||||
use crate::modules::oauth2::flow::{AuthorizeUrlRequest, OAuth2Flow};
|
||||
use crate::modules::oauth2::token::{ExternalOAuth2Request, OAuth2AccessToken};
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::response::DataPage;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::raise_error;
|
||||
use poem::web::Path;
|
||||
use poem_openapi::param::Query;
|
||||
use poem_openapi::payload::{Json, PlainText};
|
||||
use poem_openapi::OpenApi;
|
||||
|
||||
pub struct OAuth2Api;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::OAuth2")]
|
||||
impl OAuth2Api {
|
||||
/// Retrieves the OAuth2 configuration for a specified name.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
/// This endpoint fetches the OAuth2 configuration identified by the given name.
|
||||
#[oai(
|
||||
path = "/oauth2/:id",
|
||||
method = "get",
|
||||
operation_id = "get_oauth2_config"
|
||||
)]
|
||||
async fn get_oauth2_config(
|
||||
&self,
|
||||
/// The name of the OAuth2 configuration to retrieve
|
||||
id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<OAuth2>> {
|
||||
context.require_root()?;
|
||||
let id = id.0;
|
||||
Ok(Json(OAuth2::get(id).await?.ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!("OAuth2 configuration id='{id}' not found"),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})?))
|
||||
}
|
||||
|
||||
/// Deletes an OAuth2 configuration by name.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
/// This endpoint removes the OAuth2 configuration identified by the specified name.
|
||||
#[oai(
|
||||
path = "/oauth2/:id",
|
||||
method = "delete",
|
||||
operation_id = "remove_oauth2_config"
|
||||
)]
|
||||
async fn remove_oauth2_config(
|
||||
&self,
|
||||
/// The name of the OAuth2 configuration to retrieve
|
||||
id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(OAuth2::delete(id.0).await?)
|
||||
}
|
||||
|
||||
/// Creates a new OAuth2 configuration.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
/// This endpoint creates a new OAuth2 configuration based on the provided request data.
|
||||
#[oai(
|
||||
path = "/oauth2",
|
||||
method = "post",
|
||||
operation_id = "create_oauth2_config"
|
||||
)]
|
||||
async fn create_oauth2_config(
|
||||
&self,
|
||||
/// A JSON payload containing the details for the new OAuth2 configuration
|
||||
request: Json<OAuth2CreateRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
let entity = OAuth2::new(request.0)?;
|
||||
Ok(entity.save().await?)
|
||||
}
|
||||
|
||||
/// Updates an existing OAuth2 configuration.
|
||||
///
|
||||
/// Requires root privileges.
|
||||
/// This endpoint updates the OAuth2 configuration identified by the specified name
|
||||
#[oai(
|
||||
path = "/oauth2/:id",
|
||||
method = "post",
|
||||
operation_id = "update_oauth2_config"
|
||||
)]
|
||||
async fn update_oauth2_config(
|
||||
&self,
|
||||
/// The name of the OAuth2 configuration to update
|
||||
id: Path<u64>,
|
||||
/// A JSON payload containing the updated configuration details
|
||||
payload: Json<OAuth2UpdateRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(OAuth2::update(id.0, payload.0).await?)
|
||||
}
|
||||
|
||||
/// Lists OAuth2 configurations with pagination and sorting options.
|
||||
///
|
||||
/// This endpoint retrieves a paginated list of OAuth2 configurations, allowing for
|
||||
/// optional pagination and sorting parameters. It requires root access.
|
||||
#[oai(
|
||||
path = "/oauth2-list",
|
||||
method = "get",
|
||||
operation_id = "list_oauth2_config"
|
||||
)]
|
||||
async fn list_oauth2_config(
|
||||
&self,
|
||||
/// Optional. The page number to retrieve (starting from 1).
|
||||
page: Query<Option<u64>>,
|
||||
/// Optional. The number of items per page.
|
||||
page_size: Query<Option<u64>>,
|
||||
/// Optional. Whether to sort the list in descending order.
|
||||
desc: Query<Option<bool>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<OAuth2>>> {
|
||||
context.require_root()?;
|
||||
Ok(Json(
|
||||
OAuth2::paginate_list(page.0, page_size.0, desc.0).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Generates an OAuth2 authorization URL for a specific account.
|
||||
///
|
||||
/// This endpoint creates an authorization URL for the specified OAuth2 configuration
|
||||
/// and account ID. It requires root access and returns the URL as plain text.
|
||||
#[oai(
|
||||
path = "/oauth2-authorize-url",
|
||||
method = "post",
|
||||
operation_id = "create_oauth2_authorize_url"
|
||||
)]
|
||||
async fn create_oauth2_authorize_url(
|
||||
&self,
|
||||
/// A JSON payload containing the OAuth2 configuration name and account ID.
|
||||
request: Json<AuthorizeUrlRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<PlainText<String>> {
|
||||
context.require_root()?;
|
||||
let request = request.0;
|
||||
let flow = OAuth2Flow::new(request.oauth2_id);
|
||||
Ok(PlainText(flow.authorize_url(request.account_id).await?))
|
||||
}
|
||||
|
||||
/// Retrieves OAuth2 access tokens for a specified account.
|
||||
///
|
||||
/// This endpoint fetches the OAuth2 access tokens associated with the given account ID.
|
||||
#[oai(
|
||||
path = "/oauth2-tokens/:account_id",
|
||||
method = "get",
|
||||
operation_id = "get_oauth2_tokens"
|
||||
)]
|
||||
async fn get_oauth2_tokens(
|
||||
&self,
|
||||
/// The ID of the account to retrieve access tokens for
|
||||
account_id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<OAuth2AccessToken>> {
|
||||
let account = account_id.0;
|
||||
context.require_account_access(account)?;
|
||||
Ok(Json(OAuth2AccessToken::get(account).await?.ok_or_else(
|
||||
|| {
|
||||
raise_error!(
|
||||
"OAuth2 access tokens not found".into(),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
},
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Configures an external OAuth2 token for a specified account.
|
||||
///
|
||||
/// This endpoint allows two usage modes:
|
||||
/// 1. If only an `access_token` is provided, RustMailer will store it directly.
|
||||
/// - In this mode, RustMailer **cannot refresh** the token, since it has no
|
||||
/// associated OAuth2 configuration or refresh token.
|
||||
/// - The caller is responsible for periodically updating the access token
|
||||
/// by calling this endpoint again.
|
||||
/// 2. If both `oauth2_id` and `refresh_token` are provided, it means the external
|
||||
/// OAuth2 authorization flow has been completed outside RustMailer.
|
||||
/// - Since the OAuth2 configuration (including client_id and client_secret)
|
||||
/// is already stored in RustMailer, the service can use the refresh token
|
||||
/// to obtain new access tokens automatically.
|
||||
///
|
||||
/// Note: The `oauth2_id` must reference a valid OAuth2 configuration
|
||||
/// already created in RustMailer.
|
||||
#[oai(
|
||||
path = "/store-external-oauth2-token/:account_id",
|
||||
method = "post",
|
||||
operation_id = "store_external_oauth2_token"
|
||||
)]
|
||||
async fn store_external_oauth2_token(
|
||||
&self,
|
||||
account_id: Path<u64>,
|
||||
request: Json<ExternalOAuth2Request>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
let account = account_id.0;
|
||||
// Check account access permissions
|
||||
context.require_account_access(account)?;
|
||||
OAuth2AccessToken::upsert_external_oauth_token(account, request.0).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// 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::common::auth::ClientContext;
|
||||
use crate::modules::dashboard::DashboardStats;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::rest::api::ApiTags;
|
||||
use crate::modules::rest::ApiResult;
|
||||
use crate::modules::settings::proxy::Proxy;
|
||||
use crate::modules::version::{fetch_notifications, Notifications};
|
||||
use crate::raise_error;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::payload::{Json, PlainText};
|
||||
use poem_openapi::OpenApi;
|
||||
|
||||
pub struct SystemApi;
|
||||
|
||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::System")]
|
||||
impl SystemApi {
|
||||
/// Retrieves important system notifications for the RustMail service.
|
||||
///
|
||||
/// This endpoint returns a consolidated view of all critical system notifications including:
|
||||
/// - Available version updates
|
||||
/// - License expiration warnings
|
||||
#[oai(
|
||||
method = "get",
|
||||
path = "/notifications",
|
||||
operation_id = "get_notifications"
|
||||
)]
|
||||
async fn get_notifications(&self) -> ApiResult<Json<Notifications>> {
|
||||
let notification = fetch_notifications()
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(Json(notification))
|
||||
}
|
||||
|
||||
/// Get overall dashboard statistics.
|
||||
///
|
||||
/// Returns various aggregated metrics about the mail system, such as
|
||||
/// total email count, total storage size, index usage, top senders,
|
||||
/// recent activity histogram, and more.
|
||||
#[oai(
|
||||
method = "get",
|
||||
path = "/dashboard-stats",
|
||||
operation_id = "get_dashboard_stats"
|
||||
)]
|
||||
async fn get_dashboard_stats(&self) -> ApiResult<Json<DashboardStats>> {
|
||||
let stats = DashboardStats::get().await?;
|
||||
Ok(Json(stats))
|
||||
}
|
||||
|
||||
/// Get the full list of SOCKS5 proxy configurations.
|
||||
#[oai(method = "get", path = "/list-proxy", operation_id = "list_proxy")]
|
||||
async fn list_proxy(&self) -> ApiResult<Json<Vec<Proxy>>> {
|
||||
let proxies = Proxy::list_all()
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(Json(proxies))
|
||||
}
|
||||
|
||||
/// Delete a specific proxy configuration by ID. Requires root permission.
|
||||
#[oai(path = "/proxy/:id", method = "delete", operation_id = "remove_proxy")]
|
||||
async fn remove_proxy(
|
||||
&self,
|
||||
/// The name of the OAuth2 configuration to retrieve
|
||||
id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(Proxy::delete(id.0).await?)
|
||||
}
|
||||
|
||||
/// Retrieve a specific proxy configuration by ID
|
||||
#[oai(path = "/proxy/:id", method = "get", operation_id = "get_proxy")]
|
||||
async fn get_proxy(
|
||||
&self,
|
||||
/// The name of the OAuth2 configuration to retrieve
|
||||
id: Path<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<Proxy>> {
|
||||
context.require_root()?;
|
||||
Ok(Json(Proxy::get(id.0).await?))
|
||||
}
|
||||
|
||||
/// Create a new proxy configuration. Requires root permission.
|
||||
#[oai(path = "/proxy", method = "post", operation_id = "create_proxy")]
|
||||
async fn create_proxy(&self, url: PlainText<String>, context: ClientContext) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
let entity = Proxy::new(url.0);
|
||||
Ok(entity.save().await?)
|
||||
}
|
||||
|
||||
/// Update the URL of a specific proxy by ID. Requires root permission.
|
||||
#[oai(path = "/proxy/:id", method = "post", operation_id = "update_proxy")]
|
||||
async fn update_proxy(
|
||||
&self,
|
||||
id: Path<u64>,
|
||||
url: PlainText<String>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
context.require_root()?;
|
||||
Ok(Proxy::update(id.0, url.0).await?)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user