// // 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 . use std::collections::HashSet; use crate::modules::account::migration::AccountModel; use crate::modules::cache::imap::mailbox::MailBox; use crate::modules::error::code::ErrorCode; use crate::modules::utils::create_hash; use crate::modules::{error::BichonResult, indexer::schema::SchemaTools}; use crate::raise_error; use poem_openapi::Object; use serde::{Deserialize, Serialize}; use tantivy::schema::Facet; use tantivy::{doc, schema::Value, TantivyDocument}; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] pub struct Envelope { pub id: u64, pub message_id: String, pub account_id: u64, pub account_email: Option, pub mailbox_id: u64, pub mailbox_name: Option, pub uid: u32, pub subject: String, pub text: String, pub from: String, pub to: Vec, pub cc: Vec, pub bcc: Vec, pub date: i64, pub internal_date: i64, pub size: u32, pub thread_id: u64, pub attachments: Vec, pub tags: Option>, } fn extract_u64_field( document: &TantivyDocument, field: tantivy::schema::Field, ) -> BichonResult { let value = document.get_first(field).ok_or_else(|| { raise_error!( format!("miss '{}' field in tantivy document", stringify!(field)), ErrorCode::InternalError ) })?; value.as_u64().ok_or_else(|| { raise_error!( format!("'{}' field is not a u64", stringify!(field)), ErrorCode::InternalError ) }) } fn extract_i64_field( document: &TantivyDocument, field: tantivy::schema::Field, ) -> BichonResult { let value = document.get_first(field).ok_or_else(|| { raise_error!( format!("miss '{}' field in tantivy document", stringify!(field)), ErrorCode::InternalError ) })?; value.as_i64().ok_or_else(|| { raise_error!( format!("'{}' field is not a i64", stringify!(field)), ErrorCode::InternalError ) }) } fn extract_string_field( document: &TantivyDocument, field: tantivy::schema::Field, ) -> BichonResult { let value = document.get_first(field).ok_or_else(|| { raise_error!( format!("'{}' field not found", stringify!(field)), ErrorCode::InternalError ) })?; value.as_str().map(|s| s.to_string()).ok_or_else(|| { raise_error!( format!("'{}' field is not a string", stringify!(field)), ErrorCode::InternalError ) }) } fn extract_vec_string_field( document: &TantivyDocument, field: tantivy::schema::Field, ) -> BichonResult> { let value = document .get_all(field) .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); Ok(value) } impl Envelope { pub fn to_document(&self, mailbox_id: u64) -> BichonResult { let fields = SchemaTools::envelope_fields(); let mut doc = doc!(); doc.add_u64(fields.f_id, self.id); doc.add_text(fields.f_message_id, &self.message_id); doc.add_u64(fields.f_account_id, self.account_id); doc.add_u64(fields.f_mailbox_id, mailbox_id); doc.add_u64(fields.f_uid, self.uid as u64); doc.add_text(fields.f_subject, &self.subject); doc.add_text(fields.f_text, &self.text); doc.add_text(fields.f_from, &self.from); for to in &self.to { doc.add_text(fields.f_to, to); } for cc in &self.cc { doc.add_text(fields.f_cc, cc); } for bcc in &self.bcc { doc.add_text(fields.f_bcc, bcc); } doc.add_i64(fields.f_date, self.date); doc.add_i64(fields.f_internal_date, self.internal_date); doc.add_u64(fields.f_size, self.size as u64); doc.add_u64(fields.f_thread_id, self.thread_id); for att in &self.attachments { doc.add_text(fields.f_attachments, att); } doc.add_bool(fields.f_has_attachment, self.attachments.len() > 0); Ok(doc) } pub async fn from_tantivy_doc(doc: &TantivyDocument) -> BichonResult { let fields = SchemaTools::envelope_fields(); let account_id = extract_u64_field(doc, fields.f_account_id)?; let message_id = extract_string_field(doc, fields.f_message_id)?; let mailbox_id = extract_u64_field(doc, fields.f_mailbox_id)?; let id = create_hash(account_id, &message_id); let full_text = extract_string_field(doc, fields.f_text)?; // Take up to the first 500 characters as a preview; let preview = if full_text.chars().count() > 500 { full_text.chars().take(500).collect::() + "..." } else { full_text }; let tags: Vec = doc .get_all(fields.f_tags) .filter_map(|value| value.as_facet()) .map(|facet_encoded_str| { Facet::from_encoded(facet_encoded_str.as_bytes().to_vec()) .ok() .map(|facet| facet.to_string()) }) .flatten() .collect(); let account_email = AccountModel::find(account_id).await?.map(|a| a.email); let mailboxes = MailBox::list_all(account_id).await?; let mailbox_name = mailboxes .iter() .find(|m| m.id == mailbox_id) .map(|m| m.name.clone()); let envelope = Envelope { id, account_id, account_email, mailbox_id, mailbox_name, message_id: extract_string_field(doc, fields.f_message_id)?, uid: extract_u64_field(doc, fields.f_uid)? as u32, subject: extract_string_field(doc, fields.f_subject)?, text: preview, from: extract_string_field(doc, fields.f_from)?, to: extract_vec_string_field(doc, fields.f_to)?, cc: extract_vec_string_field(doc, fields.f_cc)?, bcc: extract_vec_string_field(doc, fields.f_bcc)?, date: extract_i64_field(doc, fields.f_date)?, internal_date: extract_i64_field(doc, fields.f_internal_date)?, size: extract_u64_field(doc, fields.f_size)? as u32, thread_id: extract_u64_field(doc, fields.f_thread_id)?, attachments: extract_vec_string_field(doc, fields.f_attachments)?, tags: Some(tags), }; Ok(envelope) } } pub async fn extract_contacts(doc: &TantivyDocument) -> BichonResult> { let fields = SchemaTools::envelope_fields(); let mut all_contacts = HashSet::new(); if let Ok(from_val) = extract_string_field(doc, fields.f_from) { if !from_val.is_empty() { all_contacts.insert(from_val); } } let multi_fields = [fields.f_to, fields.f_cc, fields.f_bcc]; for field in multi_fields { if let Ok(vals) = extract_vec_string_field(doc, field) { for v in vals { if !v.is_empty() { all_contacts.insert(v); } } } } Ok(all_contacts) }