diff --git a/crates/core/src/ext/event_bus.rs b/crates/core/src/ext/event_bus.rs new file mode 100644 index 0000000..637d499 --- /dev/null +++ b/crates/core/src/ext/event_bus.rs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025-2026 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 . + +// Event bus extension point. +// +// Community edition: NoopEventBus — all events are discarded. +// Pro edition: AuditEventBus — events are persisted to audit database. +// Enterprise edition: adds SIEM webhook to the same trait impl. +// +// The open-source server emits events at key points (login, view, delete, search). +// It never reads from the event bus — events are fire-and-forget. + +use std::net::IpAddr; +use std::sync::{LazyLock, RwLock}; + +#[derive(Debug, Clone)] +pub enum Event { + EmailViewed { + email_id: String, + user: String, + ip: IpAddr, + }, + EmailDeleted { + email_id: String, + user: String, + }, + UserLoggedIn { + user: String, + ip: IpAddr, + }, + UserCreated { + created_by: String, + new_user: String, + }, + SearchPerformed { + query: String, + user: String, + }, + SettingsChanged { + key: String, + user: String, + }, + AttachmentDownloaded { + email_id: String, + content_hash: String, + user: String, + }, +} + +pub trait EventBus: Send + Sync { + fn emit(&self, event: Event); +} + +/// Default — all events are discarded. +struct NoopEventBus; +impl EventBus for NoopEventBus { + fn emit(&self, _event: Event) {} +} + +static EVENT_BUS: LazyLock>> = + LazyLock::new(|| RwLock::new(Box::new(NoopEventBus))); + +/// Called by Pro/Enterprise at startup to replace the noop default. +pub fn set_event_bus(bus: Box) { + *EVENT_BUS.write().unwrap() = bus; +} + +/// Fire-and-forget. Called by the server at key points. +pub fn emit(event: Event) { + EVENT_BUS.read().unwrap().emit(event); +} diff --git a/crates/core/src/ext/mod.rs b/crates/core/src/ext/mod.rs new file mode 100644 index 0000000..7f95e4b --- /dev/null +++ b/crates/core/src/ext/mod.rs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2025-2026 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 . + +// Event bus extension point. +// +// Community edition: NoopEventBus — all events are discarded. +// Pro edition: AuditEventBus — events are persisted to audit database. +// Enterprise edition: adds SIEM webhook to the same trait impl. +// +// The open-source server emits events at key points (login, view, delete, search). +// It never reads from the event bus — events are fire-and-forget. + +pub mod event_bus; +pub mod text_extractor; diff --git a/crates/core/src/ext/text_extractor.rs b/crates/core/src/ext/text_extractor.rs new file mode 100644 index 0000000..7ea817f --- /dev/null +++ b/crates/core/src/ext/text_extractor.rs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2025-2026 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 . + +// Attachment text extraction extension point. +// +// Community edition: NoopExtractor — no attachments are text-indexed. +// Pro edition: PdfExtractor — extracts text from PDF, Word, etc. +// +// Used in: crates/core/src/envelope/extractor.rs + +use std::sync::{LazyLock, RwLock}; + +pub struct ExtractedText { + pub text: String, + pub page_count: Option, + pub is_ocr: bool, +} + +pub trait AttachmentTextExtractor: Send + Sync { + /// Returns None if this extractor doesn't handle the file type. + /// Returns Some(ExtractedText) if text was successfully extracted. + fn extract(&self, content_type: &str, ext: &str, bytes: &[u8]) -> Option; +} + +/// Default — all attachments are skipped. +struct NoopExtractor; +impl AttachmentTextExtractor for NoopExtractor { + fn extract(&self, _ct: &str, _ext: &str, _bytes: &[u8]) -> Option { + None + } +} + +static EXTRACTOR: LazyLock>> = + LazyLock::new(|| RwLock::new(Box::new(NoopExtractor))); + +/// Called by Pro/Enterprise at startup to replace the noop default. +pub fn set_extractor(extractor: Box) { + *EXTRACTOR.write().unwrap() = extractor; +} + +/// Called by the attachment pipeline during IMAP sync. +/// The caller should wrap this in spawn_blocking for CPU-bound extraction. +pub fn extract_text(content_type: &str, ext: &str, bytes: &[u8]) -> Option { + EXTRACTOR.read().unwrap().extract(content_type, ext, bytes) +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index cdadcd6..2440585 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,4 +1,5 @@ pub mod account; +pub mod ext; pub mod admin; pub mod autoconfig; pub mod cache;