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 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 PrepareReply {
194 message_id: MessageId,
195 reply_all: bool,
196 },
197 PrepareForward {
198 message_id: MessageId,
199 },
200 SendDraft {
201 draft: Draft,
202 },
203 SaveDraftToServer {
205 draft: Draft,
206 },
207 ListDrafts,
208
209 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#[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#[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#[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}