use mailrs_maildir::Flag;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Mailbox {
pub id: i64,
pub user: String,
pub name: String,
pub uidvalidity: u32,
pub uidnext: u32,
pub highest_modseq: u64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MailboxStatus {
pub total: u32,
pub unread: u32,
pub recent: u32,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Message {
pub id: i64,
pub mailbox_id: i64,
pub uid: u32,
pub blob_ref: String,
pub sender: String,
pub recipients: String,
pub subject: String,
pub date: i64,
pub internal_date: i64,
pub size: u32,
pub flags: u32,
pub message_id: String,
pub in_reply_to: String,
pub thread_id: String,
pub modseq: u64,
pub user_address: String,
}
#[derive(Debug, Clone)]
pub struct InsertMessage<'a> {
pub user: &'a str,
pub mailbox_name: &'a str,
pub blob_ref: &'a str,
pub sender: &'a str,
pub recipients: &'a str,
pub subject: &'a str,
pub size: u32,
pub date: i64,
pub internal_date: i64,
pub message_id: &'a str,
pub in_reply_to: &'a str,
pub thread_id: &'a str,
pub flags: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct Inserted {
pub id: i64,
pub uid: u32,
pub modseq: u64,
}
#[derive(Debug, Clone, Default)]
pub struct QueryFilter<'a> {
pub mailbox_id: Option<i64>,
pub user: Option<&'a str>,
pub text: Option<&'a str>,
pub has_keyword: Option<u32>,
pub not_keyword: Option<u32>,
pub position: u32,
pub limit: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagOp {
Set,
Add,
Remove,
}
pub type FlagAction = FlagOp;
#[derive(Debug, Clone)]
pub struct MessageMeta {
pub id: i64,
pub mailbox_id: i64,
pub uid: u32,
pub maildir_id: String,
pub sender: String,
pub recipients: String,
pub subject: String,
pub date: i64,
pub size: u32,
pub flags: u32,
pub internal_date: i64,
pub message_id: String,
pub in_reply_to: String,
pub thread_id: String,
pub modseq: u64,
pub user_address: String,
pub importance_level: String,
pub importance_score: f32,
pub is_bulk_sender: bool,
pub has_tracking_pixel: bool,
pub new_content: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ConversationSummary {
pub thread_id: String,
pub subject: String,
pub participants: String,
pub message_count: u32,
pub unread_count: u32,
pub last_date: i64,
pub category: String,
pub flagged: bool,
pub snippet: String,
pub pinned: bool,
pub archived: bool,
pub importance_level: String,
pub importance_score: f32,
pub requires_action: bool,
pub last_sender: String,
pub sent_count: u32,
}
#[derive(Debug, Clone)]
pub struct EmailAnalysisRow {
pub message_id: i64,
pub category: String,
pub risk_score: i16,
pub risk_reason: String,
pub summary: String,
pub people: serde_json::Value,
pub dates: serde_json::Value,
pub amounts: serde_json::Value,
pub action_items: serde_json::Value,
pub model_version: String,
pub clean_text: String,
pub requires_action: bool,
pub sender_intent: String,
pub action_deadline: Option<String>,
}
pub const FLAG_SEEN: u32 = 0b0000_0001;
pub const FLAG_ANSWERED: u32 = 0b0000_0010;
pub const FLAG_FLAGGED: u32 = 0b0000_0100;
pub const FLAG_DELETED: u32 = 0b0000_1000;
pub const FLAG_DRAFT: u32 = 0b0001_0000;
pub const FLAG_RECENT: u32 = 0b0010_0000;
pub fn maildir_flags_to_bitmask(flags: &[Flag]) -> u32 {
let mut bits = 0u32;
for flag in flags {
bits |= match flag {
Flag::Seen => FLAG_SEEN,
Flag::Replied => FLAG_ANSWERED,
Flag::Flagged => FLAG_FLAGGED,
Flag::Trashed => FLAG_DELETED,
Flag::Draft => FLAG_DRAFT,
Flag::Passed => 0,
};
}
bits
}
pub fn bitmask_to_maildir_flags(bits: u32) -> Vec<Flag> {
let mut flags = Vec::new();
if bits & FLAG_SEEN != 0 {
flags.push(Flag::Seen);
}
if bits & FLAG_ANSWERED != 0 {
flags.push(Flag::Replied);
}
if bits & FLAG_FLAGGED != 0 {
flags.push(Flag::Flagged);
}
if bits & FLAG_DELETED != 0 {
flags.push(Flag::Trashed);
}
if bits & FLAG_DRAFT != 0 {
flags.push(Flag::Draft);
}
flags
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_conversion_roundtrip() {
let flags = vec![Flag::Seen, Flag::Replied, Flag::Flagged];
let bits = maildir_flags_to_bitmask(&flags);
assert_eq!(bits, FLAG_SEEN | FLAG_ANSWERED | FLAG_FLAGGED);
let back = bitmask_to_maildir_flags(bits);
assert!(back.contains(&Flag::Seen));
assert!(back.contains(&Flag::Replied));
assert!(back.contains(&Flag::Flagged));
assert_eq!(back.len(), 3);
}
#[test]
fn empty_flags() {
assert_eq!(maildir_flags_to_bitmask(&[]), 0);
assert!(bitmask_to_maildir_flags(0).is_empty());
}
#[test]
fn single_flag_roundtrip() {
for (flag, expected_bit) in [
(Flag::Seen, FLAG_SEEN),
(Flag::Replied, FLAG_ANSWERED),
(Flag::Flagged, FLAG_FLAGGED),
(Flag::Trashed, FLAG_DELETED),
(Flag::Draft, FLAG_DRAFT),
] {
let bits = maildir_flags_to_bitmask(&[flag]);
assert_eq!(bits, expected_bit);
let back = bitmask_to_maildir_flags(bits);
assert_eq!(back.len(), 1);
assert_eq!(back[0], flag);
}
}
#[test]
fn passed_flag_maps_to_zero() {
assert_eq!(maildir_flags_to_bitmask(&[Flag::Passed]), 0);
}
#[test]
fn all_flags_combined() {
let all = vec![
Flag::Seen,
Flag::Replied,
Flag::Flagged,
Flag::Trashed,
Flag::Draft,
Flag::Passed,
];
let bits = maildir_flags_to_bitmask(&all);
assert_eq!(
bits,
FLAG_SEEN | FLAG_ANSWERED | FLAG_FLAGGED | FLAG_DELETED | FLAG_DRAFT
);
let back = bitmask_to_maildir_flags(bits);
assert_eq!(back.len(), 5); }
#[test]
fn duplicate_flags_idempotent() {
let flags = vec![Flag::Seen, Flag::Seen, Flag::Seen];
let bits = maildir_flags_to_bitmask(&flags);
assert_eq!(bits, FLAG_SEEN);
}
#[test]
fn bitmask_ignores_unknown_bits() {
let bits = 0b1111_1111;
let flags = bitmask_to_maildir_flags(bits);
assert_eq!(flags.len(), 5); }
#[test]
fn flag_action_variants() {
assert_ne!(FlagAction::Set, FlagAction::Add);
assert_ne!(FlagAction::Add, FlagAction::Remove);
assert_ne!(FlagAction::Set, FlagAction::Remove);
}
#[test]
fn flag_constants_are_powers_of_two() {
assert_eq!(FLAG_SEEN.count_ones(), 1);
assert_eq!(FLAG_ANSWERED.count_ones(), 1);
assert_eq!(FLAG_FLAGGED.count_ones(), 1);
assert_eq!(FLAG_DELETED.count_ones(), 1);
assert_eq!(FLAG_DRAFT.count_ones(), 1);
assert_eq!(FLAG_RECENT.count_ones(), 1);
}
#[test]
fn flag_constants_no_overlap() {
let all = FLAG_SEEN | FLAG_ANSWERED | FLAG_FLAGGED | FLAG_DELETED | FLAG_DRAFT | FLAG_RECENT;
assert_eq!(all.count_ones(), 6);
}
#[test]
fn bitmask_to_flags_recent_not_included() {
let flags = bitmask_to_maildir_flags(FLAG_RECENT);
assert!(flags.is_empty());
}
}