Files
RustBox/src/routes/mod.rs
T
Mateusz Gruszczyński a643e7e3ab
RustBox CI / rustbox (push) Waiting to run
v0.0.5
2026-10-30 12:06:44 +01:00

490 lines
17 KiB
Rust

pub mod attachments;
pub mod auth;
pub mod calendar;
pub mod contact_groups;
pub mod contacts;
pub mod diagnostics;
pub mod display_preferences;
pub mod drafts;
pub mod folder_mgmt;
pub mod folder_preferences;
pub mod folders;
pub mod health;
pub mod identities;
pub mod mail_preferences;
pub mod messages;
pub mod notification_preferences;
pub mod pgp;
pub mod quota;
pub mod response_templates;
pub mod search;
pub mod send;
pub mod sieve;
pub mod special_folders;
pub mod tags;
pub mod trusted_senders;
pub mod unsubscribe;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use axum::extract::{ConnectInfo, DefaultBodyLimit};
use axum::http::Request;
use axum::routing::{delete, get, patch, post, put};
use axum::{Extension, Router, middleware};
use tower_governor::GovernorError;
use tower_governor::GovernorLayer;
use tower_governor::governor::GovernorConfigBuilder;
use tower_governor::key_extractor::KeyExtractor;
use tower_http::trace::TraceLayer;
use crate::auth::csrf::csrf_protection;
use crate::auth::middleware::auth_guard;
use crate::auth::session::SessionStore;
use crate::config::{AppConfig, TrustedProxy};
use crate::imap::client::ImapClient;
use crate::realtime::events::EventBus;
use crate::realtime::idle::IdleManager;
use crate::smtp::client::SmtpClient;
#[derive(Debug, Clone)]
struct ProxyAwareIpExtractor {
trusted_proxies: Vec<TrustedProxy>,
}
impl ProxyAwareIpExtractor {
fn is_trusted(&self, ip: &IpAddr) -> bool {
self.trusted_proxies
.iter()
.any(|trusted| trusted.contains(ip))
}
fn forwarded_client_ip(
&self,
headers: &axum::http::HeaderMap,
peer_ip: IpAddr,
) -> Option<IpAddr> {
let mut current_hop = peer_ip;
for value in headers.get_all("x-forwarded-for").iter().rev() {
for entry in value.to_str().ok()?.split(',').rev() {
if !self.is_trusted(&current_hop) {
return Some(current_hop);
}
current_hop = entry.trim().parse::<IpAddr>().ok()?;
}
}
Some(current_hop)
}
}
impl KeyExtractor for ProxyAwareIpExtractor {
type Key = IpAddr;
fn extract<T>(&self, req: &Request<T>) -> Result<Self::Key, GovernorError> {
let peer_ip = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ci| ci.0.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
if self.is_trusted(&peer_ip)
&& let Some(client_ip) = self.forwarded_client_ip(req.headers(), peer_ip)
{
return Ok(client_ip);
}
Ok(peer_ip)
}
}
pub fn create_router(
config: Arc<AppConfig>,
store: Arc<SessionStore>,
imap_client: Arc<dyn ImapClient>,
smtp_client: Arc<dyn SmtpClient>,
search_engine: Arc<crate::search::engine::SearchEngine>,
event_bus: Arc<EventBus>,
idle_manager: Arc<IdleManager>,
) -> Router {
let governor_conf = GovernorConfigBuilder::default()
.key_extractor(ProxyAwareIpExtractor {
trusted_proxies: config.parsed_trusted_proxies(),
})
.period(Duration::from_secs(12))
.burst_size(5)
.finish()
.expect("valid governor config");
let public_auth = Router::new()
.route("/login", post(auth::login))
.route("/policy", get(auth::auth_policy))
.layer(middleware::from_fn(csrf_protection))
.layer(GovernorLayer::new(governor_conf));
let browser_routes = Router::new()
.route("/accounts", get(auth::list_accounts))
.route(
"/accounts/{id}",
delete(auth::remove_account).layer(middleware::from_fn(csrf_protection)),
);
let protected_auth = Router::new()
.route("/session", get(auth::get_session))
.route("/sessions", get(auth::list_active_sessions))
.route("/sessions/revoke-others", post(auth::revoke_other_sessions))
.route("/sessions/{id}", delete(auth::revoke_active_session))
.route("/logout", post(auth::logout))
.layer(middleware::from_fn(auth_guard))
.layer(middleware::from_fn(csrf_protection));
let auth_router = Router::new()
.merge(public_auth)
.merge(browser_routes)
.merge(protected_auth);
let attachment_body_limit = config
.max_attachment_size_bytes()
.saturating_add(1024 * 1024);
let protected_data = Router::new()
.route(
"/folders",
get(folders::list_folders).post(folder_mgmt::create_folder),
)
.route(
"/folders/{name}",
patch(folder_mgmt::rename_folder).delete(folder_mgmt::delete_folder),
)
.route(
"/folders/{name}/subscribe",
patch(folder_mgmt::subscribe_folder),
)
.route(
"/folders/{name}/mark-all-read",
post(folder_mgmt::mark_all_read),
)
.route("/folders/{name}/empty", post(folder_mgmt::empty_folder))
.route(
"/folders/{folder}/import-eml",
post(messages::import_eml).layer(DefaultBodyLimit::max(52 * 1024 * 1024)),
)
.route("/folders/{folder}/messages", get(messages::list_messages))
.route(
"/messages/{folder}/{uid}",
get(messages::get_message).delete(messages::delete_message_handler),
)
.route(
"/messages/{folder}/{uid}/flags",
patch(messages::update_flags),
)
.route(
"/messages/{folder}/{uid}/attachments/{attachment_id}",
get(messages::download_attachment),
)
.route(
"/messages/{folder}/{uid}/source",
get(messages::download_eml_source),
)
.route("/messages/move", post(messages::move_message_handler))
.route("/messages/copy", post(messages::copy_message_handler))
.route("/messages/send", post(send::send_message_handler))
.route(
"/messages/{folder}/{uid}/unsubscribe",
post(unsubscribe::unsubscribe_message),
)
.route(
"/drafts",
get(drafts::list_drafts_handler).post(drafts::upsert_draft_handler),
)
.route(
"/drafts/{id}",
get(drafts::get_draft_handler).delete(drafts::delete_draft_handler),
)
.route(
"/drafts/{draft_id}/attachments",
post(attachments::upload_attachment)
.layer(DefaultBodyLimit::max(attachment_body_limit)),
)
.route(
"/drafts/{draft_id}/attachments/{attachment_id}",
delete(attachments::delete_attachment),
)
.route(
"/drafts/{draft_id}/attachments/{attachment_id}/content",
get(attachments::get_attachment_content),
)
.route("/search", get(search::search_messages))
.route(
"/response-templates",
get(response_templates::list).post(response_templates::create),
)
.route(
"/response-templates/{id}",
put(response_templates::update).delete(response_templates::delete),
)
.route(
"/search/saved",
get(search::list_saved_searches).post(search::create_saved_search),
)
.route(
"/search/saved/{id}",
axum::routing::put(search::update_saved_search).delete(search::delete_saved_search),
)
.route(
"/contact-groups",
get(contact_groups::list_groups_handler).post(contact_groups::create_group_handler),
)
.route(
"/contact-groups/{id}",
put(contact_groups::update_group_handler).delete(contact_groups::delete_group_handler),
)
.route(
"/contact-groups/{id}/members",
get(contact_groups::list_members_handler).post(contact_groups::add_member_handler),
)
.route(
"/contact-groups/{id}/members/{contact_id}",
delete(contact_groups::remove_member_handler),
)
.route(
"/tags",
get(tags::list_tags_handler).post(tags::create_tag_handler),
)
.route("/tags/reconcile", post(tags::reconcile_tags_handler))
.route(
"/tags/{id}",
put(tags::update_tag_handler).delete(tags::delete_tag_handler),
)
.route(
"/tags/{id}/messages",
post(tags::tag_message_handler).get(tags::list_tag_messages_handler),
)
.route("/tags/{id}/messages/bulk", post(tags::bulk_tag_handler))
.route(
"/tags/{id}/messages/{folder}/{uid}",
delete(tags::untag_message_handler),
)
.route(
"/messages/{folder}/{uid}/tags",
get(tags::get_message_tags_handler),
)
.route(
"/contacts",
get(contacts::list_contacts_handler).post(contacts::create_contact_handler),
)
.route(
"/contacts/{id}/photo",
get(contacts::get_contact_photo_handler)
.post(contacts::upload_contact_photo_handler)
.delete(contacts::delete_contact_photo_handler)
.layer(DefaultBodyLimit::max(3 * 1024 * 1024)),
)
.route(
"/contacts/duplicates",
get(contacts::duplicate_contacts_handler),
)
.route("/contacts/merge", post(contacts::merge_contacts_handler))
.route("/contacts/export", get(contacts::export_contacts_handler))
.route(
"/contacts/import",
post(contacts::import_contacts_handler).layer(DefaultBodyLimit::max(11 * 1024 * 1024)),
)
.route(
"/contacts/autocomplete/all",
get(contacts::autocomplete_all_handler),
)
.route(
"/contacts/autocomplete",
get(contacts::autocomplete_handler),
)
.route(
"/contacts/{id}",
get(contacts::get_contact_handler).delete(contacts::delete_contact_handler),
)
.route(
"/contacts/{contact_id}/groups",
get(contact_groups::list_contact_groups_handler),
)
.route(
"/contacts/{id}/export",
get(contacts::export_single_contact_handler),
)
.route(
"/identities",
get(identities::list_identities_handler)
.post(identities::create_identity_handler)
.layer(DefaultBodyLimit::max(4 * 1024 * 1024)),
)
.route(
"/identities/{id}",
get(identities::get_identity_handler)
.put(identities::update_identity_handler)
.delete(identities::delete_identity_handler)
.layer(DefaultBodyLimit::max(4 * 1024 * 1024)),
)
.route(
"/pgp/keys",
get(pgp::list_keys)
.post(pgp::import_key)
.layer(DefaultBodyLimit::max(3 * 1024 * 1024)),
)
.route("/pgp/keys/generate", post(pgp::generate_key))
.route("/pgp/keys/{fingerprint}/export", get(pgp::export_key))
.route(
"/pgp/keys/{fingerprint}/test-passphrase",
post(pgp::test_passphrase),
)
.route(
"/pgp/keys/{fingerprint}/change-passphrase",
post(pgp::change_passphrase),
)
.route("/pgp/keys/{fingerprint}/trust", patch(pgp::set_trust))
.route("/pgp/keys/{fingerprint}", delete(pgp::delete_key))
.route(
"/pgp/preferences",
get(pgp::get_preferences).put(pgp::update_preferences),
)
.route("/pgp/discover", post(pgp::discover_key))
.route("/pgp/recipients", post(pgp::recipient_status))
.route("/pgp/verify", post(pgp::verify))
.route(
"/pgp/decrypt",
post(pgp::decrypt).layer(DefaultBodyLimit::max(12 * 1024 * 1024)),
)
.route(
"/pgp/messages/{folder}/{uid}/process",
post(pgp::process_message),
)
.route(
"/settings/display",
get(display_preferences::get_display_preferences)
.put(display_preferences::update_display_preferences),
)
.route(
"/settings/special-folders",
get(special_folders::get_special_folders).put(special_folders::update_special_folders),
)
.route(
"/settings/notifications",
get(notification_preferences::get_notification_preferences)
.put(notification_preferences::update_notification_preferences),
)
.route(
"/settings/mailbox",
get(mail_preferences::get_mail_preferences)
.put(mail_preferences::update_mail_preferences),
)
.route(
"/settings/trusted-senders",
get(trusted_senders::list_trusted_senders).post(trusted_senders::add_trusted_sender),
)
.route(
"/settings/trusted-senders/{email}",
delete(trusted_senders::remove_trusted_sender),
)
.route(
"/folders/{folder}/preferences",
get(folder_preferences::get_folder_preferences)
.put(folder_preferences::update_folder_preferences),
)
.route(
"/calendar/events",
get(calendar::list_events).post(calendar::create_event),
)
.route(
"/calendar/events/import-ics",
post(calendar::import_ics).layer(DefaultBodyLimit::max(11 * 1024 * 1024)),
)
.route("/calendar/events/export-ics", get(calendar::export_ics))
.route(
"/calendar/events/{id}",
get(calendar::get_event)
.put(calendar::update_event)
.delete(calendar::delete_event),
)
.route(
"/calendar/settings",
get(calendar::get_calendar_settings).put(calendar::update_calendar_settings),
)
.route(
"/calendar/meeting-templates",
get(calendar::list_meeting_templates).post(calendar::create_meeting_template),
)
.route(
"/calendar/meeting-templates/{id}",
delete(calendar::delete_meeting_template),
)
.route("/quota", get(quota::get_quota))
.route("/diagnostics/imap", get(diagnostics::imap_diagnostics))
.route("/diagnostics/smtp", get(diagnostics::smtp_diagnostics))
.route("/diagnostics/sync", get(diagnostics::sync_diagnostics))
.route(
"/diagnostics/imap/reconnect",
post(diagnostics::reconnect_imap),
)
.route("/diagnostics/search", get(diagnostics::search_diagnostics))
.route(
"/diagnostics/search/rebuild",
post(diagnostics::rebuild_search_index),
)
.route("/sieve/status", get(sieve::status))
.route("/sieve/scripts", get(sieve::list_scripts))
.route(
"/sieve/scripts/{name}",
get(sieve::get_script)
.put(sieve::put_script)
.delete(sieve::delete_script),
)
.route(
"/sieve/scripts/{name}/activate",
post(sieve::activate_script),
)
.route("/sieve/scripts/{name}/rename", post(sieve::rename_script))
.route("/sieve/deactivate", post(sieve::deactivate_script))
.route("/sieve/check", post(sieve::check_script))
.layer(middleware::from_fn(auth_guard))
.layer(middleware::from_fn(csrf_protection));
let ws_route = Router::new().route("/ws", get(crate::realtime::ws::ws_handler));
let api_router = Router::new()
.route("/health", get(health::health_check))
.route("/ready", get(health::readiness_check))
.nest("/auth", auth_router)
.merge(ws_route)
.merge(protected_data);
let inner = Router::new()
.nest("/api", api_router)
.route("/", get(crate::ui::serve_ui))
.fallback(crate::ui::serve_ui);
let router = match config.normalized_base_path() {
"/" => inner,
base_path => {
let base_path_slash = format!("{}/", base_path.trim_end_matches('/'));
Router::new()
.route(&base_path_slash, get(crate::ui::serve_ui))
.nest(base_path, inner)
}
};
let router = router
.layer(Extension(idle_manager))
.layer(Extension(event_bus))
.layer(Extension(smtp_client))
.layer(Extension(search_engine))
.layer(Extension(imap_client))
.layer(Extension(store))
.layer(Extension(config.clone()))
.layer(TraceLayer::new_for_http());
router
}
#[cfg(test)]
mod tests;