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