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
+232
View File
@@ -0,0 +1,232 @@
//
// 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::{code::ErrorCode, BichonResult},
settings::{cli::SETTINGS, system::SystemSetting},
token::{root::ROOT_TOKEN, AccessToken, AccountInfo},
utils::rate_limit::RATE_LIMITER_MANAGER,
},
raise_error,
};
use governor::clock::{Clock, QuantaClock};
use poem::{
web::{
headers::{authorization::Bearer, Authorization, HeaderMapExt},
RealIp,
},
Endpoint, FromRequest, Middleware, Request, RequestBody, Result,
};
use serde::Deserialize;
use std::{collections::BTreeSet, net::IpAddr, sync::Arc};
use super::create_api_error_response;
pub struct ApiGuard;
pub struct ApiGuardEndpoint<E> {
ep: E,
}
impl<E: Endpoint> Middleware<E> for ApiGuard {
type Output = ApiGuardEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
ApiGuardEndpoint { ep }
}
}
#[derive(Deserialize)]
struct Param {
access_token: String,
}
impl<E: Endpoint> Endpoint for ApiGuardEndpoint<E> {
type Output = E::Output;
async fn call(&self, mut req: Request) -> Result<Self::Output> {
let context = authorize_access(&req).await?;
req.set_data(Arc::new(context));
self.ep.call(req).await
}
}
#[derive(Clone, Debug, Default)]
pub struct ClientContext {
pub ip_addr: Option<IpAddr>,
pub access_token: Option<AccessToken>,
pub is_root: bool,
}
impl ClientContext {
pub fn require_root(&self) -> BichonResult<()> {
if !SETTINGS.bichon_enable_access_token || self.is_root {
Ok(())
} else {
Err(raise_error!(
"Root access required".into(),
ErrorCode::PermissionDenied
))
}
}
pub fn require_authorized(&self) -> BichonResult<()> {
if !SETTINGS.bichon_enable_access_token || self.is_root || self.access_token.is_some() {
Ok(())
} else {
Err(raise_error!(
"Authorization required".into(),
ErrorCode::PermissionDenied
))
}
}
pub fn require_account_access(&self, account_id: u64) -> BichonResult<()> {
if !SETTINGS.bichon_enable_access_token || self.is_root {
return Ok(());
}
match &self.access_token {
Some(token) if token.can_access_account(account_id) => Ok(()),
_ => Err(raise_error!(format!(
"You do not have permission to access the requested email account (ID: {}). Please check your access rights or contact the administrator.",
account_id
), ErrorCode::PermissionDenied)),
}
}
pub fn accessible_accounts(&self) -> BichonResult<Option<&BTreeSet<AccountInfo>>> {
if !SETTINGS.bichon_enable_access_token || self.is_root {
Ok(None) // All accounts are accessible
} else {
match &self.access_token {
Some(token) => Ok(Some(&token.accounts)),
None => Err(raise_error!(
"Missing access token".into(),
ErrorCode::PermissionDenied
)),
}
}
}
}
impl<'a> FromRequest<'a> for ClientContext {
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
extract_client_context(req).await
}
}
pub async fn extract_client_context(req: &Request) -> Result<ClientContext> {
if SETTINGS.bichon_enable_access_token {
let ip_addr = RealIp::from_request_without_body(req)
.await
.map_err(|_| {
create_api_error_response(
"Failed to parse client IP address",
ErrorCode::InvalidParameter,
)
})?
.0
.ok_or_else(|| {
create_api_error_response(
"Failed to parse client IP address",
ErrorCode::InvalidParameter,
)
})?;
// Extract access token from Bearer header or query params
let bearer = req
.headers()
.typed_get::<Authorization<Bearer>>()
.map(|auth| auth.0.token().to_string())
.or_else(|| req.params::<Param>().ok().map(|param| param.access_token));
let token = bearer.ok_or_else(|| {
create_api_error_response("Valid access token not found", ErrorCode::PermissionDenied)
})?;
// Check for root token
if let Ok(Some(root)) = SystemSetting::get(ROOT_TOKEN) {
if root.value == token {
return Ok(ClientContext {
ip_addr: Some(ip_addr),
access_token: None,
is_root: true,
});
}
}
// Validate and update access token
let validated_token = AccessToken::try_update_access_timestamp(&token)
.await
.map_err(|_| {
create_api_error_response("Invalid access token", ErrorCode::PermissionDenied)
})?;
return Ok(ClientContext {
ip_addr: Some(ip_addr),
access_token: Some(validated_token),
is_root: false,
});
}
Ok(Default::default())
}
pub async fn authorize_access(req: &Request) -> Result<ClientContext, poem::Error> {
let context = extract_client_context(&req).await?;
context.require_authorized().map_err(|error| {
create_api_error_response(&error.to_string(), ErrorCode::PermissionDenied)
})?;
if let Some(access_token) = &context.access_token {
if let Some(access_control) = &access_token.acl {
if let Some(ip_addr) = context.ip_addr {
if let Some(whitelist) = &access_control.ip_whitelist {
if !whitelist.contains(&ip_addr.to_string()) {
return Err(create_api_error_response(
&format!("IP {} not in whitelist", ip_addr),
ErrorCode::PermissionDenied,
));
}
}
}
if let Some(rate_limit) = &access_control.rate_limit {
if let Err(not_until) = RATE_LIMITER_MANAGER
.check(&access_token.token, rate_limit.clone())
.await
{
let wait_duration = not_until.wait_time_from(QuantaClock::default().now());
return Err(create_api_error_response(
&format!(
"Rate limit: {}/{}s. Retry after {}s",
rate_limit.quota,
rate_limit.interval,
wait_duration.as_secs()
),
ErrorCode::TooManyRequest,
));
}
}
}
}
Ok(context)
}
+47
View File
@@ -0,0 +1,47 @@
//
// 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::{Endpoint, IntoResponse, Middleware, Request, Response, Result};
use crate::modules::error::handler::error_handler;
pub struct ErrorCapture;
pub struct ErrorCaptureEndpoint<E> {
ep: E,
}
impl<E: Endpoint> Middleware<E> for ErrorCapture {
type Output = ErrorCaptureEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
ErrorCaptureEndpoint { ep }
}
}
impl<E: Endpoint> Endpoint for ErrorCaptureEndpoint<E> {
type Output = Response;
async fn call(&self, req: Request) -> Result<Self::Output> {
match self.ep.call(req).await {
Ok(response) => Ok(response.into_response()),
Err(error) => Ok(error_handler(error).await.into_response()),
}
}
}
+172
View File
@@ -0,0 +1,172 @@
//
// 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::{
num::NonZeroU32,
sync::{Arc, LazyLock},
time::Instant,
};
use governor::{
clock::{QuantaClock, QuantaInstant},
middleware::NoOpMiddleware,
state::InMemoryState,
Quota, RateLimiter,
};
use poem::{
http::header, web::RealIp, Endpoint, FromRequest, IntoResponse, Middleware, Request, Response,
Result,
};
use tracing::{error, info, warn, Instrument};
pub type GovRateLimiter = RateLimiter<
governor::state::NotKeyed,
InMemoryState,
QuantaClock,
NoOpMiddleware<QuantaInstant>,
>;
static RATE_LIMITER: LazyLock<LogRateLimiter> = LazyLock::new(LogRateLimiter::new);
pub struct LogRateLimiter {
limiter: Arc<GovRateLimiter>,
}
impl LogRateLimiter {
pub fn new() -> Self {
let quota = Quota::per_second(NonZeroU32::new(10).unwrap());
let limiter = RateLimiter::direct(quota);
Self {
limiter: Arc::new(limiter),
}
}
pub async fn should_log(&self, status: u16) -> bool {
let cost = match status {
500_u16.. => NonZeroU32::new(1).unwrap(), // ERROR
400_u16..=499_u16 => NonZeroU32::new(3).unwrap(), // WARN
_ => NonZeroU32::new(5).unwrap(), // INFO
};
self.limiter.check_n(cost).is_ok()
}
}
#[derive(Default)]
pub struct Tracing;
impl<E: Endpoint> Middleware<E> for Tracing {
type Output = TracingEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
TracingEndpoint { inner: ep }
}
}
/// Endpoint for the `Tracing` middleware.
pub struct TracingEndpoint<E> {
inner: E,
}
impl<E: Endpoint> Endpoint for TracingEndpoint<E> {
type Output = Response;
async fn call(&self, req: Request) -> Result<Self::Output> {
let remote_addr = RealIp::from_request_without_body(&req)
.await
.ok()
.and_then(|real_ip| real_ip.0)
.map(|addr| addr.to_string())
.unwrap_or_else(|| req.remote_addr().to_string());
let method = req.method().clone();
let path = req.uri().path().to_string();
let query = req.uri().query().map(|q| q.to_string());
let referer = req
.headers()
.get(header::REFERER)
.and_then(|v| v.to_str().ok().map(|v| v.to_string()));
let content_length = req
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok().map(|v| v.to_string()));
let span = tracing::info_span!(
"request",
remote_addr = %remote_addr,
method = %method,
path = %path,
query = ?query,
referer = ?referer,
//user_agent = ?user_agent,
// forwarded = ?forwarded,
content_length = ?content_length,
);
async move {
let now = Instant::now();
let res = self.inner.call(req).await;
let duration = now.elapsed();
match res {
Ok(resp) => {
let resp = resp.into_response();
let status = resp.status().as_u16();
log_response(status, duration).await;
Ok(resp)
}
Err(err) => {
let status = err.status().as_u16();
log_response(status, duration).await;
Err(err)
}
}
}
.instrument(span)
.await
}
}
#[inline]
async fn log_response(status: u16, duration: std::time::Duration) {
if RATE_LIMITER.should_log(status).await {
match status {
500.. => {
error!(
status = %status,
duration = ?duration,
"request completed with server error"
);
}
400..=499 => {
warn!(
status = %status,
duration = ?duration,
"request completed with client error"
);
}
_ => {
info!(
status = %status,
duration = ?duration,
"request completed successfully"
);
}
}
}
}
+233
View File
@@ -0,0 +1,233 @@
//
// 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 super::error::code::ErrorCode;
use super::error::BichonError;
use mail_parser::{Addr as ImapAddr, Address as ImapAddress};
use mail_send::mail_builder::headers::address::Address as SmtpAddress;
use mail_send::mail_builder::headers::address::EmailAddress as SmtpEmailAddress;
use poem::error::ResponseError;
use poem::Body;
use poem::{http::StatusCode, Error, Response};
use poem_openapi::Object;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::ops::Deref;
use tracing::error;
pub mod auth;
pub mod error;
pub mod log;
pub mod paginated;
pub mod periodic;
pub mod rustls;
pub mod signal;
pub mod timeout;
pub mod tls;
pub mod validator;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Object)]
pub struct Addr {
/// The optional display name associated with the email address (e.g., "John Doe").
/// If `None`, no display name is specified.
pub name: Option<String>,
/// The optional email address (e.g., "john.doe@example.com").
/// If `None`, the address is unavailable, though typically at least one of `name` or `address` is provided.
pub address: Option<String>,
}
impl Addr {
pub fn parse(s: &str) -> Self {
let re = Regex::new(r#"(?:(?P<name>.*)\s*)?<(?P<email>[^<>]+)>"#).unwrap();
if let Some(caps) = re.captures(s) {
let name: Option<String> = caps.name("name").map(|m| m.as_str().trim().into());
let email: Option<String> = caps.name("email").map(|m| m.as_str().trim().into());
Addr {
name: if let Some(n) = name {
if n.is_empty() {
None
} else {
Some(n)
}
} else {
None
},
address: email,
}
} else {
let s_trimmed = s.trim();
Addr {
name: None,
address: if s_trimmed.is_empty() {
None
} else {
Some(s_trimmed.into())
},
}
}
}
}
impl std::fmt::Display for Addr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.name, &self.address) {
(Some(name), Some(address)) => write!(f, "{} <{}>", name, address),
(None, Some(address)) => write!(f, "<{}>", address),
(Some(name), None) => write!(f, "{}", name),
(None, None) => write!(f, ""),
}
}
}
impl<'x> From<&ImapAddr<'x>> for Addr {
fn from(original: &ImapAddr<'x>) -> Self {
Addr {
name: original.name.as_ref().map(|s| s.to_string()),
address: original.address.as_ref().map(|s| s.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AddrVec(pub Vec<Addr>);
impl Deref for AddrVec {
type Target = Vec<Addr>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'x> From<&ImapAddress<'x>> for AddrVec {
fn from(original: &ImapAddress<'x>) -> Self {
let vec = match original {
ImapAddress::List(addrs) => addrs.iter().map(Addr::from).collect(),
ImapAddress::Group(groups) => groups
.iter()
.flat_map(|group| group.addresses.iter().map(Addr::from))
.collect(),
};
AddrVec(vec)
}
}
impl<'x> From<SmtpEmailAddress<'x>> for Addr {
fn from(email: SmtpEmailAddress<'x>) -> Self {
Addr {
name: email.name.map(|n| n.into_owned()),
address: Some(email.email.into_owned()),
}
}
}
impl<'x> From<&SmtpAddress<'x>> for AddrVec {
fn from(address: &SmtpAddress<'x>) -> Self {
fn collect_addresses<'x>(address: &SmtpAddress<'x>, result: &mut Vec<Addr>) {
match address {
SmtpAddress::Address(email) => {
let addr = Addr::from(email.clone());
result.push(addr);
}
SmtpAddress::Group(group) => {
for addr in &group.addresses {
collect_addresses(addr, result);
}
}
SmtpAddress::List(list) => {
for addr in list {
collect_addresses(addr, result);
}
}
}
}
let mut addresses = Vec::new();
collect_addresses(address, &mut addresses);
AddrVec(addresses)
}
}
impl<'x> From<Addr> for SmtpAddress<'x> {
fn from(addr: Addr) -> Self {
SmtpAddress::Address(SmtpEmailAddress {
name: addr.name.map(Cow::Owned),
email: Cow::Owned(addr.address.unwrap_or_default()),
})
}
}
// #[derive(Serialize)]
// pub struct ErrorResponse {
// pub message: String,
// }
#[inline]
fn create_rust_mailer_error(message: &str, code: ErrorCode) -> BichonError {
BichonError::Generic {
message: message.into(),
location: snafu::Location::default(),
code,
}
}
#[inline]
pub fn create_api_error_response(message: &str, code: ErrorCode) -> Error {
let rust_mailer_error = create_rust_mailer_error(message, code);
rust_mailer_error.into()
}
impl ResponseError for BichonError {
fn status(&self) -> StatusCode {
match self {
BichonError::Generic {
message: _,
location: _,
code,
} => code.status(),
}
}
fn as_response(&self) -> Response
where
Self: std::error::Error + Send + Sync + 'static,
{
match self {
BichonError::Generic {
message,
location,
code,
} => {
error!(
error_code = *code as u32,
error_message = %message,
error_location = ?location
);
let body = Body::from_json(serde_json::json!({
"code": *code as u32,
"message": message.to_string(),
}))
.unwrap();
Response::builder().status(self.status()).body(body)
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
//
// 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::cmp::min;
use crate::{
modules::{
database::Paginated,
error::{code::ErrorCode, BichonResult},
},
raise_error,
};
pub fn paginate_vec<T: Clone>(
items: &Vec<T>,
page: Option<u64>,
page_size: Option<u64>,
) -> BichonResult<Paginated<T>> {
let total_items = items.len() as u64;
let (offset, total_pages) = match (page, page_size) {
(Some(p), Some(s)) if p > 0 && s > 0 => {
let offset = (p - 1) * s;
let total_pages = if total_items > 0 {
(total_items + s - 1) / s
} else {
0
};
(Some(offset), Some(total_pages))
}
(Some(0), _) | (_, Some(0)) => {
return Err(raise_error!(
"'page' and 'page_size' must be greater than 0.".into(),
ErrorCode::InvalidParameter
));
}
_ => (None, None),
};
let data = match offset {
Some(offset) if offset >= total_items => vec![],
Some(offset) => {
let end = min(offset + page_size.unwrap_or(total_items), total_items) as usize;
items[offset as usize..end].to_vec()
}
None => items.clone(),
};
Ok(Paginated::new(
page,
page_size,
total_items,
total_pages,
data,
))
}
+120
View File
@@ -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::signal::SIGNAL_MANAGER, error::BichonResult};
use std::{future::Future, time::Duration};
use tokio::{sync::oneshot, time::MissedTickBehavior};
use tracing::{info, warn};
pub struct PeriodicTask {
name: String,
}
pub struct TaskHandle {
cancel_sender: Option<oneshot::Sender<()>>,
join_handle: tokio::task::JoinHandle<()>,
}
impl TaskHandle {
pub async fn cancel(self) {
if let Some(sender) = self.cancel_sender {
let _ = sender.send(());
}
let _ = self.join_handle.await;
}
}
impl PeriodicTask {
pub fn new(name: &str) -> Self {
Self {
name: name.to_owned(),
}
}
/// If `enable_cancel` is true, allows cancellation through TaskHandle::cancel
pub fn start<F, T>(
self,
task: T,
param: Option<u64>,
interval: Duration,
enable_cancel: bool,
run_immediately: bool,
) -> TaskHandle
where
T: Fn(Option<u64>) -> F + Send + Sync + 'static,
F: Future<Output = BichonResult<()>> + Send + 'static,
{
info!("Task '{}' started", &self.name);
let (cancel_sender_opt, cancel_receiver_opt) = if enable_cancel {
let (tx, rx) = oneshot::channel::<()>();
(Some(tx), Some(rx))
} else {
(None, None)
};
let name_clone = self.name.clone();
let join_handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(interval);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut shutdown = SIGNAL_MANAGER.subscribe();
if !run_immediately {
interval.tick().await; // discard first immediate tick
}
let mut cancel_receiver = cancel_receiver_opt;
loop {
tokio::select! {
_ = interval.tick() => {
match task(param).await {
Ok(()) => {},
Err(e) => {
warn!("Task '{}' failed: {:?}", name_clone, e);
},
}
}
// only enabled if cancel_receiver is Some
_ = async {
if let Some(ref mut rx) = cancel_receiver {
rx.await.ok()
} else {
futures::future::pending().await
}
} => {
info!("Task '{}' received cancellation signal", name_clone);
break;
}
_ = shutdown.recv() => {
info!("Task '{}' shutting down due to shutdown signal", name_clone);
break;
}
}
}
info!("Task '{}' stopped", name_clone);
});
TaskHandle {
cancel_sender: cancel_sender_opt,
join_handle,
}
}
}
+41
View File
@@ -0,0 +1,41 @@
//
// 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::{
context::Initialize,
error::{code::ErrorCode, BichonResult},
},
raise_error,
};
pub struct RustMailerTls;
impl Initialize for RustMailerTls {
async fn initialize() -> BichonResult<()> {
rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider())
.map_err(|_| {
raise_error!(
"failed to set crypto provider".into(),
ErrorCode::InternalError
)
})
}
}
+55
View File
@@ -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 std::sync::LazyLock;
use crate::modules::{
context::Initialize, error::BichonResult, utils::shutdown::shutdown_signal,
};
use tokio::sync::broadcast;
pub static SIGNAL_MANAGER: LazyLock<SignalManager> = LazyLock::new(SignalManager::new);
pub struct SignalManager {
sender: broadcast::Sender<()>,
}
impl SignalManager {
pub fn new() -> Self {
let (sender, _) = broadcast::channel(1);
SignalManager { sender }
}
pub fn subscribe(&self) -> broadcast::Receiver<()> {
self.sender.subscribe()
}
}
impl Initialize for SignalManager {
async fn initialize() -> BichonResult<()> {
tokio::spawn({
async move {
shutdown_signal().await;
println!("\nSending shutdown signal...");
let _ = SIGNAL_MANAGER.sender.send(());
}
});
Ok(())
}
}
+74
View File
@@ -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 poem::{Endpoint, Middleware, Request, Result};
use std::time::Duration;
use tracing::error;
use crate::modules::error::code::ErrorCode;
use super::create_api_error_response;
pub const TIMEOUT_HEADER: &str = "X-RustMailer-Timeout-Seconds";
pub struct Timeout;
impl<E: Endpoint> Middleware<E> for Timeout {
type Output = TimeoutEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
TimeoutEndpoint { ep }
}
}
pub struct TimeoutEndpoint<E> {
ep: E,
}
#[inline]
fn extract_timeout(req: &Request) -> Option<u64> {
if let Some(v) = req.header(TIMEOUT_HEADER) {
v.parse::<u64>().ok()
} else {
None
}
}
impl<E: Endpoint> Endpoint for TimeoutEndpoint<E> {
type Output = E::Output;
async fn call(&self, req: Request) -> Result<Self::Output> {
let timeout = extract_timeout(&req);
let seconds = timeout.unwrap_or(30).min(600);
match tokio::time::timeout(Duration::from_secs(seconds), self.ep.call(req)).await {
Ok(Ok(response)) => Ok(response), // If the request completes successfully
Ok(Err(e)) => Err(e), // If the request returns an error
Err(_) => {
error!("Request timed out after {} seconds", seconds);
Err(create_api_error_response(
&format!(
"Request timed out after {} seconds (timeout set via X-RustMailer-Timeout-Seconds header, max allowed: 600 seconds)",
seconds
),
ErrorCode::RequestTimeout,
))
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
//
// 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::listener::{RustlsCertificate, RustlsConfig};
use crate::{
modules::{
error::{code::ErrorCode, BichonResult},
settings::dir::DATA_DIR_MANAGER,
},
raise_error,
};
pub fn rustls_config() -> BichonResult<RustlsConfig> {
let cert = std::fs::read_to_string(&DATA_DIR_MANAGER.tls_cert).map_err(|e| {
raise_error!(
format!(
"Failed to read TLS certificate: '{}' (error: {})",
DATA_DIR_MANAGER.tls_cert.display(),
e
),
ErrorCode::InternalError
)
})?;
let key = std::fs::read_to_string(&DATA_DIR_MANAGER.tls_key).map_err(|e| {
raise_error!(
format!(
"Failed to read TLS private key: '{}' (error: {})",
DATA_DIR_MANAGER.tls_key.display(),
e
),
ErrorCode::InternalError
)
})?;
let rustls_certificate = RustlsCertificate::new().cert(cert).key(key);
Ok(RustlsConfig::new().fallback(rustls_certificate))
}
+43
View File
@@ -0,0 +1,43 @@
//
// 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::{
fmt::{self, Display, Formatter},
str::FromStr,
};
use email_address::EmailAddress;
use poem_openapi::Validator;
pub struct EmailValidator;
impl Display for EmailValidator {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("Not a valid email address")
}
}
impl Validator<String> for EmailValidator {
fn check(&self, value: &String) -> bool {
match EmailAddress::from_str(value) {
Ok(e) => &e.email() == value,
Err(_) => false,
}
}
}