···
1
-
(** Implementation of the JMAP Mail extension, as defined in RFC8621 *)
3
-
(** Module for managing JMAP Mail-specific capability URIs *)
4
-
module Capability = struct
5
-
(** Mail capability URI *)
6
-
let mail_uri = "urn:ietf:params:jmap:mail"
8
-
(** Submission capability URI *)
9
-
let submission_uri = "urn:ietf:params:jmap:submission"
11
-
(** Vacation response capability URI *)
12
-
let vacation_response_uri = "urn:ietf:params:jmap:vacationresponse"
14
-
(** All mail extension capability types *)
16
-
| Mail (** Mail capability *)
17
-
| Submission (** Submission capability *)
18
-
| VacationResponse (** Vacation response capability *)
19
-
| Extension of string (** Custom extension *)
21
-
(** Convert capability to URI string *)
22
-
let to_string = function
24
-
| Submission -> submission_uri
25
-
| VacationResponse -> vacation_response_uri
28
-
(** Parse a string to a capability *)
30
-
if s = mail_uri then Mail
31
-
else if s = submission_uri then Submission
32
-
else if s = vacation_response_uri then VacationResponse
35
-
(** Check if a capability is a standard mail capability *)
36
-
let is_standard = function
37
-
| Mail | Submission | VacationResponse -> true
38
-
| Extension _ -> false
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
44
-
(** Create a list of capability strings *)
45
-
let strings_of_capabilities capabilities =
46
-
List.map to_string capabilities
49
-
module Types = struct
52
-
(** {1 Mail capabilities} *)
54
-
(** Capability URI for JMAP Mail*)
55
-
let capability_mail = Capability.mail_uri
57
-
(** Capability URI for JMAP Submission *)
58
-
let capability_submission = Capability.submission_uri
60
-
(** Capability URI for JMAP Vacation Response *)
61
-
let capability_vacation_response = Capability.vacation_response_uri
63
-
(** {1:mailbox Mailbox objects} *)
65
-
(** A role for a mailbox. See RFC8621 Section 2. *)
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 *)
78
-
(** A mailbox (folder) in a mail account. See RFC8621 Section 2. *)
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;
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;
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;
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
122
-
(** Mailbox/get request arguments. See RFC8621 Section 2.1. *)
123
-
type mailbox_get_arguments = {
125
-
ids : id list option;
126
-
properties : string list option;
129
-
(** Mailbox/get response. See RFC8621 Section 2.1. *)
130
-
type mailbox_get_response = {
133
-
list : mailbox list;
134
-
not_found : id list;
137
-
(** Mailbox/changes request arguments. See RFC8621 Section 2.2. *)
138
-
type mailbox_changes_arguments = {
140
-
since_state : string;
141
-
max_changes : unsigned_int option;
144
-
(** Mailbox/changes response. See RFC8621 Section 2.2. *)
145
-
type mailbox_changes_response = {
147
-
old_state : string;
148
-
new_state : string;
149
-
has_more_changes : bool;
152
-
destroyed : id list;
155
-
(** Mailbox/query request arguments. See RFC8621 Section 2.3. *)
156
-
type mailbox_query_arguments = {
158
-
filter : mailbox_query_filter option;
159
-
sort : [ `name | `role | `sort_order ] list option;
160
-
limit : unsigned_int option;
163
-
(** Mailbox/query response. See RFC8621 Section 2.3. *)
164
-
type mailbox_query_response = {
166
-
query_state : string;
167
-
can_calculate_changes : bool;
168
-
position : unsigned_int;
170
-
total : unsigned_int option;
173
-
(** Mailbox/queryChanges request arguments. See RFC8621 Section 2.4. *)
174
-
type mailbox_query_changes_arguments = {
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;
183
-
(** Mailbox/queryChanges response. See RFC8621 Section 2.4. *)
184
-
type mailbox_query_changes_response = {
186
-
old_query_state : string;
187
-
new_query_state : string;
188
-
total : unsigned_int option;
190
-
added : mailbox_query_changes_added list;
193
-
and mailbox_query_changes_added = {
195
-
index : unsigned_int;
198
-
(** Mailbox/set request arguments. See RFC8621 Section 2.5. *)
199
-
type mailbox_set_arguments = {
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;
207
-
and mailbox_creation = {
209
-
parent_id : id option;
210
-
role : string option;
211
-
sort_order : unsigned_int option;
212
-
is_subscribed : bool option;
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;
223
-
(** Mailbox/set response. See RFC8621 Section 2.5. *)
224
-
type mailbox_set_response = {
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;
236
-
(** {1:thread Thread objects} *)
238
-
(** A thread in a mail account. See RFC8621 Section 3. *)
241
-
email_ids : id list;
244
-
(** Thread/get request arguments. See RFC8621 Section 3.1. *)
245
-
type thread_get_arguments = {
247
-
ids : id list option;
248
-
properties : string list option;
251
-
(** Thread/get response. See RFC8621 Section 3.1. *)
252
-
type thread_get_response = {
255
-
list : thread list;
256
-
not_found : id list;
259
-
(** Thread/changes request arguments. See RFC8621 Section 3.2. *)
260
-
type thread_changes_arguments = {
262
-
since_state : string;
263
-
max_changes : unsigned_int option;
266
-
(** Thread/changes response. See RFC8621 Section 3.2. *)
267
-
type thread_changes_response = {
269
-
old_state : string;
270
-
new_state : string;
271
-
has_more_changes : bool;
274
-
destroyed : id list;
277
-
(** {1:email Email objects} *)
279
-
(** Addressing (mailbox) information. See RFC8621 Section 4.1.1. *)
280
-
type email_address = {
281
-
name : string option;
283
-
parameters : (string * string) list;
286
-
(** Message header field. See RFC8621 Section 4.1.2. *)
292
-
(** Email keyword (flag). See RFC8621 Section 4.3. *)
305
-
(** Email message. See RFC8621 Section 4. *)
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;
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;
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;
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
380
-
(** Email/get request arguments. See RFC8621 Section 4.5. *)
381
-
type email_get_arguments = {
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;
392
-
(** Email/get response. See RFC8621 Section 4.5. *)
393
-
type email_get_response = {
397
-
not_found : id list;
400
-
(** Email/changes request arguments. See RFC8621 Section 4.6. *)
401
-
type email_changes_arguments = {
403
-
since_state : string;
404
-
max_changes : unsigned_int option;
407
-
(** Email/changes response. See RFC8621 Section 4.6. *)
408
-
type email_changes_response = {
410
-
old_state : string;
411
-
new_state : string;
412
-
has_more_changes : bool;
415
-
destroyed : id list;
418
-
(** Email/query request arguments. See RFC8621 Section 4.4. *)
419
-
type email_query_arguments = {
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;
431
-
(** Email/query response. See RFC8621 Section 4.4. *)
432
-
type email_query_response = {
434
-
query_state : string;
435
-
can_calculate_changes : bool;
436
-
position : unsigned_int;
438
-
total : unsigned_int option;
439
-
thread_ids : id list option;
442
-
(** Email/queryChanges request arguments. See RFC8621 Section 4.7. *)
443
-
type email_query_changes_arguments = {
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;
453
-
(** Email/queryChanges response. See RFC8621 Section 4.7. *)
454
-
type email_query_changes_response = {
456
-
old_query_state : string;
457
-
new_query_state : string;
458
-
total : unsigned_int option;
460
-
added : email_query_changes_added list;
463
-
and email_query_changes_added = {
465
-
index : unsigned_int;
468
-
(** Email/set request arguments. See RFC8621 Section 4.8. *)
469
-
type email_set_arguments = {
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;
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;
498
-
and email_update = {
499
-
keywords : (keyword * bool) list option;
500
-
mailbox_ids : (id * bool) list option;
503
-
(** Email/set response. See RFC8621 Section 4.8. *)
504
-
type email_set_response = {
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;
516
-
(** Email/copy request arguments. See RFC8621 Section 4.9. *)
517
-
type email_copy_arguments = {
518
-
from_account_id : id;
520
-
create : (id * email_creation) list;
521
-
on_success_destroy_original : bool option;
524
-
(** Email/copy response. See RFC8621 Section 4.9. *)
525
-
type email_copy_response = {
526
-
from_account_id : id;
528
-
created : (id * email) list option;
529
-
not_created : (id * set_error) list option;
532
-
(** Email/import request arguments. See RFC8621 Section 4.10. *)
533
-
type email_import_arguments = {
535
-
emails : (id * email_import) list;
538
-
and email_import = {
540
-
mailbox_ids : (id * bool) list;
541
-
keywords : (keyword * bool) list option;
542
-
received_at : utc_date option;
545
-
(** Email/import response. See RFC8621 Section 4.10. *)
546
-
type email_import_response = {
548
-
created : (id * email) list option;
549
-
not_created : (id * set_error) list option;
552
-
(** {1:search_snippet Search snippets} *)
554
-
(** SearchSnippet/get request arguments. See RFC8621 Section 4.11. *)
555
-
type search_snippet_get_arguments = {
557
-
email_ids : id list;
558
-
filter : email_filter_condition;
561
-
(** SearchSnippet/get response. See RFC8621 Section 4.11. *)
562
-
type search_snippet_get_response = {
564
-
list : (id * search_snippet) list;
565
-
not_found : id list;
568
-
and search_snippet = {
569
-
subject : string option;
570
-
preview : string option;
573
-
(** {1:submission EmailSubmission objects} *)
575
-
(** EmailSubmission address. See RFC8621 Section 5.1. *)
576
-
type submission_address = {
578
-
parameters : (string * string) list option;
581
-
(** Email submission object. See RFC8621 Section 5.1. *)
582
-
type email_submission = {
587
-
envelope : envelope option;
588
-
send_at : utc_date 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;
599
-
(** Envelope for mail submission. See RFC8621 Section 5.1. *)
601
-
mail_from : submission_address;
602
-
rcpt_to : submission_address list;
605
-
(** Delivery status for submitted email. See RFC8621 Section 5.1. *)
606
-
and submission_status = {
607
-
smtp_reply : string;
608
-
delivered : string option;
611
-
(** EmailSubmission/get request arguments. See RFC8621 Section 5.3. *)
612
-
type email_submission_get_arguments = {
614
-
ids : id list option;
615
-
properties : string list option;
618
-
(** EmailSubmission/get response. See RFC8621 Section 5.3. *)
619
-
type email_submission_get_response = {
622
-
list : email_submission list;
623
-
not_found : id list;
626
-
(** EmailSubmission/changes request arguments. See RFC8621 Section 5.4. *)
627
-
type email_submission_changes_arguments = {
629
-
since_state : string;
630
-
max_changes : unsigned_int option;
633
-
(** EmailSubmission/changes response. See RFC8621 Section 5.4. *)
634
-
type email_submission_changes_response = {
636
-
old_state : string;
637
-
new_state : string;
638
-
has_more_changes : bool;
641
-
destroyed : id list;
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;
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
661
-
(** EmailSubmission/query request arguments. See RFC8621 Section 5.5. *)
662
-
type email_submission_query_arguments = {
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;
673
-
(** EmailSubmission/query response. See RFC8621 Section 5.5. *)
674
-
type email_submission_query_response = {
676
-
query_state : string;
677
-
can_calculate_changes : bool;
678
-
position : unsigned_int;
680
-
total : unsigned_int option;
683
-
(** EmailSubmission/set request arguments. See RFC8621 Section 5.6. *)
684
-
type email_submission_set_arguments = {
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;
693
-
and email_submission_creation = {
696
-
envelope : envelope option;
697
-
send_at : utc_date option;
700
-
and email_submission_update = {
701
-
email_id : id option;
702
-
identity_id : id option;
703
-
envelope : envelope option;
704
-
undo_status : [`canceled] option;
707
-
(** EmailSubmission/set response. See RFC8621 Section 5.6. *)
708
-
type email_submission_set_response = {
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;
720
-
(** {1:identity Identity objects} *)
722
-
(** Identity for sending mail. See RFC8621 Section 6. *)
727
-
reply_to : email_address list option;
728
-
bcc : email_address list option;
729
-
text_signature : string option;
730
-
html_signature : string option;
734
-
(** Identity/get request arguments. See RFC8621 Section 6.1. *)
735
-
type identity_get_arguments = {
737
-
ids : id list option;
738
-
properties : string list option;
741
-
(** Identity/get response. See RFC8621 Section 6.1. *)
742
-
type identity_get_response = {
745
-
list : identity list;
746
-
not_found : id list;
749
-
(** Identity/changes request arguments. See RFC8621 Section 6.2. *)
750
-
type identity_changes_arguments = {
752
-
since_state : string;
753
-
max_changes : unsigned_int option;
756
-
(** Identity/changes response. See RFC8621 Section 6.2. *)
757
-
type identity_changes_response = {
759
-
old_state : string;
760
-
new_state : string;
761
-
has_more_changes : bool;
764
-
destroyed : id list;
767
-
(** Identity/set request arguments. See RFC8621 Section 6.3. *)
768
-
type identity_set_arguments = {
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;
776
-
and identity_creation = {
779
-
reply_to : email_address list option;
780
-
bcc : email_address list option;
781
-
text_signature : string option;
782
-
html_signature : string option;
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;
794
-
(** Identity/set response. See RFC8621 Section 6.3. *)
795
-
type identity_set_response = {
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;
807
-
(** {1:vacation_response VacationResponse objects} *)
809
-
(** Vacation auto-reply setting. See RFC8621 Section 7. *)
810
-
type vacation_response = {
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;
820
-
(** VacationResponse/get request arguments. See RFC8621 Section 7.2. *)
821
-
type vacation_response_get_arguments = {
823
-
ids : id list option;
824
-
properties : string list option;
827
-
(** VacationResponse/get response. See RFC8621 Section 7.2. *)
828
-
type vacation_response_get_response = {
831
-
list : vacation_response list;
832
-
not_found : id list;
835
-
(** VacationResponse/set request arguments. See RFC8621 Section 7.3. *)
836
-
type vacation_response_set_arguments = {
838
-
if_in_state : string option;
839
-
update : (id * vacation_response_update) list;
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;
851
-
(** VacationResponse/set response. See RFC8621 Section 7.3. *)
852
-
type vacation_response_set_response = {
854
-
old_state : string option;
855
-
new_state : string;
856
-
updated : id list option;
857
-
not_updated : (id * set_error) list option;
860
-
(** {1:message_flags Message Flags and Mailbox Attributes} *)
862
-
(** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords *)
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 *)
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 *)
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 *)
900
-
(** Functions for working with flag colors based on the specification in
901
-
draft-ietf-mailmaint-messageflag-mailboxattribute-02, section 3.1. *)
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 *)
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)
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
932
-
let has_bit1 = List.exists (function
933
-
| (Custom s, true) when s = "$MailFlagBit1" -> true
937
-
let has_bit2 = List.exists (function
938
-
| (Custom s, true) when s = "$MailFlagBit2" -> true
942
-
has_bit0 || has_bit1 || has_bit2
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
952
-
if not is_flagged then
955
-
(* Get values of each bit flag *)
956
-
let bit0 = List.exists (function
957
-
| (Custom s, true) when s = "$MailFlagBit0" -> true
961
-
let bit1 = List.exists (function
962
-
| (Custom s, true) when s = "$MailFlagBit1" -> true
966
-
let bit2 = List.exists (function
967
-
| (Custom s, true) when s = "$MailFlagBit2" -> true
971
-
Some (flag_color_of_bits bit0 bit1 bit2)
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"
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"
989
-
| MailFlagBit0 -> "$MailFlagBit0"
990
-
| MailFlagBit1 -> "$MailFlagBit1"
991
-
| MailFlagBit2 -> "$MailFlagBit2"
992
-
| OtherKeyword s -> s
994
-
(** Parse a string into a message keyword *)
995
-
let message_keyword_of_string = function
996
-
| "$notify" -> Notify
997
-
| "$muted" -> Muted
998
-
| "$followed" -> Followed
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
1010
-
| "$MailFlagBit0" -> MailFlagBit0
1011
-
| "$MailFlagBit1" -> MailFlagBit1
1012
-
| "$MailFlagBit2" -> MailFlagBit2
1013
-
| s -> OtherKeyword s
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
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
1029
-
(** Get a human-readable representation of a flag color *)
1030
-
let human_readable_flag_color = function
1032
-
| Orange -> "Orange"
1033
-
| Yellow -> "Yellow"
1034
-
| Green -> "Green"
1036
-
| Purple -> "Purple"
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"
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"
1055
-
| MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> "Flag Bit"
1056
-
| OtherKeyword s -> s
1058
-
(** Format email keywords into a human-readable string representation *)
1059
-
let format_email_keywords keywords =
1060
-
(* Get flag color if present *)
1062
-
match get_flag_color keywords with
1063
-
| Some color -> human_readable_flag_color color
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"
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
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))
1097
-
(* Combine all human-readable labels *)
1099
-
(if color_str <> "" then [color_str] else []) @
1100
-
standard_keywords @
1104
-
String.concat ", " all_parts
1107
-
(** {1 JSON serialization} *)
1109
-
module Json = struct
1112
-
(** {2 Helper functions for serialization} *)
1114
-
let string_of_mailbox_role = function
1116
-
| Archive -> "archive"
1117
-
| Drafts -> "drafts"
1118
-
| Flagged -> "flagged"
1119
-
| Important -> "important"
1120
-
| Inbox -> "inbox"
1123
-
| Trash -> "trash"
1126
-
let mailbox_role_of_string = function
1128
-
| "archive" -> Archive
1129
-
| "drafts" -> Drafts
1130
-
| "flagged" -> Flagged
1131
-
| "important" -> Important
1132
-
| "inbox" -> Inbox
1135
-
| "trash" -> Trash
1138
-
let string_of_keyword = function
1139
-
| Flagged -> "$flagged"
1140
-
| Answered -> "$answered"
1141
-
| Draft -> "$draft"
1142
-
| Forwarded -> "$forwarded"
1143
-
| Phishing -> "$phishing"
1145
-
| NotJunk -> "$notjunk"
1147
-
| Unread -> "$unread"
1150
-
let keyword_of_string = function
1151
-
| "$flagged" -> Flagged
1152
-
| "$answered" -> Answered
1153
-
| "$draft" -> Draft
1154
-
| "$forwarded" -> Forwarded
1155
-
| "$phishing" -> Phishing
1157
-
| "$notjunk" -> NotJunk
1159
-
| "$unread" -> Unread
1162
-
(** {2 Mailbox serialization} *)
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.
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
1180
-
(** {1 API functions} *)
1186
-
(** Authentication credentials for a JMAP server *)
1187
-
type credentials = {
1192
-
(** Connection to a JMAP mail server *)
1193
-
type connection = {
1194
-
session: Jmap.Types.session;
1195
-
config: Jmap.Api.config;
1198
-
(** Convert JSON mail object to OCaml type *)
1199
-
let mailbox_of_json json =
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 *)
1206
-
match find_opt json ["parentId"] with
1207
-
| Some (`Null) -> None
1208
-
| Some (`String s) -> Some s
1212
-
(* Handle role which might be null *)
1214
-
match find_opt json ["role"] with
1215
-
| Some (`Null) -> None
1216
-
| Some (`String s) -> Some (Json.mailbox_role_of_string s)
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
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"]);
1254
-
Error (Parse_error "Required field not found in mailbox object")
1255
-
| Invalid_argument msg ->
1256
-
Error (Parse_error msg)
1258
-
Error (Parse_error (Printexc.to_string e))
1260
-
(** Convert JSON email object to OCaml type *)
1261
-
let email_of_json json =
1263
-
let open Ezjsonm in
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
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")
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")
1284
-
let size = get_int (find json ["size"]) in
1285
-
let received_at = get_string (find json ["receivedAt"]) in
1287
-
(* Handle messageId which might be an array or missing *)
1289
-
match find_opt json ["messageId"] with
1290
-
| Some (`A ids) -> List.map (fun id ->
1293
-
| _ -> raise (Invalid_argument "messageId item is not a string")
1295
-
| Some (`String s) -> [s] (* Handle single string case *)
1296
-
| None -> [] (* Handle missing case *)
1297
-
| _ -> raise (Invalid_argument "messageId has unexpected type")
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 ->
1306
-
match find_opt addr_json ["name"] with
1307
-
| Some (`String s) -> Some s
1308
-
| Some (`Null) -> None
1312
-
let email = get_string (find addr_json ["email"]) in
1314
-
match find_opt addr_json ["parameters"] with
1315
-
| Some (`O items) -> List.map (fun (k, v) ->
1317
-
| `String s -> (k, s)
1322
-
{ Types.name; email; parameters }
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
1335
-
| Some (`Null) -> None
1340
-
let in_reply_to = parse_string_array_opt "inReplyTo" in
1341
-
let references = parse_string_array_opt "references" in
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
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
1359
-
let subject = parse_string_opt "subject" in
1360
-
let sent_at = parse_string_opt "sentAt" in
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
1371
-
let has_attachment = parse_bool_opt "hasAttachment" in
1372
-
let preview = parse_string_opt "preview" in
1374
-
(* TODO Body parts parsing would go here - omitting for brevity *)
1396
-
body_values = None;
1399
-
attachments = None;
1404
-
Error (Parse_error "Required field not found in email object")
1405
-
| Invalid_argument msg ->
1406
-
Error (Parse_error msg)
1408
-
Error (Parse_error (Printexc.to_string e))
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
1416
-
let login ~uri ~credentials =
1417
-
let* session_result = get_session (Uri.of_string uri)
1418
-
~username:credentials.username
1419
-
~authentication_token:credentials.password
1421
-
match session_result with
1423
-
let api_uri = Uri.of_string session.api_url in
1426
-
username = credentials.username;
1427
-
authentication_token = credentials.password;
1429
-
Lwt.return (Ok { session; config })
1430
-
| Error e -> Lwt.return (Error e)
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
1438
-
let login_with_token ~uri ~api_token =
1439
-
let* session_result = get_session (Uri.of_string uri)
1442
-
match session_result with
1444
-
let api_uri = Uri.of_string session.api_url in
1447
-
username = ""; (* Empty username indicates we're using token auth *)
1448
-
authentication_token = api_token;
1450
-
Lwt.return (Ok { session; config })
1451
-
| Error e -> Lwt.return (Error e)
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
1459
-
let get_mailboxes conn ~account_id =
1462
-
Jmap.Capability.to_string Jmap.Capability.Core;
1463
-
Capability.to_string Capability.Mail
1467
-
name = "Mailbox/get";
1469
-
("accountId", `String account_id);
1471
-
method_call_id = "m1";
1474
-
created_ids = None;
1477
-
let* response_result = make_request conn.config request in
1478
-
match response_result with
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")
1492
-
Ok (List.map Result.get_ok successes)
1493
-
| _ -> Error (Parse_error "Mailbox list not found in response")
1495
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
1496
-
| e -> Error (Parse_error (Printexc.to_string e))
1499
-
| Error e -> Lwt.return (Error e)
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
1508
-
let get_mailbox conn ~account_id ~mailbox_id =
1511
-
Jmap.Capability.to_string Jmap.Capability.Core;
1512
-
Capability.to_string Capability.Mail
1516
-
name = "Mailbox/get";
1518
-
("accountId", `String account_id);
1519
-
("ids", `A [`String mailbox_id]);
1521
-
method_call_id = "m1";
1524
-
created_ids = None;
1527
-
let* response_result = make_request conn.config request in
1528
-
match response_result with
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")
1540
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
1541
-
| e -> Error (Parse_error (Printexc.to_string e))
1544
-
| Error e -> Lwt.return (Error e)
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
1554
-
let get_messages_in_mailbox conn ~account_id ~mailbox_id ?limit () =
1555
-
(* First query the emails in the mailbox *)
1556
-
let query_request = {
1558
-
Jmap.Capability.to_string Jmap.Capability.Core;
1559
-
Capability.to_string Capability.Mail
1563
-
name = "Email/query";
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))]
1572
-
method_call_id = "q1";
1575
-
created_ids = None;
1578
-
let* query_result = make_request conn.config query_request in
1579
-
match query_result with
1580
-
| Ok query_response ->
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")
1592
-
(* If we have IDs, fetch the actual email objects *)
1593
-
if List.length email_ids > 0 then
1594
-
let get_request = {
1596
-
Jmap.Capability.to_string Jmap.Capability.Core;
1597
-
Capability.to_string Capability.Mail
1601
-
name = "Email/get";
1603
-
("accountId", `String account_id);
1604
-
("ids", `A (List.map (fun id -> `String id) email_ids));
1606
-
method_call_id = "g1";
1609
-
created_ids = None;
1612
-
let* get_result = make_request conn.config get_request in
1613
-
match get_result with
1614
-
| Ok get_response ->
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"))
1626
-
Lwt.return (Ok (List.map Result.get_ok successes))
1627
-
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
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)
1633
-
(* No emails in mailbox *)
1634
-
Lwt.return (Ok [])
1636
-
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
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)
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
1650
-
let get_email conn ~account_id ~email_id =
1653
-
Jmap.Capability.to_string Jmap.Capability.Core;
1654
-
Capability.to_string Capability.Mail
1658
-
name = "Email/get";
1660
-
("accountId", `String account_id);
1661
-
("ids", `A [`String email_id]);
1663
-
method_call_id = "m1";
1666
-
created_ids = None;
1669
-
let* response_result = make_request conn.config request in
1670
-
match response_result with
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")
1682
-
| Not_found -> Error (Parse_error "Email/get method response not found")
1683
-
| e -> Error (Parse_error (Printexc.to_string e))
1686
-
| Error e -> Lwt.return (Error e)
1688
-
(** Helper functions for working with message flags and mailbox attributes *)
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
1696
-
let has_message_keyword (email:Types.email) keyword =
1698
-
let keyword_string = string_of_message_keyword keyword in
1699
-
List.exists (function
1700
-
| (Custom s, true) when s = keyword_string -> true
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
1712
-
let add_message_keyword conn ~account_id ~email_id ~keyword =
1713
-
let keyword_string = Types.string_of_message_keyword keyword in
1717
-
Jmap.Capability.to_string Jmap.Capability.Core;
1718
-
Capability.to_string Capability.Mail
1722
-
name = "Email/set";
1724
-
("accountId", `String account_id);
1728
-
(keyword_string, `Bool true)
1733
-
method_call_id = "m1";
1736
-
created_ids = None;
1739
-
let* response_result = make_request conn.config request in
1740
-
match response_result with
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 ()
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")
1755
-
| Not_found -> Error (Parse_error "Email/set method response not found")
1756
-
| e -> Error (Parse_error (Printexc.to_string e))
1759
-
| Error e -> Lwt.return (Error e)
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
1769
-
let 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
1773
-
(* Build the keywords update object *)
1775
-
("$flagged", `Bool true);
1776
-
("$MailFlagBit0", `Bool bit0);
1777
-
("$MailFlagBit1", `Bool bit1);
1778
-
("$MailFlagBit2", `Bool bit2);
1783
-
Jmap.Capability.to_string Jmap.Capability.Core;
1784
-
Capability.to_string Capability.Mail
1788
-
name = "Email/set";
1790
-
("accountId", `String account_id);
1793
-
("keywords", `O keywords)
1797
-
method_call_id = "m1";
1800
-
created_ids = None;
1803
-
let* response_result = make_request conn.config request in
1804
-
match response_result with
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 ()
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")
1819
-
| Not_found -> Error (Parse_error "Email/set method response not found")
1820
-
| e -> Error (Parse_error (Printexc.to_string e))
1823
-
| Error e -> Lwt.return (Error e)
1825
-
(** Convert an email's keywords to typed message_keyword list
1826
-
@param email The email to analyze
1827
-
@return List of message keywords
1830
-
let get_message_keywords (email:Types.email) =
1832
-
List.filter_map (function
1833
-
| (Custom s, true) -> Some (message_keyword_of_string s)
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
1845
-
let get_emails_with_keyword conn ~account_id ~keyword ?limit () =
1846
-
let keyword_string = Types.string_of_message_keyword keyword in
1848
-
(* Query for emails with the specified keyword *)
1849
-
let query_request = {
1851
-
Jmap.Capability.to_string Jmap.Capability.Core;
1852
-
Capability.to_string Capability.Mail
1856
-
name = "Email/query";
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))]
1865
-
method_call_id = "q1";
1868
-
created_ids = None;
1871
-
let* query_result = make_request conn.config query_request in
1872
-
match query_result with
1873
-
| Ok query_response ->
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")
1885
-
(* If we have IDs, fetch the actual email objects *)
1886
-
if List.length email_ids > 0 then
1887
-
let get_request = {
1889
-
Jmap.Capability.to_string Jmap.Capability.Core;
1890
-
Capability.to_string Capability.Mail
1894
-
name = "Email/get";
1896
-
("accountId", `String account_id);
1897
-
("ids", `A (List.map (fun id -> `String id) email_ids));
1899
-
method_call_id = "g1";
1902
-
created_ids = None;
1905
-
let* get_result = make_request conn.config get_request in
1906
-
match get_result with
1907
-
| Ok get_response ->
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"))
1919
-
Lwt.return (Ok (List.map Result.get_ok successes))
1920
-
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
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)
1926
-
(* No emails with the keyword *)
1927
-
Lwt.return (Ok [])
1929
-
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
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)
1936
-
(** {1 Email Submission} *)
1938
-
(** Create a new email draft
1939
-
@param conn The JMAP connection
1940
-
@param account_id The account ID
1941
-
@param mailbox_id The mailbox ID to store the draft in (usually "drafts")
1942
-
@param from The sender's email address
1943
-
@param to_addresses List of recipient email addresses
1944
-
@param subject The email subject line
1945
-
@param text_body Plain text message body
1946
-
@param html_body Optional HTML message body
1947
-
@return The created email ID if successful
1951
-
let create_email_draft conn ~account_id ~mailbox_id ~from ~to_addresses ~subject ~text_body ?html_body () =
1952
-
(* Create email addresses *)
1954
-
Types.name = None;
1959
-
let to_addrs = List.map (fun addr -> {
1960
-
Types.name = None;
1963
-
}) to_addresses in
1965
-
(* Create text body part *)
1967
-
Types.part_id = Some "part1";
1972
-
type_ = Some "text/plain";
1973
-
charset = Some "utf-8";
1974
-
disposition = None;
1979
-
header_parameter_name = None;
1980
-
header_parameter_value = None;
1983
-
(* Create HTML body part if provided *)
1984
-
let html_part_opt = match html_body with
1985
-
| Some _html -> Some {
1986
-
Types.part_id = Some "part2";
1991
-
type_ = Some "text/html";
1992
-
charset = Some "utf-8";
1993
-
disposition = None;
1998
-
header_parameter_name = None;
1999
-
header_parameter_value = None;
2004
-
(* Create body values *)
2005
-
let body_values = [
2006
-
("part1", text_body)
2007
-
] @ (match html_body with
2008
-
| Some html -> [("part2", html)]
2012
-
(* Create email *)
2013
-
let html_body_list = match html_part_opt with
2014
-
| Some part -> Some [part]
2018
-
let _email_creation = {
2019
-
Types.mailbox_ids = [(mailbox_id, true)];
2020
-
keywords = Some [(Draft, true)];
2021
-
received_at = None; (* Server will set this *)
2022
-
message_id = None; (* Server will generate this *)
2023
-
in_reply_to = None;
2024
-
references = None;
2026
-
from = Some [from_addr];
2027
-
to_ = Some to_addrs;
2031
-
subject = Some subject;
2032
-
body_values = Some body_values;
2033
-
text_body = Some [text_part];
2034
-
html_body = html_body_list;
2035
-
attachments = None;
2041
-
Jmap.Capability.to_string Jmap.Capability.Core;
2042
-
Capability.to_string Capability.Mail
2046
-
name = "Email/set";
2048
-
("accountId", `String account_id);
2052
-
("mailboxIds", `O [(mailbox_id, `Bool true)]);
2053
-
("keywords", `O [("$draft", `Bool true)]);
2054
-
("from", `A [`O [("name", `Null); ("email", `String from)]]);
2055
-
("to", `A (List.map (fun addr ->
2056
-
`O [("name", `Null); ("email", `String addr)]
2058
-
("subject", `String subject);
2059
-
("bodyStructure", `O [
2060
-
("type", `String "multipart/alternative");
2063
-
("partId", `String "part1");
2064
-
("type", `String "text/plain")
2067
-
("partId", `String "part2");
2068
-
("type", `String "text/html")
2072
-
("bodyValues", `O ([
2073
-
("part1", `O [("value", `String text_body)])
2074
-
] @ (match html_body with
2075
-
| Some html -> [("part2", `O [("value", `String html)])]
2076
-
| None -> [("part2", `O [("value", `String ("<html><body>" ^ text_body ^ "</body></html>"))])]
2082
-
method_call_id = "m1";
2085
-
created_ids = None;
2088
-
let* response_result = make_request conn.config request in
2089
-
match response_result with
2093
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2094
-
inv.name = "Email/set") response.method_responses in
2095
-
let args = method_response.arguments in
2096
-
match Ezjsonm.find_opt args ["created"] with
2097
-
| Some (`O created) ->
2098
-
let draft_created = List.find_opt (fun (id, _) -> id = "draft1") created in
2099
-
(match draft_created with
2100
-
| Some (_, json) ->
2101
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
2103
-
| None -> Error (Parse_error "Created email not found in response"))
2105
-
match Ezjsonm.find_opt args ["notCreated"] with
2106
-
| Some (`O errors) ->
2107
-
let error_msg = match List.find_opt (fun (id, _) -> id = "draft1") errors with
2108
-
| Some (_, err) ->
2109
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
2111
-
match Ezjsonm.find_opt err ["description"] with
2112
-
| Some (`String desc) -> desc
2113
-
| _ -> "Unknown error"
2115
-
"Error type: " ^ type_ ^ ", Description: " ^ description
2116
-
| None -> "Unknown error"
2118
-
Error (Parse_error ("Failed to create email: " ^ error_msg))
2119
-
| _ -> Error (Parse_error "Unexpected response format")
2121
-
| Not_found -> Error (Parse_error "Email/set method response not found")
2122
-
| e -> Error (Parse_error (Printexc.to_string e))
2125
-
| Error e -> Lwt.return (Error e)
2127
-
(** Get all identities for an account
2128
-
@param conn The JMAP connection
2129
-
@param account_id The account ID
2130
-
@return A list of identities if successful
2134
-
let get_identities conn ~account_id =
2137
-
Jmap.Capability.to_string Jmap.Capability.Core;
2138
-
Capability.to_string Capability.Submission
2142
-
name = "Identity/get";
2144
-
("accountId", `String account_id);
2146
-
method_call_id = "m1";
2149
-
created_ids = None;
2152
-
let* response_result = make_request conn.config request in
2153
-
match response_result with
2157
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2158
-
inv.name = "Identity/get") response.method_responses in
2159
-
let args = method_response.arguments in
2160
-
match Ezjsonm.find_opt args ["list"] with
2161
-
| Some (`A identities) ->
2162
-
let parse_identity json =
2164
-
let open Ezjsonm in
2165
-
let id = get_string (find json ["id"]) in
2166
-
let name = get_string (find json ["name"]) in
2167
-
let email = get_string (find json ["email"]) in
2169
-
let parse_email_addresses field =
2170
-
match find_opt json [field] with
2171
-
| Some (`A items) ->
2172
-
Some (List.map (fun addr_json ->
2174
-
match find_opt addr_json ["name"] with
2175
-
| Some (`String s) -> Some s
2176
-
| Some (`Null) -> None
2180
-
let email = get_string (find addr_json ["email"]) in
2182
-
match find_opt addr_json ["parameters"] with
2183
-
| Some (`O items) -> List.map (fun (k, v) ->
2185
-
| `String s -> (k, s)
2190
-
{ Types.name; email; parameters }
2195
-
let reply_to = parse_email_addresses "replyTo" in
2196
-
let bcc = parse_email_addresses "bcc" in
2198
-
let text_signature =
2199
-
match find_opt json ["textSignature"] with
2200
-
| Some (`String s) -> Some s
2204
-
let html_signature =
2205
-
match find_opt json ["htmlSignature"] with
2206
-
| Some (`String s) -> Some s
2211
-
match find_opt json ["mayDelete"] with
2212
-
| Some (`Bool b) -> b
2216
-
(* Create our own identity record for simplicity *)
2217
-
let r : Types.identity = {
2221
-
reply_to = reply_to;
2223
-
text_signature = text_signature;
2224
-
html_signature = html_signature;
2225
-
may_delete = may_delete
2228
-
| Not_found -> Error (Parse_error "Required field not found in identity object")
2229
-
| Invalid_argument msg -> Error (Parse_error msg)
2230
-
| e -> Error (Parse_error (Printexc.to_string e))
2233
-
let results = List.map parse_identity identities in
2234
-
let (successes, failures) = List.partition Result.is_ok results in
2235
-
if List.length failures > 0 then
2236
-
Error (Parse_error "Failed to parse some identity objects")
2238
-
Ok (List.map Result.get_ok successes)
2239
-
| _ -> Error (Parse_error "Identity list not found in response")
2241
-
| Not_found -> Error (Parse_error "Identity/get method response not found")
2242
-
| e -> Error (Parse_error (Printexc.to_string e))
2245
-
| Error e -> Lwt.return (Error e)
2247
-
(** Find a suitable identity by email address
2248
-
@param conn The JMAP connection
2249
-
@param account_id The account ID
2250
-
@param email The email address to match
2251
-
@return The identity if found, otherwise Error
2255
-
let find_identity_by_email conn ~account_id ~email =
2256
-
let* identities_result = get_identities conn ~account_id in
2257
-
match identities_result with
2258
-
| Ok identities -> begin
2259
-
let matching_identity = List.find_opt (fun (identity:Types.identity) ->
2261
-
if String.lowercase_ascii identity.email = String.lowercase_ascii email then
2264
-
(* Wildcard match (e.g., *@example.com) *)
2265
-
let parts = String.split_on_char '@' identity.email in
2266
-
if List.length parts = 2 && List.hd parts = "*" then
2267
-
let domain = List.nth parts 1 in
2268
-
let email_parts = String.split_on_char '@' email in
2269
-
if List.length email_parts = 2 then
2270
-
List.nth email_parts 1 = domain
2277
-
match matching_identity with
2278
-
| Some identity -> Lwt.return (Ok identity)
2279
-
| None -> Lwt.return (Error (Parse_error "No matching identity found"))
2281
-
| Error e -> Lwt.return (Error e)
2283
-
(** Submit an email for delivery
2284
-
@param conn The JMAP connection
2285
-
@param account_id The account ID
2286
-
@param identity_id The identity ID to send from
2287
-
@param email_id The email ID to submit
2288
-
@param envelope Optional custom envelope
2289
-
@return The submission ID if successful
2293
-
let submit_email conn ~account_id ~identity_id ~email_id ?envelope () =
2296
-
Jmap.Capability.to_string Jmap.Capability.Core;
2297
-
Capability.to_string Capability.Mail;
2298
-
Capability.to_string Capability.Submission
2302
-
name = "EmailSubmission/set";
2304
-
("accountId", `String account_id);
2306
-
("submission1", `O (
2308
-
("emailId", `String email_id);
2309
-
("identityId", `String identity_id);
2310
-
] @ (match envelope with
2314
-
("email", `String env.Types.mail_from.email);
2315
-
("parameters", match env.Types.mail_from.parameters with
2316
-
| Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
2320
-
("rcptTo", `A (List.map (fun (rcpt:Types.submission_address) ->
2322
-
("email", `String rcpt.Types.email);
2323
-
("parameters", match rcpt.Types.parameters with
2324
-
| Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
2328
-
) env.Types.rcpt_to))
2335
-
("onSuccessUpdateEmail", `O [
2338
-
("$draft", `Bool false);
2339
-
("$sent", `Bool true);
2344
-
method_call_id = "m1";
2347
-
created_ids = None;
2350
-
let* response_result = make_request conn.config request in
2351
-
match response_result with
2355
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2356
-
inv.name = "EmailSubmission/set") response.method_responses in
2357
-
let args = method_response.arguments in
2358
-
match Ezjsonm.find_opt args ["created"] with
2359
-
| Some (`O created) ->
2360
-
let submission_created = List.find_opt (fun (id, _) -> id = "submission1") created in
2361
-
(match submission_created with
2362
-
| Some (_, json) ->
2363
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
2365
-
| None -> Error (Parse_error "Created submission not found in response"))
2367
-
match Ezjsonm.find_opt args ["notCreated"] with
2368
-
| Some (`O errors) ->
2369
-
let error_msg = match List.find_opt (fun (id, _) -> id = "submission1") errors with
2370
-
| Some (_, err) ->
2371
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
2373
-
match Ezjsonm.find_opt err ["description"] with
2374
-
| Some (`String desc) -> desc
2375
-
| _ -> "Unknown error"
2377
-
"Error type: " ^ type_ ^ ", Description: " ^ description
2378
-
| None -> "Unknown error"
2380
-
Error (Parse_error ("Failed to submit email: " ^ error_msg))
2381
-
| _ -> Error (Parse_error "Unexpected response format")
2383
-
| Not_found -> Error (Parse_error "EmailSubmission/set method response not found")
2384
-
| e -> Error (Parse_error (Printexc.to_string e))
2387
-
| Error e -> Lwt.return (Error e)
2389
-
(** Create and submit an email in one operation
2390
-
@param conn The JMAP connection
2391
-
@param account_id The account ID
2392
-
@param from The sender's email address
2393
-
@param to_addresses List of recipient email addresses
2394
-
@param subject The email subject line
2395
-
@param text_body Plain text message body
2396
-
@param html_body Optional HTML message body
2397
-
@return The submission ID if successful
2401
-
let create_and_submit_email conn ~account_id ~from ~to_addresses ~subject ~text_body ?html_body:_ () =
2402
-
(* First get accounts to find the draft mailbox and identity in a single request *)
2403
-
let* initial_result =
2406
-
Jmap.Capability.to_string Jmap.Capability.Core;
2407
-
Capability.to_string Capability.Mail;
2408
-
Capability.to_string Capability.Submission
2412
-
name = "Mailbox/get";
2414
-
("accountId", `String account_id);
2416
-
method_call_id = "m1";
2419
-
name = "Identity/get";
2421
-
("accountId", `String account_id)
2423
-
method_call_id = "m2";
2426
-
created_ids = None;
2428
-
make_request conn.config request
2431
-
match initial_result with
2432
-
| Ok initial_response -> begin
2433
-
(* Find drafts mailbox ID *)
2434
-
let find_drafts_result =
2436
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2437
-
inv.name = "Mailbox/get") initial_response.method_responses in
2438
-
let args = method_response.arguments in
2439
-
match Ezjsonm.find_opt args ["list"] with
2440
-
| Some (`A mailboxes) -> begin
2441
-
let draft_mailbox = List.find_opt (fun mailbox ->
2442
-
match Ezjsonm.find_opt mailbox ["role"] with
2443
-
| Some (`String role) -> role = "drafts"
2447
-
match draft_mailbox with
2448
-
| Some mb -> Ok (Ezjsonm.get_string (Ezjsonm.find mb ["id"]))
2449
-
| None -> Error (Parse_error "No drafts mailbox found")
2451
-
| _ -> Error (Parse_error "Mailbox list not found in response")
2453
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
2454
-
| e -> Error (Parse_error (Printexc.to_string e))
2457
-
(* Find matching identity for from address *)
2458
-
let find_identity_result =
2460
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2461
-
inv.name = "Identity/get") initial_response.method_responses in
2462
-
let args = method_response.arguments in
2463
-
match Ezjsonm.find_opt args ["list"] with
2464
-
| Some (`A identities) -> begin
2465
-
let matching_identity = List.find_opt (fun identity ->
2466
-
match Ezjsonm.find_opt identity ["email"] with
2467
-
| Some (`String email) ->
2468
-
let email_lc = String.lowercase_ascii email in
2469
-
let from_lc = String.lowercase_ascii from in
2470
-
email_lc = from_lc || (* Exact match *)
2471
-
(* Wildcard domain match *)
2472
-
(let parts = String.split_on_char '@' email_lc in
2473
-
if List.length parts = 2 && List.hd parts = "*" then
2474
-
let domain = List.nth parts 1 in
2475
-
let from_parts = String.split_on_char '@' from_lc in
2476
-
if List.length from_parts = 2 then
2477
-
List.nth from_parts 1 = domain
2483
-
match matching_identity with
2485
-
let identity_id = Ezjsonm.get_string (Ezjsonm.find id ["id"]) in
2487
-
| None -> Error (Parse_error ("No matching identity found for " ^ from))
2489
-
| _ -> Error (Parse_error "Identity list not found in response")
2491
-
| Not_found -> Error (Parse_error "Identity/get method response not found")
2492
-
| e -> Error (Parse_error (Printexc.to_string e))
2495
-
(* If we have both required IDs, create and submit the email in one request *)
2496
-
match (find_drafts_result, find_identity_result) with
2497
-
| (Ok drafts_id, Ok identity_id) -> begin
2498
-
(* Now create and submit the email in a single request *)
2501
-
Jmap.Capability.to_string Jmap.Capability.Core;
2502
-
Capability.to_string Capability.Mail;
2503
-
Capability.to_string Capability.Submission
2507
-
name = "Email/set";
2509
-
("accountId", `String account_id);
2513
-
("mailboxIds", `O [(drafts_id, `Bool true)]);
2514
-
("keywords", `O [("$draft", `Bool true)]);
2515
-
("from", `A [`O [("email", `String from)]]);
2516
-
("to", `A (List.map (fun addr ->
2517
-
`O [("email", `String addr)]
2519
-
("subject", `String subject);
2520
-
("textBody", `A [`O [
2521
-
("partId", `String "body");
2522
-
("type", `String "text/plain")
2524
-
("bodyValues", `O [
2526
-
("charset", `String "utf-8");
2527
-
("value", `String text_body)
2534
-
method_call_id = "0";
2537
-
name = "EmailSubmission/set";
2539
-
("accountId", `String account_id);
2542
-
("emailId", `String "#draft");
2543
-
("identityId", `String identity_id)
2547
-
method_call_id = "1";
2550
-
created_ids = None;
2553
-
let* submit_result = make_request conn.config request in
2554
-
match submit_result with
2555
-
| Ok submit_response -> begin
2557
-
let submission_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2558
-
inv.name = "EmailSubmission/set") submit_response.method_responses in
2559
-
let args = submission_method.arguments in
2561
-
(* Check if email was created and submission was created *)
2562
-
match Ezjsonm.find_opt args ["created"] with
2563
-
| Some (`O created) -> begin
2564
-
(* Extract the submission ID *)
2565
-
let submission_created = List.find_opt (fun (id, _) -> id = "sendIt") created in
2566
-
match submission_created with
2567
-
| Some (_, json) ->
2568
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
2569
-
Lwt.return (Ok id)
2571
-
(* Check if there was an error in creation *)
2572
-
match Ezjsonm.find_opt args ["notCreated"] with
2573
-
| Some (`O errors) ->
2574
-
let error_msg = match List.find_opt (fun (id, _) -> id = "sendIt") errors with
2575
-
| Some (_, err) ->
2576
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
2578
-
match Ezjsonm.find_opt err ["description"] with
2579
-
| Some (`String desc) -> desc
2580
-
| _ -> "Unknown error"
2582
-
"Error type: " ^ type_ ^ ", Description: " ^ description
2583
-
| None -> "Unknown error"
2585
-
Lwt.return (Error (Parse_error ("Failed to submit email: " ^ error_msg)))
2586
-
| Some _ -> Lwt.return (Error (Parse_error "Email submission not found in response"))
2587
-
| None -> Lwt.return (Error (Parse_error "Email submission not found in response"))
2590
-
| Some (`Null) -> Lwt.return (Error (Parse_error "No created submissions in response"))
2591
-
| Some _ -> Lwt.return (Error (Parse_error "Invalid response format for created submissions"))
2592
-
| None -> Lwt.return (Error (Parse_error "No created submissions in response"))
2594
-
| Not_found -> Lwt.return (Error (Parse_error "EmailSubmission/set method response not found"))
2595
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e)))
2597
-
| Error e -> Lwt.return (Error e)
2599
-
| (Error e, _) -> Lwt.return (Error e)
2600
-
| (_, Error e) -> Lwt.return (Error e)
2602
-
| Error e -> Lwt.return (Error e)
2604
-
(** Get status of an email submission
2605
-
@param conn The JMAP connection
2606
-
@param account_id The account ID
2607
-
@param submission_id The email submission ID
2608
-
@return The submission status if successful
2612
-
let get_submission_status conn ~account_id ~submission_id =
2615
-
Jmap.Capability.to_string Jmap.Capability.Core;
2616
-
Capability.to_string Capability.Submission
2620
-
name = "EmailSubmission/get";
2622
-
("accountId", `String account_id);
2623
-
("ids", `A [`String submission_id]);
2625
-
method_call_id = "m1";
2628
-
created_ids = None;
2631
-
let* response_result = make_request conn.config request in
2632
-
match response_result with
2636
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
2637
-
inv.name = "EmailSubmission/get") response.method_responses in
2638
-
let args = method_response.arguments in
2639
-
match Ezjsonm.find_opt args ["list"] with
2640
-
| Some (`A [submission]) ->
2641
-
let parse_submission json =
2643
-
let open Ezjsonm in
2644
-
let id = get_string (find json ["id"]) in
2645
-
let identity_id = get_string (find json ["identityId"]) in
2646
-
let email_id = get_string (find json ["emailId"]) in
2647
-
let thread_id = get_string (find json ["threadId"]) in
2650
-
match find_opt json ["envelope"] with
2651
-
| Some (`O env) -> begin
2652
-
let parse_address addr_json =
2653
-
let email = get_string (find addr_json ["email"]) in
2655
-
match find_opt addr_json ["parameters"] with
2656
-
| Some (`O params) ->
2657
-
Some (List.map (fun (k, v) -> (k, get_string v)) params)
2660
-
{ Types.email; parameters }
2663
-
let mail_from = parse_address (find (`O env) ["mailFrom"]) in
2665
-
match find (`O env) ["rcptTo"] with
2666
-
| `A rcpts -> List.map parse_address rcpts
2670
-
Some { Types.mail_from; rcpt_to }
2676
-
match find_opt json ["sendAt"] with
2677
-
| Some (`String date) -> Some date
2682
-
match find_opt json ["undoStatus"] with
2683
-
| Some (`String "pending") -> Some `pending
2684
-
| Some (`String "final") -> Some `final
2685
-
| Some (`String "canceled") -> Some `canceled
2689
-
let parse_delivery_status deliveries =
2690
-
match deliveries with
2692
-
Some (List.map (fun (email, status_json) ->
2693
-
let smtp_reply = get_string (find status_json ["smtpReply"]) in
2695
-
match find_opt status_json ["delivered"] with
2696
-
| Some (`String d) -> Some d
2699
-
(email, { Types.smtp_reply; delivered })
2704
-
let delivery_status =
2705
-
match find_opt json ["deliveryStatus"] with
2706
-
| Some status -> parse_delivery_status status
2710
-
let dsn_blob_ids =
2711
-
match find_opt json ["dsnBlobIds"] with
2712
-
| Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
2716
-
let mdn_blob_ids =
2717
-
match find_opt json ["mdnBlobIds"] with
2718
-
| Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
2735
-
| Not_found -> Error (Parse_error "Required field not found in submission object")
2736
-
| Invalid_argument msg -> Error (Parse_error msg)
2737
-
| e -> Error (Parse_error (Printexc.to_string e))
2740
-
parse_submission submission
2741
-
| Some (`A []) -> Error (Parse_error ("Submission not found: " ^ submission_id))
2742
-
| _ -> Error (Parse_error "Expected single submission in response")
2744
-
| Not_found -> Error (Parse_error "EmailSubmission/get method response not found")
2745
-
| e -> Error (Parse_error (Printexc.to_string e))
2748
-
| Error e -> Lwt.return (Error e)
2750
-
(** {1 Email Address Utilities} *)
2752
-
(** Custom implementation of substring matching *)
2753
-
let contains_substring str sub =
2755
-
let _ = Str.search_forward (Str.regexp_string sub) str 0 in
2757
-
with Not_found -> false
2759
-
(** Checks if a pattern with wildcards matches a string
2760
-
@param pattern Pattern string with * and ? wildcards
2761
-
@param str String to match against
2762
-
Based on simple recursive wildcard matching algorithm
2764
-
let matches_wildcard pattern str =
2765
-
let pattern_len = String.length pattern in
2766
-
let str_len = String.length str in
2768
-
(* Convert both to lowercase for case-insensitive matching *)
2769
-
let pattern = String.lowercase_ascii pattern in
2770
-
let str = String.lowercase_ascii str in
2772
-
(* If there are no wildcards, do a simple substring check *)
2773
-
if not (String.contains pattern '*' || String.contains pattern '?') then
2774
-
contains_substring str pattern
2776
-
(* Classic recursive matching algorithm *)
2777
-
let rec match_from p_pos s_pos =
2778
-
(* Pattern matched to the end *)
2779
-
if p_pos = pattern_len then
2781
-
(* Star matches zero or more chars *)
2782
-
else if pattern.[p_pos] = '*' then
2783
-
match_from (p_pos + 1) s_pos || (* Match empty string *)
2784
-
(s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
2785
-
(* If both have more chars and they match or ? wildcard *)
2786
-
else if s_pos < str_len &&
2787
-
(pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
2788
-
match_from (p_pos + 1) (s_pos + 1)
2795
-
(** Check if an email address matches a filter string
2796
-
@param email The email address to check
2797
-
@param pattern The filter pattern to match against
2798
-
@return True if the email address matches the filter
2800
-
let email_address_matches email pattern =
2801
-
matches_wildcard pattern email
2803
-
(** Check if an email matches a sender filter
2804
-
@param email The email object to check
2805
-
@param pattern The sender filter pattern
2806
-
@return True if any sender address matches the filter
2808
-
let email_matches_sender (email : Types.email) pattern =
2809
-
(* Helper to extract emails from address list *)
2810
-
let addresses_match addrs =
2811
-
List.exists (fun (addr : Types.email_address) ->
2812
-
email_address_matches addr.email pattern
2816
-
(* Check From addresses first *)
2818
-
match email.Types.from with
2819
-
| Some addrs -> addresses_match addrs
2823
-
(* If no match in From, check Sender field *)
2824
-
if from_match then true
2826
-
match email.Types.sender with
2827
-
| Some addrs -> addresses_match addrs