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 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#[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#[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}