Skip to main content

mxr_protocol/
types.rs

1use mxr_core::id::*;
2use mxr_core::types::*;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct IpcMessage {
7    pub id: u64,
8    pub payload: IpcPayload,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type")]
13#[allow(clippy::large_enum_variant)]
14pub enum IpcPayload {
15    Request(Request),
16    Response(Response),
17    Event(DaemonEvent),
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "cmd")]
22pub enum Request {
23    ListEnvelopes {
24        label_id: Option<LabelId>,
25        account_id: Option<AccountId>,
26        limit: u32,
27        offset: u32,
28    },
29    ListEnvelopesByIds {
30        message_ids: Vec<MessageId>,
31    },
32    GetEnvelope {
33        message_id: MessageId,
34    },
35    GetBody {
36        message_id: MessageId,
37    },
38    DownloadAttachment {
39        message_id: MessageId,
40        attachment_id: AttachmentId,
41    },
42    OpenAttachment {
43        message_id: MessageId,
44        attachment_id: AttachmentId,
45    },
46    ListBodies {
47        message_ids: Vec<MessageId>,
48    },
49    GetThread {
50        thread_id: ThreadId,
51    },
52    ListLabels {
53        account_id: Option<AccountId>,
54    },
55    CreateLabel {
56        name: String,
57        color: Option<String>,
58        account_id: Option<AccountId>,
59    },
60    DeleteLabel {
61        name: String,
62        account_id: Option<AccountId>,
63    },
64    RenameLabel {
65        old: String,
66        new: String,
67        account_id: Option<AccountId>,
68    },
69    ListRules,
70    ListAccounts,
71    ListAccountsConfig,
72    AuthorizeAccountConfig {
73        account: AccountConfigData,
74        reauthorize: bool,
75    },
76    UpsertAccountConfig {
77        account: AccountConfigData,
78    },
79    SetDefaultAccount {
80        key: String,
81    },
82    TestAccountConfig {
83        account: AccountConfigData,
84    },
85    GetRule {
86        rule: String,
87    },
88    GetRuleForm {
89        rule: String,
90    },
91    UpsertRule {
92        rule: serde_json::Value,
93    },
94    UpsertRuleForm {
95        existing_rule: Option<String>,
96        name: String,
97        condition: String,
98        action: String,
99        priority: i32,
100        enabled: bool,
101    },
102    DeleteRule {
103        rule: String,
104    },
105    DryRunRules {
106        rule: Option<String>,
107        all: bool,
108        after: Option<String>,
109    },
110    ListEvents {
111        limit: u32,
112        level: Option<String>,
113        category: Option<String>,
114    },
115    GetLogs {
116        limit: u32,
117        level: Option<String>,
118    },
119    GetDoctorReport,
120    GenerateBugReport {
121        verbose: bool,
122        full_logs: bool,
123        since: Option<String>,
124    },
125    ListRuleHistory {
126        rule: Option<String>,
127        limit: u32,
128    },
129    Search {
130        query: String,
131        limit: u32,
132        #[serde(default)]
133        offset: u32,
134        mode: Option<SearchMode>,
135        #[serde(default)]
136        sort: Option<SortOrder>,
137        explain: bool,
138    },
139    SyncNow {
140        account_id: Option<AccountId>,
141    },
142    GetSyncStatus {
143        account_id: AccountId,
144    },
145    SetFlags {
146        message_id: MessageId,
147        flags: MessageFlags,
148    },
149    Count {
150        query: String,
151        mode: Option<SearchMode>,
152    },
153    GetHeaders {
154        message_id: MessageId,
155    },
156    ListSavedSearches,
157    ListSubscriptions {
158        limit: u32,
159    },
160    GetSemanticStatus,
161    EnableSemantic {
162        enabled: bool,
163    },
164    InstallSemanticProfile {
165        profile: SemanticProfile,
166    },
167    UseSemanticProfile {
168        profile: SemanticProfile,
169    },
170    ReindexSemantic,
171    CreateSavedSearch {
172        name: String,
173        query: String,
174        search_mode: SearchMode,
175    },
176    DeleteSavedSearch {
177        name: String,
178    },
179    RunSavedSearch {
180        name: String,
181        limit: u32,
182    },
183    // Mutations (Phase 2)
184    Mutation(MutationCommand),
185    Unsubscribe {
186        message_id: MessageId,
187    },
188    Snooze {
189        message_id: MessageId,
190        wake_at: chrono::DateTime<chrono::Utc>,
191    },
192    Unsnooze {
193        message_id: MessageId,
194    },
195    ListSnoozed,
196    // Compose (Phase 2)
197    PrepareReply {
198        message_id: MessageId,
199        reply_all: bool,
200    },
201    PrepareForward {
202        message_id: MessageId,
203    },
204    SendDraft {
205        draft: Draft,
206    },
207    /// Save draft to the mail server (e.g. Gmail Drafts folder).
208    SaveDraftToServer {
209        draft: Draft,
210    },
211    ListDrafts,
212
213    // Export (Phase 3)
214    ExportThread {
215        thread_id: ThreadId,
216        format: ExportFormat,
217    },
218    ExportSearch {
219        query: String,
220        format: ExportFormat,
221    },
222
223    GetStatus,
224    Ping,
225    Shutdown,
226}
227
228/// Mutation commands for modifying messages.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(tag = "mutation")]
231pub enum MutationCommand {
232    Archive {
233        message_ids: Vec<MessageId>,
234    },
235    ReadAndArchive {
236        message_ids: Vec<MessageId>,
237    },
238    Trash {
239        message_ids: Vec<MessageId>,
240    },
241    Spam {
242        message_ids: Vec<MessageId>,
243    },
244    Star {
245        message_ids: Vec<MessageId>,
246        starred: bool,
247    },
248    SetRead {
249        message_ids: Vec<MessageId>,
250        read: bool,
251    },
252    ModifyLabels {
253        message_ids: Vec<MessageId>,
254        add: Vec<String>,
255        remove: Vec<String>,
256    },
257    Move {
258        message_ids: Vec<MessageId>,
259        target_label: String,
260    },
261}
262
263/// Reply context returned by PrepareReply.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ReplyContext {
266    pub in_reply_to: String,
267    pub references: Vec<String>,
268    pub reply_to: String,
269    pub cc: String,
270    pub subject: String,
271    pub from: String,
272    pub thread_context: String,
273}
274
275/// Forward context returned by PrepareForward.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct ForwardContext {
278    pub subject: String,
279    pub from: String,
280    pub forwarded_content: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(tag = "status")]
285#[allow(clippy::large_enum_variant)]
286pub enum Response {
287    Ok { data: ResponseData },
288    Error { message: String },
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(tag = "kind")]
293#[allow(clippy::large_enum_variant)]
294pub enum ResponseData {
295    Envelopes {
296        envelopes: Vec<Envelope>,
297    },
298    Envelope {
299        envelope: Envelope,
300    },
301    Body {
302        body: MessageBody,
303    },
304    AttachmentFile {
305        file: AttachmentFile,
306    },
307    Bodies {
308        bodies: Vec<MessageBody>,
309    },
310    Thread {
311        thread: Thread,
312        messages: Vec<Envelope>,
313    },
314    Labels {
315        labels: Vec<Label>,
316    },
317    Label {
318        label: Label,
319    },
320    Rules {
321        rules: Vec<serde_json::Value>,
322    },
323    RuleData {
324        rule: serde_json::Value,
325    },
326    Accounts {
327        accounts: Vec<AccountSummaryData>,
328    },
329    AccountsConfig {
330        accounts: Vec<AccountConfigData>,
331    },
332    AccountOperation {
333        result: AccountOperationResult,
334    },
335    RuleFormData {
336        form: RuleFormData,
337    },
338    RuleDryRun {
339        results: Vec<serde_json::Value>,
340    },
341    EventLogEntries {
342        entries: Vec<EventLogEntry>,
343    },
344    LogLines {
345        lines: Vec<String>,
346    },
347    DoctorReport {
348        report: DoctorReport,
349    },
350    BugReport {
351        content: String,
352    },
353    RuleHistory {
354        entries: Vec<serde_json::Value>,
355    },
356    SearchResults {
357        results: Vec<SearchResultItem>,
358        #[serde(default)]
359        has_more: bool,
360        explain: Option<SearchExplain>,
361    },
362    SyncStatus {
363        sync: AccountSyncStatus,
364    },
365    Count {
366        count: u32,
367    },
368    Headers {
369        headers: Vec<(String, String)>,
370    },
371    SavedSearches {
372        searches: Vec<mxr_core::types::SavedSearch>,
373    },
374    Subscriptions {
375        subscriptions: Vec<mxr_core::types::SubscriptionSummary>,
376    },
377    SemanticStatus {
378        snapshot: SemanticStatusSnapshot,
379    },
380    SavedSearchData {
381        search: mxr_core::types::SavedSearch,
382    },
383    Status {
384        uptime_secs: u64,
385        accounts: Vec<String>,
386        total_messages: u32,
387        #[serde(default)]
388        daemon_pid: Option<u32>,
389        #[serde(default)]
390        sync_statuses: Vec<AccountSyncStatus>,
391        #[serde(default)]
392        protocol_version: u32,
393        #[serde(default)]
394        daemon_version: Option<String>,
395        #[serde(default)]
396        daemon_build_id: Option<String>,
397        #[serde(default)]
398        repair_required: bool,
399    },
400    ReplyContext {
401        context: ReplyContext,
402    },
403    ForwardContext {
404        context: ForwardContext,
405    },
406    Drafts {
407        drafts: Vec<Draft>,
408    },
409    SnoozedMessages {
410        snoozed: Vec<Snoozed>,
411    },
412    ExportResult {
413        content: String,
414    },
415    Pong,
416    Ack,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct SearchResultItem {
421    pub message_id: MessageId,
422    pub account_id: AccountId,
423    pub thread_id: ThreadId,
424    pub score: f32,
425    pub mode: SearchMode,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct SearchExplain {
430    pub requested_mode: SearchMode,
431    pub executed_mode: SearchMode,
432    pub semantic_query: Option<String>,
433    pub lexical_window: u32,
434    pub dense_window: Option<u32>,
435    pub lexical_candidates: u32,
436    pub dense_candidates: u32,
437    pub final_results: u32,
438    pub rrf_k: Option<u32>,
439    pub notes: Vec<String>,
440    pub results: Vec<SearchExplainResult>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct SearchExplainResult {
445    pub rank: u32,
446    pub message_id: MessageId,
447    pub final_score: f32,
448    pub lexical_rank: Option<u32>,
449    pub lexical_score: Option<f32>,
450    pub dense_rank: Option<u32>,
451    pub dense_score: Option<f32>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct AttachmentFile {
456    pub attachment_id: AttachmentId,
457    pub filename: String,
458    pub path: String,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct EventLogEntry {
463    pub timestamp: i64,
464    pub level: String,
465    pub category: String,
466    pub account_id: Option<AccountId>,
467    pub message_id: Option<String>,
468    pub rule_id: Option<String>,
469    pub summary: String,
470    pub details: Option<String>,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
474#[serde(rename_all = "snake_case")]
475pub enum DaemonHealthClass {
476    #[default]
477    Healthy,
478    Degraded,
479    RestartRequired,
480    RepairRequired,
481}
482
483impl DaemonHealthClass {
484    pub fn as_str(&self) -> &'static str {
485        match self {
486            Self::Healthy => "healthy",
487            Self::Degraded => "degraded",
488            Self::RestartRequired => "restart_required",
489            Self::RepairRequired => "repair_required",
490        }
491    }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
495#[serde(rename_all = "snake_case")]
496pub enum IndexFreshness {
497    #[default]
498    Unknown,
499    Current,
500    Stale,
501    Disabled,
502    Indexing,
503    Error,
504    RepairRequired,
505}
506
507impl IndexFreshness {
508    pub fn as_str(&self) -> &'static str {
509        match self {
510            Self::Unknown => "unknown",
511            Self::Current => "current",
512            Self::Stale => "stale",
513            Self::Disabled => "disabled",
514            Self::Indexing => "indexing",
515            Self::Error => "error",
516            Self::RepairRequired => "repair_required",
517        }
518    }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
522pub struct AccountSyncStatus {
523    pub account_id: AccountId,
524    pub account_name: String,
525    pub last_attempt_at: Option<String>,
526    pub last_success_at: Option<String>,
527    pub last_error: Option<String>,
528    pub failure_class: Option<String>,
529    pub consecutive_failures: u32,
530    pub backoff_until: Option<String>,
531    pub sync_in_progress: bool,
532    pub current_cursor_summary: Option<String>,
533    pub last_synced_count: u32,
534    pub healthy: bool,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct DoctorReport {
539    pub healthy: bool,
540    #[serde(default)]
541    pub health_class: DaemonHealthClass,
542    #[serde(default)]
543    pub lexical_index_freshness: IndexFreshness,
544    #[serde(default)]
545    pub last_successful_sync_at: Option<String>,
546    #[serde(default)]
547    pub lexical_last_rebuilt_at: Option<String>,
548    #[serde(default)]
549    pub semantic_enabled: bool,
550    #[serde(default)]
551    pub semantic_active_profile: Option<String>,
552    #[serde(default)]
553    pub semantic_index_freshness: IndexFreshness,
554    #[serde(default)]
555    pub semantic_last_indexed_at: Option<String>,
556    #[serde(default)]
557    pub data_stats: DoctorDataStats,
558    pub data_dir_exists: bool,
559    pub database_exists: bool,
560    pub index_exists: bool,
561    pub socket_exists: bool,
562    pub socket_reachable: bool,
563    pub stale_socket: bool,
564    pub daemon_running: bool,
565    pub daemon_pid: Option<u32>,
566    #[serde(default)]
567    pub daemon_protocol_version: u32,
568    #[serde(default)]
569    pub daemon_version: Option<String>,
570    #[serde(default)]
571    pub daemon_build_id: Option<String>,
572    pub index_lock_held: bool,
573    pub index_lock_error: Option<String>,
574    #[serde(default)]
575    pub restart_required: bool,
576    #[serde(default)]
577    pub repair_required: bool,
578    pub database_path: String,
579    pub database_size_bytes: u64,
580    pub index_path: String,
581    pub index_size_bytes: u64,
582    pub log_path: String,
583    pub log_size_bytes: u64,
584    pub sync_statuses: Vec<AccountSyncStatus>,
585    pub recent_sync_events: Vec<EventLogEntry>,
586    pub recent_error_logs: Vec<String>,
587    pub recommended_next_steps: Vec<String>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize, Default)]
591pub struct DoctorDataStats {
592    pub accounts: u32,
593    pub labels: u32,
594    pub messages: u32,
595    pub unread_messages: u32,
596    pub starred_messages: u32,
597    pub messages_with_attachments: u32,
598    pub message_labels: u32,
599    pub bodies: u32,
600    pub attachments: u32,
601    pub drafts: u32,
602    pub snoozed: u32,
603    pub saved_searches: u32,
604    pub rules: u32,
605    pub rule_logs: u32,
606    pub sync_log: u32,
607    pub sync_runtime_statuses: u32,
608    pub event_log: u32,
609    pub semantic_profiles: u32,
610    pub semantic_chunks: u32,
611    pub semantic_embeddings: u32,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct RuleFormData {
616    pub id: Option<String>,
617    pub name: String,
618    pub condition: String,
619    pub action: String,
620    pub priority: i32,
621    pub enabled: bool,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct AccountConfigData {
626    pub key: String,
627    pub name: String,
628    pub email: String,
629    pub sync: Option<AccountSyncConfigData>,
630    pub send: Option<AccountSendConfigData>,
631    pub is_default: bool,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
635#[serde(rename_all = "snake_case")]
636pub enum AccountSourceData {
637    Runtime,
638    Config,
639    Both,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
643#[serde(rename_all = "snake_case")]
644pub enum AccountEditModeData {
645    Full,
646    RuntimeOnly,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct AccountSummaryData {
651    pub account_id: AccountId,
652    pub key: Option<String>,
653    pub name: String,
654    pub email: String,
655    pub provider_kind: String,
656    pub sync_kind: Option<String>,
657    pub send_kind: Option<String>,
658    pub enabled: bool,
659    pub is_default: bool,
660    pub source: AccountSourceData,
661    pub editable: AccountEditModeData,
662    pub sync: Option<AccountSyncConfigData>,
663    pub send: Option<AccountSendConfigData>,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
667#[serde(rename_all = "snake_case")]
668pub enum GmailCredentialSourceData {
669    #[default]
670    Bundled,
671    Custom,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
675#[serde(tag = "type", rename_all = "snake_case")]
676pub enum AccountSyncConfigData {
677    Gmail {
678        #[serde(default)]
679        credential_source: GmailCredentialSourceData,
680        client_id: String,
681        client_secret: Option<String>,
682        token_ref: String,
683    },
684    Imap {
685        host: String,
686        port: u16,
687        username: String,
688        password_ref: String,
689        password: Option<String>,
690        use_tls: bool,
691    },
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
695pub struct AccountOperationStep {
696    pub ok: bool,
697    pub detail: String,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct AccountOperationResult {
702    pub ok: bool,
703    pub summary: String,
704    pub save: Option<AccountOperationStep>,
705    pub auth: Option<AccountOperationStep>,
706    pub sync: Option<AccountOperationStep>,
707    pub send: Option<AccountOperationStep>,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
711#[serde(tag = "type", rename_all = "snake_case")]
712pub enum AccountSendConfigData {
713    Gmail,
714    Smtp {
715        host: String,
716        port: u16,
717        username: String,
718        password_ref: String,
719        password: Option<String>,
720        use_tls: bool,
721    },
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
725#[serde(tag = "event")]
726pub enum DaemonEvent {
727    SyncCompleted {
728        account_id: AccountId,
729        messages_synced: u32,
730    },
731    SyncError {
732        account_id: AccountId,
733        error: String,
734    },
735    NewMessages {
736        envelopes: Vec<Envelope>,
737    },
738    MessageUnsnoozed {
739        message_id: MessageId,
740    },
741    LabelCountsUpdated {
742        counts: Vec<LabelCount>,
743    },
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct LabelCount {
748    pub label_id: LabelId,
749    pub unread_count: u32,
750    pub total_count: u32,
751}