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