this repo has no description
1(** Implementation of the JMAP Mail extension, as defined in RFC8621 *)
2
3(** Module for managing JMAP Mail-specific capability URIs *)
4module Capability = struct
5 (** Mail capability URI *)
6 let mail_uri = "urn:ietf:params:jmap:mail"
7
8 (** Submission capability URI *)
9 let submission_uri = "urn:ietf:params:jmap:submission"
10
11 (** Vacation response capability URI *)
12 let vacation_response_uri = "urn:ietf:params:jmap:vacationresponse"
13
14 (** All mail extension capability types *)
15 type t =
16 | Mail (** Mail capability *)
17 | Submission (** Submission capability *)
18 | VacationResponse (** Vacation response capability *)
19 | Extension of string (** Custom extension *)
20
21 (** Convert capability to URI string *)
22 let to_string = function
23 | Mail -> mail_uri
24 | Submission -> submission_uri
25 | VacationResponse -> vacation_response_uri
26 | Extension s -> s
27
28 (** Parse a string to a capability *)
29 let of_string s =
30 if s = mail_uri then Mail
31 else if s = submission_uri then Submission
32 else if s = vacation_response_uri then VacationResponse
33 else Extension s
34
35 (** Check if a capability is a standard mail capability *)
36 let is_standard = function
37 | Mail | Submission | VacationResponse -> true
38 | Extension _ -> false
39
40 (** Check if a capability string is a standard mail capability *)
41 let is_standard_string s =
42 s = mail_uri || s = submission_uri || s = vacation_response_uri
43
44 (** Create a list of capability strings *)
45 let strings_of_capabilities capabilities =
46 List.map to_string capabilities
47end
48
49module Types = struct
50 open Jmap.Types
51
52 (** {1 Mail capabilities} *)
53
54 (** Capability URI for JMAP Mail*)
55 let capability_mail = Capability.mail_uri
56
57 (** Capability URI for JMAP Submission *)
58 let capability_submission = Capability.submission_uri
59
60 (** Capability URI for JMAP Vacation Response *)
61 let capability_vacation_response = Capability.vacation_response_uri
62
63 (** {1:mailbox Mailbox objects} *)
64
65 (** A role for a mailbox. See RFC8621 Section 2. *)
66 type mailbox_role =
67 | All (** All mail *)
68 | Archive (** Archived mail *)
69 | Drafts (** Draft messages *)
70 | Flagged (** Starred/flagged mail *)
71 | Important (** Important mail *)
72 | Inbox (** Inbox *)
73 | Junk (** Spam/Junk mail *)
74 | Sent (** Sent mail *)
75 | Trash (** Deleted/Trash mail *)
76 | Unknown of string (** Server-specific roles *)
77
78 (** A mailbox (folder) in a mail account. See RFC8621 Section 2. *)
79 type mailbox = {
80 id : id;
81 name : string;
82 parent_id : id option;
83 role : mailbox_role option;
84 sort_order : unsigned_int;
85 total_emails : unsigned_int;
86 unread_emails : unsigned_int;
87 total_threads : unsigned_int;
88 unread_threads : unsigned_int;
89 is_subscribed : bool;
90 my_rights : mailbox_rights;
91 }
92
93 (** Rights for a mailbox. See RFC8621 Section 2. *)
94 and mailbox_rights = {
95 may_read_items : bool;
96 may_add_items : bool;
97 may_remove_items : bool;
98 may_set_seen : bool;
99 may_set_keywords : bool;
100 may_create_child : bool;
101 may_rename : bool;
102 may_delete : bool;
103 may_submit : bool;
104 }
105
106 (** Filter condition for mailbox queries. See RFC8621 Section 2.3. *)
107 type mailbox_filter_condition = {
108 parent_id : id option;
109 name : string option;
110 role : string option;
111 has_any_role : bool option;
112 is_subscribed : bool option;
113 }
114
115 type mailbox_query_filter = [
116 | `And of mailbox_query_filter list
117 | `Or of mailbox_query_filter list
118 | `Not of mailbox_query_filter
119 | `Condition of mailbox_filter_condition
120 ]
121
122 (** Mailbox/get request arguments. See RFC8621 Section 2.1. *)
123 type mailbox_get_arguments = {
124 account_id : id;
125 ids : id list option;
126 properties : string list option;
127 }
128
129 (** Mailbox/get response. See RFC8621 Section 2.1. *)
130 type mailbox_get_response = {
131 account_id : id;
132 state : string;
133 list : mailbox list;
134 not_found : id list;
135 }
136
137 (** Mailbox/changes request arguments. See RFC8621 Section 2.2. *)
138 type mailbox_changes_arguments = {
139 account_id : id;
140 since_state : string;
141 max_changes : unsigned_int option;
142 }
143
144 (** Mailbox/changes response. See RFC8621 Section 2.2. *)
145 type mailbox_changes_response = {
146 account_id : id;
147 old_state : string;
148 new_state : string;
149 has_more_changes : bool;
150 created : id list;
151 updated : id list;
152 destroyed : id list;
153 }
154
155 (** Mailbox/query request arguments. See RFC8621 Section 2.3. *)
156 type mailbox_query_arguments = {
157 account_id : id;
158 filter : mailbox_query_filter option;
159 sort : [ `name | `role | `sort_order ] list option;
160 limit : unsigned_int option;
161 }
162
163 (** Mailbox/query response. See RFC8621 Section 2.3. *)
164 type mailbox_query_response = {
165 account_id : id;
166 query_state : string;
167 can_calculate_changes : bool;
168 position : unsigned_int;
169 ids : id list;
170 total : unsigned_int option;
171 }
172
173 (** Mailbox/queryChanges request arguments. See RFC8621 Section 2.4. *)
174 type mailbox_query_changes_arguments = {
175 account_id : id;
176 filter : mailbox_query_filter option;
177 sort : [ `name | `role | `sort_order ] list option;
178 since_query_state : string;
179 max_changes : unsigned_int option;
180 up_to_id : id option;
181 }
182
183 (** Mailbox/queryChanges response. See RFC8621 Section 2.4. *)
184 type mailbox_query_changes_response = {
185 account_id : id;
186 old_query_state : string;
187 new_query_state : string;
188 total : unsigned_int option;
189 removed : id list;
190 added : mailbox_query_changes_added list;
191 }
192
193 and mailbox_query_changes_added = {
194 id : id;
195 index : unsigned_int;
196 }
197
198 (** Mailbox/set request arguments. See RFC8621 Section 2.5. *)
199 type mailbox_set_arguments = {
200 account_id : id;
201 if_in_state : string option;
202 create : (id * mailbox_creation) list option;
203 update : (id * mailbox_update) list option;
204 destroy : id list option;
205 }
206
207 and mailbox_creation = {
208 name : string;
209 parent_id : id option;
210 role : string option;
211 sort_order : unsigned_int option;
212 is_subscribed : bool option;
213 }
214
215 and mailbox_update = {
216 name : string option;
217 parent_id : id option;
218 role : string option;
219 sort_order : unsigned_int option;
220 is_subscribed : bool option;
221 }
222
223 (** Mailbox/set response. See RFC8621 Section 2.5. *)
224 type mailbox_set_response = {
225 account_id : id;
226 old_state : string option;
227 new_state : string;
228 created : (id * mailbox) list option;
229 updated : id list option;
230 destroyed : id list option;
231 not_created : (id * set_error) list option;
232 not_updated : (id * set_error) list option;
233 not_destroyed : (id * set_error) list option;
234 }
235
236 (** {1:thread Thread objects} *)
237
238 (** A thread in a mail account. See RFC8621 Section 3. *)
239 type thread = {
240 id : id;
241 email_ids : id list;
242 }
243
244 (** Thread/get request arguments. See RFC8621 Section 3.1. *)
245 type thread_get_arguments = {
246 account_id : id;
247 ids : id list option;
248 properties : string list option;
249 }
250
251 (** Thread/get response. See RFC8621 Section 3.1. *)
252 type thread_get_response = {
253 account_id : id;
254 state : string;
255 list : thread list;
256 not_found : id list;
257 }
258
259 (** Thread/changes request arguments. See RFC8621 Section 3.2. *)
260 type thread_changes_arguments = {
261 account_id : id;
262 since_state : string;
263 max_changes : unsigned_int option;
264 }
265
266 (** Thread/changes response. See RFC8621 Section 3.2. *)
267 type thread_changes_response = {
268 account_id : id;
269 old_state : string;
270 new_state : string;
271 has_more_changes : bool;
272 created : id list;
273 updated : id list;
274 destroyed : id list;
275 }
276
277 (** {1:email Email objects} *)
278
279 (** Addressing (mailbox) information. See RFC8621 Section 4.1.1. *)
280 type email_address = {
281 name : string option;
282 email : string;
283 parameters : (string * string) list;
284 }
285
286 (** Message header field. See RFC8621 Section 4.1.2. *)
287 type header = {
288 name : string;
289 value : string;
290 }
291
292 (** Email keyword (flag). See RFC8621 Section 4.3. *)
293 type keyword =
294 | Flagged
295 | Answered
296 | Draft
297 | Forwarded
298 | Phishing
299 | Junk
300 | NotJunk
301 | Seen
302 | Unread
303 | Custom of string
304
305 (** Email message. See RFC8621 Section 4. *)
306 type email = {
307 id : id;
308 blob_id : id;
309 thread_id : id;
310 mailbox_ids : (id * bool) list;
311 keywords : (keyword * bool) list;
312 size : unsigned_int;
313 received_at : utc_date;
314 message_id : string list;
315 in_reply_to : string list option;
316 references : string list option;
317 sender : email_address list option;
318 from : email_address list option;
319 to_ : email_address list option;
320 cc : email_address list option;
321 bcc : email_address list option;
322 reply_to : email_address list option;
323 subject : string option;
324 sent_at : utc_date option;
325 has_attachment : bool option;
326 preview : string option;
327 body_values : (string * string) list option;
328 text_body : email_body_part list option;
329 html_body : email_body_part list option;
330 attachments : email_body_part list option;
331 headers : header list option;
332 }
333
334 (** Email body part. See RFC8621 Section 4.1.4. *)
335 and email_body_part = {
336 part_id : string option;
337 blob_id : id option;
338 size : unsigned_int option;
339 headers : header list option;
340 name : string option;
341 type_ : string option;
342 charset : string option;
343 disposition : string option;
344 cid : string option;
345 language : string list option;
346 location : string option;
347 sub_parts : email_body_part list option;
348 header_parameter_name : string option;
349 header_parameter_value : string option;
350 }
351
352 (** Email query filter condition. See RFC8621 Section 4.4. *)
353 type email_filter_condition = {
354 in_mailbox : id option;
355 in_mailbox_other_than : id list option;
356 min_size : unsigned_int option;
357 max_size : unsigned_int option;
358 before : utc_date option;
359 after : utc_date option;
360 header : (string * string) option;
361 from : string option;
362 to_ : string option;
363 cc : string option;
364 bcc : string option;
365 subject : string option;
366 body : string option;
367 has_keyword : string option;
368 not_keyword : string option;
369 has_attachment : bool option;
370 text : string option;
371 }
372
373 type email_query_filter = [
374 | `And of email_query_filter list
375 | `Or of email_query_filter list
376 | `Not of email_query_filter
377 | `Condition of email_filter_condition
378 ]
379
380 (** Email/get request arguments. See RFC8621 Section 4.5. *)
381 type email_get_arguments = {
382 account_id : id;
383 ids : id list option;
384 properties : string list option;
385 body_properties : string list option;
386 fetch_text_body_values : bool option;
387 fetch_html_body_values : bool option;
388 fetch_all_body_values : bool option;
389 max_body_value_bytes : unsigned_int option;
390 }
391
392 (** Email/get response. See RFC8621 Section 4.5. *)
393 type email_get_response = {
394 account_id : id;
395 state : string;
396 list : email list;
397 not_found : id list;
398 }
399
400 (** Email/changes request arguments. See RFC8621 Section 4.6. *)
401 type email_changes_arguments = {
402 account_id : id;
403 since_state : string;
404 max_changes : unsigned_int option;
405 }
406
407 (** Email/changes response. See RFC8621 Section 4.6. *)
408 type email_changes_response = {
409 account_id : id;
410 old_state : string;
411 new_state : string;
412 has_more_changes : bool;
413 created : id list;
414 updated : id list;
415 destroyed : id list;
416 }
417
418 (** Email/query request arguments. See RFC8621 Section 4.4. *)
419 type email_query_arguments = {
420 account_id : id;
421 filter : email_query_filter option;
422 sort : comparator list option;
423 collapse_threads : bool option;
424 position : unsigned_int option;
425 anchor : id option;
426 anchor_offset : int_t option;
427 limit : unsigned_int option;
428 calculate_total : bool option;
429 }
430
431 (** Email/query response. See RFC8621 Section 4.4. *)
432 type email_query_response = {
433 account_id : id;
434 query_state : string;
435 can_calculate_changes : bool;
436 position : unsigned_int;
437 ids : id list;
438 total : unsigned_int option;
439 thread_ids : id list option;
440 }
441
442 (** Email/queryChanges request arguments. See RFC8621 Section 4.7. *)
443 type email_query_changes_arguments = {
444 account_id : id;
445 filter : email_query_filter option;
446 sort : comparator list option;
447 collapse_threads : bool option;
448 since_query_state : string;
449 max_changes : unsigned_int option;
450 up_to_id : id option;
451 }
452
453 (** Email/queryChanges response. See RFC8621 Section 4.7. *)
454 type email_query_changes_response = {
455 account_id : id;
456 old_query_state : string;
457 new_query_state : string;
458 total : unsigned_int option;
459 removed : id list;
460 added : email_query_changes_added list;
461 }
462
463 and email_query_changes_added = {
464 id : id;
465 index : unsigned_int;
466 }
467
468 (** Email/set request arguments. See RFC8621 Section 4.8. *)
469 type email_set_arguments = {
470 account_id : id;
471 if_in_state : string option;
472 create : (id * email_creation) list option;
473 update : (id * email_update) list option;
474 destroy : id list option;
475 }
476
477 and email_creation = {
478 mailbox_ids : (id * bool) list;
479 keywords : (keyword * bool) list option;
480 received_at : utc_date option;
481 message_id : string list option;
482 in_reply_to : string list option;
483 references : string list option;
484 sender : email_address list option;
485 from : email_address list option;
486 to_ : email_address list option;
487 cc : email_address list option;
488 bcc : email_address list option;
489 reply_to : email_address list option;
490 subject : string option;
491 body_values : (string * string) list option;
492 text_body : email_body_part list option;
493 html_body : email_body_part list option;
494 attachments : email_body_part list option;
495 headers : header list option;
496 }
497
498 and email_update = {
499 keywords : (keyword * bool) list option;
500 mailbox_ids : (id * bool) list option;
501 }
502
503 (** Email/set response. See RFC8621 Section 4.8. *)
504 type email_set_response = {
505 account_id : id;
506 old_state : string option;
507 new_state : string;
508 created : (id * email) list option;
509 updated : id list option;
510 destroyed : id list option;
511 not_created : (id * set_error) list option;
512 not_updated : (id * set_error) list option;
513 not_destroyed : (id * set_error) list option;
514 }
515
516 (** Email/copy request arguments. See RFC8621 Section 4.9. *)
517 type email_copy_arguments = {
518 from_account_id : id;
519 account_id : id;
520 create : (id * email_creation) list;
521 on_success_destroy_original : bool option;
522 }
523
524 (** Email/copy response. See RFC8621 Section 4.9. *)
525 type email_copy_response = {
526 from_account_id : id;
527 account_id : id;
528 created : (id * email) list option;
529 not_created : (id * set_error) list option;
530 }
531
532 (** Email/import request arguments. See RFC8621 Section 4.10. *)
533 type email_import_arguments = {
534 account_id : id;
535 emails : (id * email_import) list;
536 }
537
538 and email_import = {
539 blob_id : id;
540 mailbox_ids : (id * bool) list;
541 keywords : (keyword * bool) list option;
542 received_at : utc_date option;
543 }
544
545 (** Email/import response. See RFC8621 Section 4.10. *)
546 type email_import_response = {
547 account_id : id;
548 created : (id * email) list option;
549 not_created : (id * set_error) list option;
550 }
551
552 (** {1:search_snippet Search snippets} *)
553
554 (** SearchSnippet/get request arguments. See RFC8621 Section 4.11. *)
555 type search_snippet_get_arguments = {
556 account_id : id;
557 email_ids : id list;
558 filter : email_filter_condition;
559 }
560
561 (** SearchSnippet/get response. See RFC8621 Section 4.11. *)
562 type search_snippet_get_response = {
563 account_id : id;
564 list : (id * search_snippet) list;
565 not_found : id list;
566 }
567
568 and search_snippet = {
569 subject : string option;
570 preview : string option;
571 }
572
573 (** {1:submission EmailSubmission objects} *)
574
575 (** EmailSubmission address. See RFC8621 Section 5.1. *)
576 type submission_address = {
577 email : string;
578 parameters : (string * string) list option;
579 }
580
581 (** Email submission object. See RFC8621 Section 5.1. *)
582 type email_submission = {
583 id : id;
584 identity_id : id;
585 email_id : id;
586 thread_id : id;
587 envelope : envelope option;
588 send_at : utc_date option;
589 undo_status : [
590 | `pending
591 | `final
592 | `canceled
593 ] option;
594 delivery_status : (string * submission_status) list option;
595 dsn_blob_ids : (string * id) list option;
596 mdn_blob_ids : (string * id) list option;
597 }
598
599 (** Envelope for mail submission. See RFC8621 Section 5.1. *)
600 and envelope = {
601 mail_from : submission_address;
602 rcpt_to : submission_address list;
603 }
604
605 (** Delivery status for submitted email. See RFC8621 Section 5.1. *)
606 and submission_status = {
607 smtp_reply : string;
608 delivered : string option;
609 }
610
611 (** EmailSubmission/get request arguments. See RFC8621 Section 5.3. *)
612 type email_submission_get_arguments = {
613 account_id : id;
614 ids : id list option;
615 properties : string list option;
616 }
617
618 (** EmailSubmission/get response. See RFC8621 Section 5.3. *)
619 type email_submission_get_response = {
620 account_id : id;
621 state : string;
622 list : email_submission list;
623 not_found : id list;
624 }
625
626 (** EmailSubmission/changes request arguments. See RFC8621 Section 5.4. *)
627 type email_submission_changes_arguments = {
628 account_id : id;
629 since_state : string;
630 max_changes : unsigned_int option;
631 }
632
633 (** EmailSubmission/changes response. See RFC8621 Section 5.4. *)
634 type email_submission_changes_response = {
635 account_id : id;
636 old_state : string;
637 new_state : string;
638 has_more_changes : bool;
639 created : id list;
640 updated : id list;
641 destroyed : id list;
642 }
643
644 (** EmailSubmission/query filter condition. See RFC8621 Section 5.5. *)
645 type email_submission_filter_condition = {
646 identity_id : id option;
647 email_id : id option;
648 thread_id : id option;
649 before : utc_date option;
650 after : utc_date option;
651 subject : string option;
652 }
653
654 type email_submission_query_filter = [
655 | `And of email_submission_query_filter list
656 | `Or of email_submission_query_filter list
657 | `Not of email_submission_query_filter
658 | `Condition of email_submission_filter_condition
659 ]
660
661 (** EmailSubmission/query request arguments. See RFC8621 Section 5.5. *)
662 type email_submission_query_arguments = {
663 account_id : id;
664 filter : email_submission_query_filter option;
665 sort : comparator list option;
666 position : unsigned_int option;
667 anchor : id option;
668 anchor_offset : int_t option;
669 limit : unsigned_int option;
670 calculate_total : bool option;
671 }
672
673 (** EmailSubmission/query response. See RFC8621 Section 5.5. *)
674 type email_submission_query_response = {
675 account_id : id;
676 query_state : string;
677 can_calculate_changes : bool;
678 position : unsigned_int;
679 ids : id list;
680 total : unsigned_int option;
681 }
682
683 (** EmailSubmission/set request arguments. See RFC8621 Section 5.6. *)
684 type email_submission_set_arguments = {
685 account_id : id;
686 if_in_state : string option;
687 create : (id * email_submission_creation) list option;
688 update : (id * email_submission_update) list option;
689 destroy : id list option;
690 on_success_update_email : (id * email_update) list option;
691 }
692
693 and email_submission_creation = {
694 email_id : id;
695 identity_id : id;
696 envelope : envelope option;
697 send_at : utc_date option;
698 }
699
700 and email_submission_update = {
701 email_id : id option;
702 identity_id : id option;
703 envelope : envelope option;
704 undo_status : [`canceled] option;
705 }
706
707 (** EmailSubmission/set response. See RFC8621 Section 5.6. *)
708 type email_submission_set_response = {
709 account_id : id;
710 old_state : string option;
711 new_state : string;
712 created : (id * email_submission) list option;
713 updated : id list option;
714 destroyed : id list option;
715 not_created : (id * set_error) list option;
716 not_updated : (id * set_error) list option;
717 not_destroyed : (id * set_error) list option;
718 }
719
720 (** {1:identity Identity objects} *)
721
722 (** Identity for sending mail. See RFC8621 Section 6. *)
723 type identity = {
724 id : id;
725 name : string;
726 email : string;
727 reply_to : email_address list option;
728 bcc : email_address list option;
729 text_signature : string option;
730 html_signature : string option;
731 may_delete : bool;
732 }
733
734 (** Identity/get request arguments. See RFC8621 Section 6.1. *)
735 type identity_get_arguments = {
736 account_id : id;
737 ids : id list option;
738 properties : string list option;
739 }
740
741 (** Identity/get response. See RFC8621 Section 6.1. *)
742 type identity_get_response = {
743 account_id : id;
744 state : string;
745 list : identity list;
746 not_found : id list;
747 }
748
749 (** Identity/changes request arguments. See RFC8621 Section 6.2. *)
750 type identity_changes_arguments = {
751 account_id : id;
752 since_state : string;
753 max_changes : unsigned_int option;
754 }
755
756 (** Identity/changes response. See RFC8621 Section 6.2. *)
757 type identity_changes_response = {
758 account_id : id;
759 old_state : string;
760 new_state : string;
761 has_more_changes : bool;
762 created : id list;
763 updated : id list;
764 destroyed : id list;
765 }
766
767 (** Identity/set request arguments. See RFC8621 Section 6.3. *)
768 type identity_set_arguments = {
769 account_id : id;
770 if_in_state : string option;
771 create : (id * identity_creation) list option;
772 update : (id * identity_update) list option;
773 destroy : id list option;
774 }
775
776 and identity_creation = {
777 name : string;
778 email : string;
779 reply_to : email_address list option;
780 bcc : email_address list option;
781 text_signature : string option;
782 html_signature : string option;
783 }
784
785 and identity_update = {
786 name : string option;
787 email : string option;
788 reply_to : email_address list option;
789 bcc : email_address list option;
790 text_signature : string option;
791 html_signature : string option;
792 }
793
794 (** Identity/set response. See RFC8621 Section 6.3. *)
795 type identity_set_response = {
796 account_id : id;
797 old_state : string option;
798 new_state : string;
799 created : (id * identity) list option;
800 updated : id list option;
801 destroyed : id list option;
802 not_created : (id * set_error) list option;
803 not_updated : (id * set_error) list option;
804 not_destroyed : (id * set_error) list option;
805 }
806
807 (** {1:vacation_response VacationResponse objects} *)
808
809 (** Vacation auto-reply setting. See RFC8621 Section 7. *)
810 type vacation_response = {
811 id : id;
812 is_enabled : bool;
813 from_date : utc_date option;
814 to_date : utc_date option;
815 subject : string option;
816 text_body : string option;
817 html_body : string option;
818 }
819
820 (** VacationResponse/get request arguments. See RFC8621 Section 7.2. *)
821 type vacation_response_get_arguments = {
822 account_id : id;
823 ids : id list option;
824 properties : string list option;
825 }
826
827 (** VacationResponse/get response. See RFC8621 Section 7.2. *)
828 type vacation_response_get_response = {
829 account_id : id;
830 state : string;
831 list : vacation_response list;
832 not_found : id list;
833 }
834
835 (** VacationResponse/set request arguments. See RFC8621 Section 7.3. *)
836 type vacation_response_set_arguments = {
837 account_id : id;
838 if_in_state : string option;
839 update : (id * vacation_response_update) list;
840 }
841
842 and vacation_response_update = {
843 is_enabled : bool option;
844 from_date : utc_date option;
845 to_date : utc_date option;
846 subject : string option;
847 text_body : string option;
848 html_body : string option;
849 }
850
851 (** VacationResponse/set response. See RFC8621 Section 7.3. *)
852 type vacation_response_set_response = {
853 account_id : id;
854 old_state : string option;
855 new_state : string;
856 updated : id list option;
857 not_updated : (id * set_error) list option;
858 }
859
860 (** {1:message_flags Message Flags and Mailbox Attributes} *)
861
862 (** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords *)
863 type flag_color =
864 | Red (** Bit pattern 000 *)
865 | Orange (** Bit pattern 100 *)
866 | Yellow (** Bit pattern 010 *)
867 | Green (** Bit pattern 111 *)
868 | Blue (** Bit pattern 001 *)
869 | Purple (** Bit pattern 101 *)
870 | Gray (** Bit pattern 011 *)
871
872 (** Standard message keywords as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
873 type message_keyword =
874 | Notify (** Indicate a notification should be shown for this message *)
875 | Muted (** User is not interested in future replies to this thread *)
876 | Followed (** User is particularly interested in future replies to this thread *)
877 | Memo (** Message is a note-to-self about another message in the same thread *)
878 | HasMemo (** Message has an associated memo with the $memo keyword *)
879 | HasAttachment (** Message has an attachment *)
880 | HasNoAttachment (** Message does not have an attachment *)
881 | AutoSent (** Message was sent automatically as a response due to a user rule *)
882 | Unsubscribed (** User has unsubscribed from the thread this message is in *)
883 | CanUnsubscribe (** Message has an RFC8058-compliant List-Unsubscribe header *)
884 | Imported (** Message was imported from another mailbox *)
885 | IsTrusted (** Server has verified authenticity of the from name and email *)
886 | MaskedEmail (** Message was received via an alias created for an individual sender *)
887 | New (** Message should be made more prominent due to a recent action *)
888 | MailFlagBit0 (** Bit 0 of the 3-bit flag color pattern *)
889 | MailFlagBit1 (** Bit 1 of the 3-bit flag color pattern *)
890 | MailFlagBit2 (** Bit 2 of the 3-bit flag color pattern *)
891 | OtherKeyword of string (** Other non-standard keywords *)
892
893 (** Special mailbox attribute names as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
894 type mailbox_attribute =
895 | Snoozed (** Mailbox containing messages that have been snoozed *)
896 | Scheduled (** Mailbox containing messages scheduled to be sent later *)
897 | Memos (** Mailbox containing messages with the $memo keyword *)
898 | OtherAttribute of string (** Other non-standard mailbox attributes *)
899
900 (** Functions for working with flag colors based on the specification in
901 draft-ietf-mailmaint-messageflag-mailboxattribute-02, section 3.1. *)
902
903 (** Convert bit pattern to flag color *)
904 let flag_color_of_bits bit0 bit1 bit2 =
905 match (bit0, bit1, bit2) with
906 | (false, false, false) -> Red (* 000 *)
907 | (true, false, false) -> Orange (* 100 *)
908 | (false, true, false) -> Yellow (* 010 *)
909 | (true, true, true) -> Green (* 111 *)
910 | (false, false, true) -> Blue (* 001 *)
911 | (true, false, true) -> Purple (* 101 *)
912 | (false, true, true) -> Gray (* 011 *)
913 | (true, true, false) -> Green (* 110 - not in spec, defaulting to green *)
914
915 (** Get bits for a flag color *)
916 let bits_of_flag_color = function
917 | Red -> (false, false, false)
918 | Orange -> (true, false, false)
919 | Yellow -> (false, true, false)
920 | Green -> (true, true, true)
921 | Blue -> (false, false, true)
922 | Purple -> (true, false, true)
923 | Gray -> (false, true, true)
924
925 (** Check if a keyword list contains a flag color *)
926 let has_flag_color keywords =
927 let has_bit0 = List.exists (function
928 | (Custom s, true) when s = "$MailFlagBit0" -> true
929 | _ -> false
930 ) keywords in
931
932 let has_bit1 = List.exists (function
933 | (Custom s, true) when s = "$MailFlagBit1" -> true
934 | _ -> false
935 ) keywords in
936
937 let has_bit2 = List.exists (function
938 | (Custom s, true) when s = "$MailFlagBit2" -> true
939 | _ -> false
940 ) keywords in
941
942 has_bit0 || has_bit1 || has_bit2
943
944 (** Extract flag color from keywords if present *)
945 let get_flag_color keywords =
946 (* First check if the message has the \Flagged system flag *)
947 let is_flagged = List.exists (function
948 | (Flagged, true) -> true
949 | _ -> false
950 ) keywords in
951
952 if not is_flagged then
953 None
954 else
955 (* Get values of each bit flag *)
956 let bit0 = List.exists (function
957 | (Custom s, true) when s = "$MailFlagBit0" -> true
958 | _ -> false
959 ) keywords in
960
961 let bit1 = List.exists (function
962 | (Custom s, true) when s = "$MailFlagBit1" -> true
963 | _ -> false
964 ) keywords in
965
966 let bit2 = List.exists (function
967 | (Custom s, true) when s = "$MailFlagBit2" -> true
968 | _ -> false
969 ) keywords in
970
971 Some (flag_color_of_bits bit0 bit1 bit2)
972
973 (** Convert a message keyword to its string representation *)
974 let string_of_message_keyword = function
975 | Notify -> "$notify"
976 | Muted -> "$muted"
977 | Followed -> "$followed"
978 | Memo -> "$memo"
979 | HasMemo -> "$hasmemo"
980 | HasAttachment -> "$hasattachment"
981 | HasNoAttachment -> "$hasnoattachment"
982 | AutoSent -> "$autosent"
983 | Unsubscribed -> "$unsubscribed"
984 | CanUnsubscribe -> "$canunsubscribe"
985 | Imported -> "$imported"
986 | IsTrusted -> "$istrusted"
987 | MaskedEmail -> "$maskedemail"
988 | New -> "$new"
989 | MailFlagBit0 -> "$MailFlagBit0"
990 | MailFlagBit1 -> "$MailFlagBit1"
991 | MailFlagBit2 -> "$MailFlagBit2"
992 | OtherKeyword s -> s
993
994 (** Parse a string into a message keyword *)
995 let message_keyword_of_string = function
996 | "$notify" -> Notify
997 | "$muted" -> Muted
998 | "$followed" -> Followed
999 | "$memo" -> Memo
1000 | "$hasmemo" -> HasMemo
1001 | "$hasattachment" -> HasAttachment
1002 | "$hasnoattachment" -> HasNoAttachment
1003 | "$autosent" -> AutoSent
1004 | "$unsubscribed" -> Unsubscribed
1005 | "$canunsubscribe" -> CanUnsubscribe
1006 | "$imported" -> Imported
1007 | "$istrusted" -> IsTrusted
1008 | "$maskedemail" -> MaskedEmail
1009 | "$new" -> New
1010 | "$MailFlagBit0" -> MailFlagBit0
1011 | "$MailFlagBit1" -> MailFlagBit1
1012 | "$MailFlagBit2" -> MailFlagBit2
1013 | s -> OtherKeyword s
1014
1015 (** Convert a mailbox attribute to its string representation *)
1016 let string_of_mailbox_attribute = function
1017 | Snoozed -> "Snoozed"
1018 | Scheduled -> "Scheduled"
1019 | Memos -> "Memos"
1020 | OtherAttribute s -> s
1021
1022 (** Parse a string into a mailbox attribute *)
1023 let mailbox_attribute_of_string = function
1024 | "Snoozed" -> Snoozed
1025 | "Scheduled" -> Scheduled
1026 | "Memos" -> Memos
1027 | s -> OtherAttribute s
1028
1029 (** Get a human-readable representation of a flag color *)
1030 let human_readable_flag_color = function
1031 | Red -> "Red"
1032 | Orange -> "Orange"
1033 | Yellow -> "Yellow"
1034 | Green -> "Green"
1035 | Blue -> "Blue"
1036 | Purple -> "Purple"
1037 | Gray -> "Gray"
1038
1039 (** Get a human-readable representation of a message keyword *)
1040 let human_readable_message_keyword = function
1041 | Notify -> "Notify"
1042 | Muted -> "Muted"
1043 | Followed -> "Followed"
1044 | Memo -> "Memo"
1045 | HasMemo -> "Has Memo"
1046 | HasAttachment -> "Has Attachment"
1047 | HasNoAttachment -> "No Attachment"
1048 | AutoSent -> "Auto Sent"
1049 | Unsubscribed -> "Unsubscribed"
1050 | CanUnsubscribe -> "Can Unsubscribe"
1051 | Imported -> "Imported"
1052 | IsTrusted -> "Trusted"
1053 | MaskedEmail -> "Masked Email"
1054 | New -> "New"
1055 | MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> "Flag Bit"
1056 | OtherKeyword s -> s
1057
1058 (** Format email keywords into a human-readable string representation *)
1059 let format_email_keywords keywords =
1060 (* Get flag color if present *)
1061 let color_str =
1062 match get_flag_color keywords with
1063 | Some color -> human_readable_flag_color color
1064 | None -> ""
1065 in
1066
1067 (* Get standard JMAP keywords *)
1068 let standard_keywords = List.filter_map (fun (kw, active) ->
1069 if not active then None
1070 else match kw with
1071 | Flagged -> Some "Flagged"
1072 | Answered -> Some "Answered"
1073 | Draft -> Some "Draft"
1074 | Forwarded -> Some "Forwarded"
1075 | Phishing -> Some "Phishing"
1076 | Junk -> Some "Junk"
1077 | NotJunk -> Some "Not Junk"
1078 | Seen -> Some "Seen"
1079 | Unread -> Some "Unread"
1080 | _ -> None
1081 ) keywords in
1082
1083 (* Get message keywords *)
1084 let message_keywords = List.filter_map (fun (kw, active) ->
1085 if not active then None
1086 else match kw with
1087 | Custom s ->
1088 (* Try to parse as message keyword *)
1089 let message_kw = message_keyword_of_string s in
1090 (match message_kw with
1091 | OtherKeyword _ -> None
1092 | MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> None
1093 | kw -> Some (human_readable_message_keyword kw))
1094 | _ -> None
1095 ) keywords in
1096
1097 (* Combine all human-readable labels *)
1098 let all_parts =
1099 (if color_str <> "" then [color_str] else []) @
1100 standard_keywords @
1101 message_keywords
1102 in
1103
1104 String.concat ", " all_parts
1105end
1106
1107(** {1 JSON serialization} *)
1108
1109module Json = struct
1110 open Types
1111
1112 (** {2 Helper functions for serialization} *)
1113
1114 let string_of_mailbox_role = function
1115 | All -> "all"
1116 | Archive -> "archive"
1117 | Drafts -> "drafts"
1118 | Flagged -> "flagged"
1119 | Important -> "important"
1120 | Inbox -> "inbox"
1121 | Junk -> "junk"
1122 | Sent -> "sent"
1123 | Trash -> "trash"
1124 | Unknown s -> s
1125
1126 let mailbox_role_of_string = function
1127 | "all" -> All
1128 | "archive" -> Archive
1129 | "drafts" -> Drafts
1130 | "flagged" -> Flagged
1131 | "important" -> Important
1132 | "inbox" -> Inbox
1133 | "junk" -> Junk
1134 | "sent" -> Sent
1135 | "trash" -> Trash
1136 | s -> Unknown s
1137
1138 let string_of_keyword = function
1139 | Flagged -> "$flagged"
1140 | Answered -> "$answered"
1141 | Draft -> "$draft"
1142 | Forwarded -> "$forwarded"
1143 | Phishing -> "$phishing"
1144 | Junk -> "$junk"
1145 | NotJunk -> "$notjunk"
1146 | Seen -> "$seen"
1147 | Unread -> "$unread"
1148 | Custom s -> s
1149
1150 let keyword_of_string = function
1151 | "$flagged" -> Flagged
1152 | "$answered" -> Answered
1153 | "$draft" -> Draft
1154 | "$forwarded" -> Forwarded
1155 | "$phishing" -> Phishing
1156 | "$junk" -> Junk
1157 | "$notjunk" -> NotJunk
1158 | "$seen" -> Seen
1159 | "$unread" -> Unread
1160 | s -> Custom s
1161
1162 (** {2 Mailbox serialization} *)
1163
1164 (** TODO:claude - Need to implement all JSON serialization functions
1165 for each type we've defined. This would be a substantial amount of
1166 code and likely require additional understanding of the ezjsonm API.
1167
1168 For a full implementation, we would need functions to convert between
1169 OCaml types and JSON for each of:
1170 - mailbox, mailbox_rights, mailbox query/update operations
1171 - thread operations
1172 - email, email_address, header, email_body_part
1173 - email query/update operations
1174 - submission operations
1175 - identity operations
1176 - vacation response operations
1177 *)
1178end
1179
1180(** {1 API functions} *)
1181
1182open Lwt.Syntax
1183open Jmap.Api
1184open Jmap.Types
1185
1186(** Authentication credentials for a JMAP server *)
1187type credentials = {
1188 username: string;
1189 password: string;
1190}
1191
1192(** Connection to a JMAP mail server *)
1193type connection = {
1194 session: Jmap.Types.session;
1195 config: Jmap.Api.config;
1196}
1197
1198(** Convert JSON mail object to OCaml type *)
1199let mailbox_of_json json =
1200 try
1201 let open Ezjsonm in
1202 let id = get_string (find json ["id"]) in
1203 let name = get_string (find json ["name"]) in
1204 (* Handle parentId which can be null *)
1205 let parent_id =
1206 match find_opt json ["parentId"] with
1207 | Some (`Null) -> None
1208 | Some (`String s) -> Some s
1209 | None -> None
1210 | _ -> None
1211 in
1212 (* Handle role which might be null *)
1213 let role =
1214 match find_opt json ["role"] with
1215 | Some (`Null) -> None
1216 | Some (`String s) -> Some (Json.mailbox_role_of_string s)
1217 | None -> None
1218 | _ -> None
1219 in
1220 let sort_order = get_int (find json ["sortOrder"]) in
1221 let total_emails = get_int (find json ["totalEmails"]) in
1222 let unread_emails = get_int (find json ["unreadEmails"]) in
1223 let total_threads = get_int (find json ["totalThreads"]) in
1224 let unread_threads = get_int (find json ["unreadThreads"]) in
1225 let is_subscribed = get_bool (find json ["isSubscribed"]) in
1226 let rights_json = find json ["myRights"] in
1227 let my_rights = {
1228 Types.may_read_items = get_bool (find rights_json ["mayReadItems"]);
1229 may_add_items = get_bool (find rights_json ["mayAddItems"]);
1230 may_remove_items = get_bool (find rights_json ["mayRemoveItems"]);
1231 may_set_seen = get_bool (find rights_json ["maySetSeen"]);
1232 may_set_keywords = get_bool (find rights_json ["maySetKeywords"]);
1233 may_create_child = get_bool (find rights_json ["mayCreateChild"]);
1234 may_rename = get_bool (find rights_json ["mayRename"]);
1235 may_delete = get_bool (find rights_json ["mayDelete"]);
1236 may_submit = get_bool (find rights_json ["maySubmit"]);
1237 } in
1238 let result = {
1239 Types.id;
1240 name;
1241 parent_id;
1242 role;
1243 sort_order;
1244 total_emails;
1245 unread_emails;
1246 total_threads;
1247 unread_threads;
1248 is_subscribed;
1249 my_rights;
1250 } in
1251 Ok (result)
1252 with
1253 | Not_found ->
1254 Error (Parse_error "Required field not found in mailbox object")
1255 | Invalid_argument msg ->
1256 Error (Parse_error msg)
1257 | e ->
1258 Error (Parse_error (Printexc.to_string e))
1259
1260(** Convert JSON email object to OCaml type *)
1261let email_of_json json =
1262 try
1263 let open Ezjsonm in
1264
1265 let id = get_string (find json ["id"]) in
1266 let blob_id = get_string (find json ["blobId"]) in
1267 let thread_id = get_string (find json ["threadId"]) in
1268
1269 (* Process mailboxIds map *)
1270 let mailbox_ids_json = find json ["mailboxIds"] in
1271 let mailbox_ids = match mailbox_ids_json with
1272 | `O items -> List.map (fun (id, v) -> (id, get_bool v)) items
1273 | _ -> raise (Invalid_argument "mailboxIds is not an object")
1274 in
1275
1276 (* Process keywords map *)
1277 let keywords_json = find json ["keywords"] in
1278 let keywords = match keywords_json with
1279 | `O items -> List.map (fun (k, v) ->
1280 (Json.keyword_of_string k, get_bool v)) items
1281 | _ -> raise (Invalid_argument "keywords is not an object")
1282 in
1283
1284 let size = get_int (find json ["size"]) in
1285 let received_at = get_string (find json ["receivedAt"]) in
1286
1287 (* Handle messageId which might be an array or missing *)
1288 let message_id =
1289 match find_opt json ["messageId"] with
1290 | Some (`A ids) -> List.map (fun id ->
1291 match id with
1292 | `String s -> s
1293 | _ -> raise (Invalid_argument "messageId item is not a string")
1294 ) ids
1295 | Some (`String s) -> [s] (* Handle single string case *)
1296 | None -> [] (* Handle missing case *)
1297 | _ -> raise (Invalid_argument "messageId has unexpected type")
1298 in
1299
1300 (* Parse optional fields *)
1301 let parse_email_addresses opt_json =
1302 match opt_json with
1303 | Some (`A items) ->
1304 Some (List.map (fun addr_json ->
1305 let name =
1306 match find_opt addr_json ["name"] with
1307 | Some (`String s) -> Some s
1308 | Some (`Null) -> None
1309 | None -> None
1310 | _ -> None
1311 in
1312 let email = get_string (find addr_json ["email"]) in
1313 let parameters =
1314 match find_opt addr_json ["parameters"] with
1315 | Some (`O items) -> List.map (fun (k, v) ->
1316 match v with
1317 | `String s -> (k, s)
1318 | _ -> (k, "")
1319 ) items
1320 | _ -> []
1321 in
1322 { Types.name; email; parameters }
1323 ) items)
1324 | _ -> None
1325 in
1326
1327 (* Handle optional string arrays with null handling *)
1328 let parse_string_array_opt field_name =
1329 match find_opt json [field_name] with
1330 | Some (`A ids) ->
1331 Some (List.filter_map (function
1332 | `String s -> Some s
1333 | _ -> None
1334 ) ids)
1335 | Some (`Null) -> None
1336 | None -> None
1337 | _ -> None
1338 in
1339
1340 let in_reply_to = parse_string_array_opt "inReplyTo" in
1341 let references = parse_string_array_opt "references" in
1342
1343 let sender = parse_email_addresses (find_opt json ["sender"]) in
1344 let from = parse_email_addresses (find_opt json ["from"]) in
1345 let to_ = parse_email_addresses (find_opt json ["to"]) in
1346 let cc = parse_email_addresses (find_opt json ["cc"]) in
1347 let bcc = parse_email_addresses (find_opt json ["bcc"]) in
1348 let reply_to = parse_email_addresses (find_opt json ["replyTo"]) in
1349
1350 (* Handle optional string fields with null handling *)
1351 let parse_string_opt field_name =
1352 match find_opt json [field_name] with
1353 | Some (`String s) -> Some s
1354 | Some (`Null) -> None
1355 | None -> None
1356 | _ -> None
1357 in
1358
1359 let subject = parse_string_opt "subject" in
1360 let sent_at = parse_string_opt "sentAt" in
1361
1362 (* Handle optional boolean fields with null handling *)
1363 let parse_bool_opt field_name =
1364 match find_opt json [field_name] with
1365 | Some (`Bool b) -> Some b
1366 | Some (`Null) -> None
1367 | None -> None
1368 | _ -> None
1369 in
1370
1371 let has_attachment = parse_bool_opt "hasAttachment" in
1372 let preview = parse_string_opt "preview" in
1373
1374 (* TODO Body parts parsing would go here - omitting for brevity *)
1375 Ok ({
1376 Types.id;
1377 blob_id;
1378 thread_id;
1379 mailbox_ids;
1380 keywords;
1381 size;
1382 received_at;
1383 message_id;
1384 in_reply_to;
1385 references;
1386 sender;
1387 from;
1388 to_;
1389 cc;
1390 bcc;
1391 reply_to;
1392 subject;
1393 sent_at;
1394 has_attachment;
1395 preview;
1396 body_values = None;
1397 text_body = None;
1398 html_body = None;
1399 attachments = None;
1400 headers = None;
1401 })
1402 with
1403 | Not_found ->
1404 Error (Parse_error "Required field not found in email object")
1405 | Invalid_argument msg ->
1406 Error (Parse_error msg)
1407 | e ->
1408 Error (Parse_error (Printexc.to_string e))
1409
1410(** Login to a JMAP server and establish a connection
1411 @param uri The URI of the JMAP server
1412 @param credentials Authentication credentials
1413 @return A connection object if successful
1414
1415 TODO:claude *)
1416let login ~uri ~credentials =
1417 let* session_result = get_session (Uri.of_string uri)
1418 ~username:credentials.username
1419 ~authentication_token:credentials.password
1420 () in
1421 match session_result with
1422 | Ok session ->
1423 let api_uri = Uri.of_string session.api_url in
1424 let config = {
1425 api_uri;
1426 username = credentials.username;
1427 authentication_token = credentials.password;
1428 } in
1429 Lwt.return (Ok { session; config })
1430 | Error e -> Lwt.return (Error e)
1431
1432(** Login to a JMAP server using an API token
1433 @param uri The URI of the JMAP server
1434 @param api_token The API token for authentication
1435 @return A connection object if successful
1436
1437 TODO:claude *)
1438let login_with_token ~uri ~api_token =
1439 let* session_result = get_session (Uri.of_string uri)
1440 ~api_token
1441 () in
1442 match session_result with
1443 | Ok session ->
1444 let api_uri = Uri.of_string session.api_url in
1445 let config = {
1446 api_uri;
1447 username = ""; (* Empty username indicates we're using token auth *)
1448 authentication_token = api_token;
1449 } in
1450 Lwt.return (Ok { session; config })
1451 | Error e -> Lwt.return (Error e)
1452
1453(** Get all mailboxes for an account
1454 @param conn The JMAP connection
1455 @param account_id The account ID to get mailboxes for
1456 @return A list of mailboxes if successful
1457
1458 TODO:claude *)
1459let get_mailboxes conn ~account_id =
1460 let request = {
1461 using = [
1462 Jmap.Capability.to_string Jmap.Capability.Core;
1463 Capability.to_string Capability.Mail
1464 ];
1465 method_calls = [
1466 {
1467 name = "Mailbox/get";
1468 arguments = `O [
1469 ("accountId", `String account_id);
1470 ];
1471 method_call_id = "m1";
1472 }
1473 ];
1474 created_ids = None;
1475 } in
1476
1477 let* response_result = make_request conn.config request in
1478 match response_result with
1479 | Ok response ->
1480 let result =
1481 try
1482 let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1483 inv.name = "Mailbox/get") response.method_responses in
1484 let args = method_response.arguments in
1485 match Ezjsonm.find_opt args ["list"] with
1486 | Some (`A mailbox_list) ->
1487 let parse_results = List.map mailbox_of_json mailbox_list in
1488 let (successes, failures) = List.partition Result.is_ok parse_results in
1489 if List.length failures > 0 then
1490 Error (Parse_error "Failed to parse some mailboxes")
1491 else
1492 Ok (List.map Result.get_ok successes)
1493 | _ -> Error (Parse_error "Mailbox list not found in response")
1494 with
1495 | Not_found -> Error (Parse_error "Mailbox/get method response not found")
1496 | e -> Error (Parse_error (Printexc.to_string e))
1497 in
1498 Lwt.return result
1499 | Error e -> Lwt.return (Error e)
1500
1501(** Get a specific mailbox by ID
1502 @param conn The JMAP connection
1503 @param account_id The account ID
1504 @param mailbox_id The mailbox ID to retrieve
1505 @return The mailbox if found
1506
1507 TODO:claude *)
1508let get_mailbox conn ~account_id ~mailbox_id =
1509 let request = {
1510 using = [
1511 Jmap.Capability.to_string Jmap.Capability.Core;
1512 Capability.to_string Capability.Mail
1513 ];
1514 method_calls = [
1515 {
1516 name = "Mailbox/get";
1517 arguments = `O [
1518 ("accountId", `String account_id);
1519 ("ids", `A [`String mailbox_id]);
1520 ];
1521 method_call_id = "m1";
1522 }
1523 ];
1524 created_ids = None;
1525 } in
1526
1527 let* response_result = make_request conn.config request in
1528 match response_result with
1529 | Ok response ->
1530 let result =
1531 try
1532 let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1533 inv.name = "Mailbox/get") response.method_responses in
1534 let args = method_response.arguments in
1535 match Ezjsonm.find_opt args ["list"] with
1536 | Some (`A [mailbox]) -> mailbox_of_json mailbox
1537 | Some (`A []) -> Error (Parse_error ("Mailbox not found: " ^ mailbox_id))
1538 | _ -> Error (Parse_error "Expected single mailbox in response")
1539 with
1540 | Not_found -> Error (Parse_error "Mailbox/get method response not found")
1541 | e -> Error (Parse_error (Printexc.to_string e))
1542 in
1543 Lwt.return result
1544 | Error e -> Lwt.return (Error e)
1545
1546(** Get messages in a mailbox
1547 @param conn The JMAP connection
1548 @param account_id The account ID
1549 @param mailbox_id The mailbox ID to get messages from
1550 @param limit Optional limit on number of messages to return
1551 @return The list of email messages if successful
1552
1553 TODO:claude *)
1554let get_messages_in_mailbox conn ~account_id ~mailbox_id ?limit () =
1555 (* First query the emails in the mailbox *)
1556 let query_request = {
1557 using = [
1558 Jmap.Capability.to_string Jmap.Capability.Core;
1559 Capability.to_string Capability.Mail
1560 ];
1561 method_calls = [
1562 {
1563 name = "Email/query";
1564 arguments = `O ([
1565 ("accountId", `String account_id);
1566 ("filter", `O [("inMailbox", `String mailbox_id)]);
1567 ("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
1568 ] @ (match limit with
1569 | Some l -> [("limit", `Float (float_of_int l))]
1570 | None -> []
1571 ));
1572 method_call_id = "q1";
1573 }
1574 ];
1575 created_ids = None;
1576 } in
1577
1578 let* query_result = make_request conn.config query_request in
1579 match query_result with
1580 | Ok query_response ->
1581 (try
1582 let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1583 inv.name = "Email/query") query_response.method_responses in
1584 let args = query_method.arguments in
1585 match Ezjsonm.find_opt args ["ids"] with
1586 | Some (`A ids) ->
1587 let email_ids = List.map (function
1588 | `String id -> id
1589 | _ -> raise (Invalid_argument "Email ID is not a string")
1590 ) ids in
1591
1592 (* If we have IDs, fetch the actual email objects *)
1593 if List.length email_ids > 0 then
1594 let get_request = {
1595 using = [
1596 Jmap.Capability.to_string Jmap.Capability.Core;
1597 Capability.to_string Capability.Mail
1598 ];
1599 method_calls = [
1600 {
1601 name = "Email/get";
1602 arguments = `O [
1603 ("accountId", `String account_id);
1604 ("ids", `A (List.map (fun id -> `String id) email_ids));
1605 ];
1606 method_call_id = "g1";
1607 }
1608 ];
1609 created_ids = None;
1610 } in
1611
1612 let* get_result = make_request conn.config get_request in
1613 match get_result with
1614 | Ok get_response ->
1615 (try
1616 let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1617 inv.name = "Email/get") get_response.method_responses in
1618 let args = get_method.arguments in
1619 match Ezjsonm.find_opt args ["list"] with
1620 | Some (`A email_list) ->
1621 let parse_results = List.map email_of_json email_list in
1622 let (successes, failures) = List.partition Result.is_ok parse_results in
1623 if List.length failures > 0 then
1624 Lwt.return (Error (Parse_error "Failed to parse some emails"))
1625 else
1626 Lwt.return (Ok (List.map Result.get_ok successes))
1627 | _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
1628 with
1629 | Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
1630 | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1631 | Error e -> Lwt.return (Error e)
1632 else
1633 (* No emails in mailbox *)
1634 Lwt.return (Ok [])
1635
1636 | _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
1637 with
1638 | Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
1639 | Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
1640 | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1641 | Error e -> Lwt.return (Error e)
1642
1643(** Get a single email message by ID
1644 @param conn The JMAP connection
1645 @param account_id The account ID
1646 @param email_id The email ID to retrieve
1647 @return The email message if found
1648
1649 TODO:claude *)
1650let get_email conn ~account_id ~email_id =
1651 let request = {
1652 using = [
1653 Jmap.Capability.to_string Jmap.Capability.Core;
1654 Capability.to_string Capability.Mail
1655 ];
1656 method_calls = [
1657 {
1658 name = "Email/get";
1659 arguments = `O [
1660 ("accountId", `String account_id);
1661 ("ids", `A [`String email_id]);
1662 ];
1663 method_call_id = "m1";
1664 }
1665 ];
1666 created_ids = None;
1667 } in
1668
1669 let* response_result = make_request conn.config request in
1670 match response_result with
1671 | Ok response ->
1672 let result =
1673 try
1674 let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1675 inv.name = "Email/get") response.method_responses in
1676 let args = method_response.arguments in
1677 match Ezjsonm.find_opt args ["list"] with
1678 | Some (`A [email]) -> email_of_json email
1679 | Some (`A []) -> Error (Parse_error ("Email not found: " ^ email_id))
1680 | _ -> Error (Parse_error "Expected single email in response")
1681 with
1682 | Not_found -> Error (Parse_error "Email/get method response not found")
1683 | e -> Error (Parse_error (Printexc.to_string e))
1684 in
1685 Lwt.return result
1686 | Error e -> Lwt.return (Error e)
1687
1688(** Helper functions for working with message flags and mailbox attributes *)
1689
1690(** Check if an email has a specific message keyword
1691 @param email The email to check
1692 @param keyword The message keyword to look for
1693 @return true if the email has the keyword, false otherwise
1694
1695 TODO:claude *)
1696let has_message_keyword (email:Types.email) keyword =
1697 let open Types in
1698 let keyword_string = string_of_message_keyword keyword in
1699 List.exists (function
1700 | (Custom s, true) when s = keyword_string -> true
1701 | _ -> false
1702 ) email.keywords
1703
1704(** Add a message keyword to an email
1705 @param conn The JMAP connection
1706 @param account_id The account ID
1707 @param email_id The email ID
1708 @param keyword The message keyword to add
1709 @return Success or error
1710
1711 TODO:claude *)
1712let add_message_keyword conn ~account_id ~email_id ~keyword =
1713 let keyword_string = Types.string_of_message_keyword keyword in
1714
1715 let request = {
1716 using = [
1717 Jmap.Capability.to_string Jmap.Capability.Core;
1718 Capability.to_string Capability.Mail
1719 ];
1720 method_calls = [
1721 {
1722 name = "Email/set";
1723 arguments = `O [
1724 ("accountId", `String account_id);
1725 ("update", `O [
1726 (email_id, `O [
1727 ("keywords", `O [
1728 (keyword_string, `Bool true)
1729 ])
1730 ])
1731 ]);
1732 ];
1733 method_call_id = "m1";
1734 }
1735 ];
1736 created_ids = None;
1737 } in
1738
1739 let* response_result = make_request conn.config request in
1740 match response_result with
1741 | Ok response ->
1742 let result =
1743 try
1744 let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1745 inv.name = "Email/set") response.method_responses in
1746 let args = method_response.arguments in
1747 match Ezjsonm.find_opt args ["updated"] with
1748 | Some (`A _ids) -> Ok ()
1749 | _ ->
1750 match Ezjsonm.find_opt args ["notUpdated"] with
1751 | Some (`O _errors) ->
1752 Error (Parse_error ("Failed to update email: " ^ email_id))
1753 | _ -> Error (Parse_error "Unexpected response format")
1754 with
1755 | Not_found -> Error (Parse_error "Email/set method response not found")
1756 | e -> Error (Parse_error (Printexc.to_string e))
1757 in
1758 Lwt.return result
1759 | Error e -> Lwt.return (Error e)
1760
1761(** Set a flag color for an email
1762 @param conn The JMAP connection
1763 @param account_id The account ID
1764 @param email_id The email ID
1765 @param color The flag color to set
1766 @return Success or error
1767
1768 TODO:claude *)
1769let set_flag_color conn ~account_id ~email_id ~color =
1770 (* Get the bit pattern for the color *)
1771 let (bit0, bit1, bit2) = Types.bits_of_flag_color color in
1772
1773 (* Build the keywords update object *)
1774 let keywords = [
1775 ("$flagged", `Bool true);
1776 ("$MailFlagBit0", `Bool bit0);
1777 ("$MailFlagBit1", `Bool bit1);
1778 ("$MailFlagBit2", `Bool bit2);
1779 ] in
1780
1781 let request = {
1782 using = [
1783 Jmap.Capability.to_string Jmap.Capability.Core;
1784 Capability.to_string Capability.Mail
1785 ];
1786 method_calls = [
1787 {
1788 name = "Email/set";
1789 arguments = `O [
1790 ("accountId", `String account_id);
1791 ("update", `O [
1792 (email_id, `O [
1793 ("keywords", `O keywords)
1794 ])
1795 ]);
1796 ];
1797 method_call_id = "m1";
1798 }
1799 ];
1800 created_ids = None;
1801 } in
1802
1803 let* response_result = make_request conn.config request in
1804 match response_result with
1805 | Ok response ->
1806 let result =
1807 try
1808 let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1809 inv.name = "Email/set") response.method_responses in
1810 let args = method_response.arguments in
1811 match Ezjsonm.find_opt args ["updated"] with
1812 | Some (`A _ids) -> Ok ()
1813 | _ ->
1814 match Ezjsonm.find_opt args ["notUpdated"] with
1815 | Some (`O _errors) ->
1816 Error (Parse_error ("Failed to update email: " ^ email_id))
1817 | _ -> Error (Parse_error "Unexpected response format")
1818 with
1819 | Not_found -> Error (Parse_error "Email/set method response not found")
1820 | e -> Error (Parse_error (Printexc.to_string e))
1821 in
1822 Lwt.return result
1823 | Error e -> Lwt.return (Error e)
1824
1825(** Convert an email's keywords to typed message_keyword list
1826 @param email The email to analyze
1827 @return List of message keywords
1828
1829 TODO:claude *)
1830let get_message_keywords (email:Types.email) =
1831 let open Types in
1832 List.filter_map (function
1833 | (Custom s, true) -> Some (message_keyword_of_string s)
1834 | _ -> None
1835 ) email.keywords
1836
1837(** Get emails with a specific message keyword
1838 @param conn The JMAP connection
1839 @param account_id The account ID
1840 @param keyword The message keyword to search for
1841 @param limit Optional limit on number of emails to return
1842 @return List of emails with the keyword if successful
1843
1844 TODO:claude *)
1845let get_emails_with_keyword conn ~account_id ~keyword ?limit () =
1846 let keyword_string = Types.string_of_message_keyword keyword in
1847
1848 (* Query for emails with the specified keyword *)
1849 let query_request = {
1850 using = [
1851 Jmap.Capability.to_string Jmap.Capability.Core;
1852 Capability.to_string Capability.Mail
1853 ];
1854 method_calls = [
1855 {
1856 name = "Email/query";
1857 arguments = `O ([
1858 ("accountId", `String account_id);
1859 ("filter", `O [("hasKeyword", `String keyword_string)]);
1860 ("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
1861 ] @ (match limit with
1862 | Some l -> [("limit", `Float (float_of_int l))]
1863 | None -> []
1864 ));
1865 method_call_id = "q1";
1866 }
1867 ];
1868 created_ids = None;
1869 } in
1870
1871 let* query_result = make_request conn.config query_request in
1872 match query_result with
1873 | Ok query_response ->
1874 (try
1875 let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1876 inv.name = "Email/query") query_response.method_responses in
1877 let args = query_method.arguments in
1878 match Ezjsonm.find_opt args ["ids"] with
1879 | Some (`A ids) ->
1880 let email_ids = List.map (function
1881 | `String id -> id
1882 | _ -> raise (Invalid_argument "Email ID is not a string")
1883 ) ids in
1884
1885 (* If we have IDs, fetch the actual email objects *)
1886 if List.length email_ids > 0 then
1887 let get_request = {
1888 using = [
1889 Jmap.Capability.to_string Jmap.Capability.Core;
1890 Capability.to_string Capability.Mail
1891 ];
1892 method_calls = [
1893 {
1894 name = "Email/get";
1895 arguments = `O [
1896 ("accountId", `String account_id);
1897 ("ids", `A (List.map (fun id -> `String id) email_ids));
1898 ];
1899 method_call_id = "g1";
1900 }
1901 ];
1902 created_ids = None;
1903 } in
1904
1905 let* get_result = make_request conn.config get_request in
1906 match get_result with
1907 | Ok get_response ->
1908 (try
1909 let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1910 inv.name = "Email/get") get_response.method_responses in
1911 let args = get_method.arguments in
1912 match Ezjsonm.find_opt args ["list"] with
1913 | Some (`A email_list) ->
1914 let parse_results = List.map email_of_json email_list in
1915 let (successes, failures) = List.partition Result.is_ok parse_results in
1916 if List.length failures > 0 then
1917 Lwt.return (Error (Parse_error "Failed to parse some emails"))
1918 else
1919 Lwt.return (Ok (List.map Result.get_ok successes))
1920 | _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
1921 with
1922 | Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
1923 | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1924 | Error e -> Lwt.return (Error e)
1925 else
1926 (* No emails with the keyword *)
1927 Lwt.return (Ok [])
1928
1929 | _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
1930 with
1931 | Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
1932 | Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
1933 | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1934 | Error e -> Lwt.return (Error e)
1935
1936(** {1 Email Address Utilities} *)
1937
1938(** Custom implementation of substring matching *)
1939let contains_substring str sub =
1940 try
1941 let _ = Str.search_forward (Str.regexp_string sub) str 0 in
1942 true
1943 with Not_found -> false
1944
1945(** Checks if a pattern with wildcards matches a string
1946 @param pattern Pattern string with * and ? wildcards
1947 @param str String to match against
1948 Based on simple recursive wildcard matching algorithm
1949*)
1950let matches_wildcard pattern str =
1951 let pattern_len = String.length pattern in
1952 let str_len = String.length str in
1953
1954 (* Convert both to lowercase for case-insensitive matching *)
1955 let pattern = String.lowercase_ascii pattern in
1956 let str = String.lowercase_ascii str in
1957
1958 (* If there are no wildcards, do a simple substring check *)
1959 if not (String.contains pattern '*' || String.contains pattern '?') then
1960 contains_substring str pattern
1961 else
1962 (* Classic recursive matching algorithm *)
1963 let rec match_from p_pos s_pos =
1964 (* Pattern matched to the end *)
1965 if p_pos = pattern_len then
1966 s_pos = str_len
1967 (* Star matches zero or more chars *)
1968 else if pattern.[p_pos] = '*' then
1969 match_from (p_pos + 1) s_pos || (* Match empty string *)
1970 (s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
1971 (* If both have more chars and they match or ? wildcard *)
1972 else if s_pos < str_len &&
1973 (pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
1974 match_from (p_pos + 1) (s_pos + 1)
1975 else
1976 false
1977 in
1978
1979 match_from 0 0
1980
1981(** Check if an email address matches a filter string
1982 @param email The email address to check
1983 @param pattern The filter pattern to match against
1984 @return True if the email address matches the filter
1985*)
1986let email_address_matches email pattern =
1987 matches_wildcard pattern email
1988
1989(** Check if an email matches a sender filter
1990 @param email The email object to check
1991 @param pattern The sender filter pattern
1992 @return True if any sender address matches the filter
1993*)
1994let email_matches_sender (email : Types.email) pattern =
1995 (* Helper to extract emails from address list *)
1996 let addresses_match addrs =
1997 List.exists (fun (addr : Types.email_address) ->
1998 email_address_matches addr.email pattern
1999 ) addrs
2000 in
2001
2002 (* Check From addresses first *)
2003 let from_match =
2004 match email.Types.from with
2005 | Some addrs -> addresses_match addrs
2006 | None -> false
2007 in
2008
2009 (* If no match in From, check Sender field *)
2010 if from_match then true
2011 else
2012 match email.Types.sender with
2013 | Some addrs -> addresses_match addrs
2014 | None -> false