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,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)
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user