···
-
(** Implementation of the JMAP Mail extension, as defined in RFC8621 *)
-
(** Module for managing JMAP Mail-specific capability URIs *)
-
module Capability = struct
-
(** Mail capability URI *)
-
let mail_uri = "urn:ietf:params:jmap:mail"
-
(** Submission capability URI *)
-
let submission_uri = "urn:ietf:params:jmap:submission"
-
(** Vacation response capability URI *)
-
let vacation_response_uri = "urn:ietf:params:jmap:vacationresponse"
-
(** All mail extension capability types *)
-
| Mail (** Mail capability *)
-
| Submission (** Submission capability *)
-
| VacationResponse (** Vacation response capability *)
-
| Extension of string (** Custom extension *)
-
(** Convert capability to URI string *)
-
let to_string = function
-
| Submission -> submission_uri
-
| VacationResponse -> vacation_response_uri
-
(** Parse a string to a capability *)
-
if s = mail_uri then Mail
-
else if s = submission_uri then Submission
-
else if s = vacation_response_uri then VacationResponse
-
(** Check if a capability is a standard mail capability *)
-
let is_standard = function
-
| Mail | Submission | VacationResponse -> true
-
(** Check if a capability string is a standard mail capability *)
-
let is_standard_string s =
-
s = mail_uri || s = submission_uri || s = vacation_response_uri
-
(** Create a list of capability strings *)
-
let strings_of_capabilities capabilities =
-
List.map to_string capabilities
-
(** {1 Mail capabilities} *)
-
(** Capability URI for JMAP Mail*)
-
let capability_mail = Capability.mail_uri
-
(** Capability URI for JMAP Submission *)
-
let capability_submission = Capability.submission_uri
-
(** Capability URI for JMAP Vacation Response *)
-
let capability_vacation_response = Capability.vacation_response_uri
-
(** {1:mailbox Mailbox objects} *)
-
(** A role for a mailbox. See RFC8621 Section 2. *)
-
| Archive (** Archived mail *)
-
| Drafts (** Draft messages *)
-
| Flagged (** Starred/flagged mail *)
-
| Important (** Important mail *)
-
| Junk (** Spam/Junk mail *)
-
| Sent (** Sent mail *)
-
| Trash (** Deleted/Trash mail *)
-
| Unknown of string (** Server-specific roles *)
-
(** A mailbox (folder) in a mail account. See RFC8621 Section 2. *)
-
role : mailbox_role option;
-
sort_order : unsigned_int;
-
total_emails : unsigned_int;
-
unread_emails : unsigned_int;
-
total_threads : unsigned_int;
-
unread_threads : unsigned_int;
-
my_rights : mailbox_rights;
-
(** Rights for a mailbox. See RFC8621 Section 2. *)
-
may_remove_items : bool;
-
may_set_keywords : bool;
-
may_create_child : bool;
-
(** Filter condition for mailbox queries. See RFC8621 Section 2.3. *)
-
type mailbox_filter_condition = {
-
has_any_role : bool option;
-
is_subscribed : bool option;
-
type mailbox_query_filter = [
-
| `And of mailbox_query_filter list
-
| `Or of mailbox_query_filter list
-
| `Not of mailbox_query_filter
-
| `Condition of mailbox_filter_condition
-
(** Mailbox/get request arguments. See RFC8621 Section 2.1. *)
-
type mailbox_get_arguments = {
-
properties : string list option;
-
(** Mailbox/get response. See RFC8621 Section 2.1. *)
-
type mailbox_get_response = {
-
(** Mailbox/changes request arguments. See RFC8621 Section 2.2. *)
-
type mailbox_changes_arguments = {
-
max_changes : unsigned_int option;
-
(** Mailbox/changes response. See RFC8621 Section 2.2. *)
-
type mailbox_changes_response = {
-
has_more_changes : bool;
-
(** Mailbox/query request arguments. See RFC8621 Section 2.3. *)
-
type mailbox_query_arguments = {
-
filter : mailbox_query_filter option;
-
sort : [ `name | `role | `sort_order ] list option;
-
limit : unsigned_int option;
-
(** Mailbox/query response. See RFC8621 Section 2.3. *)
-
type mailbox_query_response = {
-
can_calculate_changes : bool;
-
position : unsigned_int;
-
total : unsigned_int option;
-
(** Mailbox/queryChanges request arguments. See RFC8621 Section 2.4. *)
-
type mailbox_query_changes_arguments = {
-
filter : mailbox_query_filter option;
-
sort : [ `name | `role | `sort_order ] list option;
-
since_query_state : string;
-
max_changes : unsigned_int option;
-
(** Mailbox/queryChanges response. See RFC8621 Section 2.4. *)
-
type mailbox_query_changes_response = {
-
old_query_state : string;
-
new_query_state : string;
-
total : unsigned_int option;
-
added : mailbox_query_changes_added list;
-
and mailbox_query_changes_added = {
-
(** Mailbox/set request arguments. See RFC8621 Section 2.5. *)
-
type mailbox_set_arguments = {
-
if_in_state : string option;
-
create : (id * mailbox_creation) list option;
-
update : (id * mailbox_update) list option;
-
destroy : id list option;
-
and mailbox_creation = {
-
sort_order : unsigned_int option;
-
is_subscribed : bool option;
-
sort_order : unsigned_int option;
-
is_subscribed : bool option;
-
(** Mailbox/set response. See RFC8621 Section 2.5. *)
-
type mailbox_set_response = {
-
old_state : string option;
-
created : (id * mailbox) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
(** {1:thread Thread objects} *)
-
(** A thread in a mail account. See RFC8621 Section 3. *)
-
(** Thread/get request arguments. See RFC8621 Section 3.1. *)
-
type thread_get_arguments = {
-
properties : string list option;
-
(** Thread/get response. See RFC8621 Section 3.1. *)
-
type thread_get_response = {
-
(** Thread/changes request arguments. See RFC8621 Section 3.2. *)
-
type thread_changes_arguments = {
-
max_changes : unsigned_int option;
-
(** Thread/changes response. See RFC8621 Section 3.2. *)
-
type thread_changes_response = {
-
has_more_changes : bool;
-
(** {1:email Email objects} *)
-
(** Addressing (mailbox) information. See RFC8621 Section 4.1.1. *)
-
parameters : (string * string) list;
-
(** Message header field. See RFC8621 Section 4.1.2. *)
-
(** Email keyword (flag). See RFC8621 Section 4.3. *)
-
(** Email message. See RFC8621 Section 4. *)
-
mailbox_ids : (id * bool) list;
-
keywords : (keyword * bool) list;
-
received_at : utc_date;
-
message_id : string list;
-
in_reply_to : string list option;
-
references : string list option;
-
sender : email_address list option;
-
from : email_address list option;
-
to_ : email_address list option;
-
cc : email_address list option;
-
bcc : email_address list option;
-
reply_to : email_address list option;
-
subject : string option;
-
sent_at : utc_date option;
-
has_attachment : bool option;
-
preview : string option;
-
body_values : (string * string) list option;
-
text_body : email_body_part list option;
-
html_body : email_body_part list option;
-
attachments : email_body_part list option;
-
headers : header list option;
-
(** Email body part. See RFC8621 Section 4.1.4. *)
-
and email_body_part = {
-
part_id : string option;
-
size : unsigned_int option;
-
headers : header list option;
-
charset : string option;
-
disposition : string option;
-
language : string list option;
-
location : string option;
-
sub_parts : email_body_part list option;
-
header_parameter_name : string option;
-
header_parameter_value : string option;
-
(** Email query filter condition. See RFC8621 Section 4.4. *)
-
type email_filter_condition = {
-
in_mailbox : id option;
-
in_mailbox_other_than : id list option;
-
min_size : unsigned_int option;
-
max_size : unsigned_int option;
-
before : utc_date option;
-
after : utc_date option;
-
header : (string * string) option;
-
subject : string option;
-
has_keyword : string option;
-
not_keyword : string option;
-
has_attachment : bool option;
-
type email_query_filter = [
-
| `And of email_query_filter list
-
| `Or of email_query_filter list
-
| `Not of email_query_filter
-
| `Condition of email_filter_condition
-
(** Email/get request arguments. See RFC8621 Section 4.5. *)
-
type email_get_arguments = {
-
properties : string list option;
-
body_properties : string list option;
-
fetch_text_body_values : bool option;
-
fetch_html_body_values : bool option;
-
fetch_all_body_values : bool option;
-
max_body_value_bytes : unsigned_int option;
-
(** Email/get response. See RFC8621 Section 4.5. *)
-
type email_get_response = {
-
(** Email/changes request arguments. See RFC8621 Section 4.6. *)
-
type email_changes_arguments = {
-
max_changes : unsigned_int option;
-
(** Email/changes response. See RFC8621 Section 4.6. *)
-
type email_changes_response = {
-
has_more_changes : bool;
-
(** Email/query request arguments. See RFC8621 Section 4.4. *)
-
type email_query_arguments = {
-
filter : email_query_filter option;
-
sort : comparator list option;
-
collapse_threads : bool option;
-
position : unsigned_int option;
-
anchor_offset : int_t option;
-
limit : unsigned_int option;
-
calculate_total : bool option;
-
(** Email/query response. See RFC8621 Section 4.4. *)
-
type email_query_response = {
-
can_calculate_changes : bool;
-
position : unsigned_int;
-
total : unsigned_int option;
-
thread_ids : id list option;
-
(** Email/queryChanges request arguments. See RFC8621 Section 4.7. *)
-
type email_query_changes_arguments = {
-
filter : email_query_filter option;
-
sort : comparator list option;
-
collapse_threads : bool option;
-
since_query_state : string;
-
max_changes : unsigned_int option;
-
(** Email/queryChanges response. See RFC8621 Section 4.7. *)
-
type email_query_changes_response = {
-
old_query_state : string;
-
new_query_state : string;
-
total : unsigned_int option;
-
added : email_query_changes_added list;
-
and email_query_changes_added = {
-
(** Email/set request arguments. See RFC8621 Section 4.8. *)
-
type email_set_arguments = {
-
if_in_state : string option;
-
create : (id * email_creation) list option;
-
update : (id * email_update) list option;
-
destroy : id list option;
-
mailbox_ids : (id * bool) list;
-
keywords : (keyword * bool) list option;
-
received_at : utc_date option;
-
message_id : string list option;
-
in_reply_to : string list option;
-
references : string list option;
-
sender : email_address list option;
-
from : email_address list option;
-
to_ : email_address list option;
-
cc : email_address list option;
-
bcc : email_address list option;
-
reply_to : email_address list option;
-
subject : string option;
-
body_values : (string * string) list option;
-
text_body : email_body_part list option;
-
html_body : email_body_part list option;
-
attachments : email_body_part list option;
-
headers : header list option;
-
keywords : (keyword * bool) list option;
-
mailbox_ids : (id * bool) list option;
-
(** Email/set response. See RFC8621 Section 4.8. *)
-
type email_set_response = {
-
old_state : string option;
-
created : (id * email) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
(** Email/copy request arguments. See RFC8621 Section 4.9. *)
-
type email_copy_arguments = {
-
create : (id * email_creation) list;
-
on_success_destroy_original : bool option;
-
(** Email/copy response. See RFC8621 Section 4.9. *)
-
type email_copy_response = {
-
created : (id * email) list option;
-
not_created : (id * set_error) list option;
-
(** Email/import request arguments. See RFC8621 Section 4.10. *)
-
type email_import_arguments = {
-
emails : (id * email_import) list;
-
mailbox_ids : (id * bool) list;
-
keywords : (keyword * bool) list option;
-
received_at : utc_date option;
-
(** Email/import response. See RFC8621 Section 4.10. *)
-
type email_import_response = {
-
created : (id * email) list option;
-
not_created : (id * set_error) list option;
-
(** {1:search_snippet Search snippets} *)
-
(** SearchSnippet/get request arguments. See RFC8621 Section 4.11. *)
-
type search_snippet_get_arguments = {
-
filter : email_filter_condition;
-
(** SearchSnippet/get response. See RFC8621 Section 4.11. *)
-
type search_snippet_get_response = {
-
list : (id * search_snippet) list;
-
subject : string option;
-
preview : string option;
-
(** {1:submission EmailSubmission objects} *)
-
(** EmailSubmission address. See RFC8621 Section 5.1. *)
-
type submission_address = {
-
parameters : (string * string) list option;
-
(** Email submission object. See RFC8621 Section 5.1. *)
-
type email_submission = {
-
envelope : envelope option;
-
send_at : utc_date option;
-
delivery_status : (string * submission_status) list option;
-
dsn_blob_ids : (string * id) list option;
-
mdn_blob_ids : (string * id) list option;
-
(** Envelope for mail submission. See RFC8621 Section 5.1. *)
-
mail_from : submission_address;
-
rcpt_to : submission_address list;
-
(** Delivery status for submitted email. See RFC8621 Section 5.1. *)
-
and submission_status = {
-
delivered : string option;
-
(** EmailSubmission/get request arguments. See RFC8621 Section 5.3. *)
-
type email_submission_get_arguments = {
-
properties : string list option;
-
(** EmailSubmission/get response. See RFC8621 Section 5.3. *)
-
type email_submission_get_response = {
-
list : email_submission list;
-
(** EmailSubmission/changes request arguments. See RFC8621 Section 5.4. *)
-
type email_submission_changes_arguments = {
-
max_changes : unsigned_int option;
-
(** EmailSubmission/changes response. See RFC8621 Section 5.4. *)
-
type email_submission_changes_response = {
-
has_more_changes : bool;
-
(** EmailSubmission/query filter condition. See RFC8621 Section 5.5. *)
-
type email_submission_filter_condition = {
-
identity_id : id option;
-
before : utc_date option;
-
after : utc_date option;
-
subject : string option;
-
type email_submission_query_filter = [
-
| `And of email_submission_query_filter list
-
| `Or of email_submission_query_filter list
-
| `Not of email_submission_query_filter
-
| `Condition of email_submission_filter_condition
-
(** EmailSubmission/query request arguments. See RFC8621 Section 5.5. *)
-
type email_submission_query_arguments = {
-
filter : email_submission_query_filter option;
-
sort : comparator list option;
-
position : unsigned_int option;
-
anchor_offset : int_t option;
-
limit : unsigned_int option;
-
calculate_total : bool option;
-
(** EmailSubmission/query response. See RFC8621 Section 5.5. *)
-
type email_submission_query_response = {
-
can_calculate_changes : bool;
-
position : unsigned_int;
-
total : unsigned_int option;
-
(** EmailSubmission/set request arguments. See RFC8621 Section 5.6. *)
-
type email_submission_set_arguments = {
-
if_in_state : string option;
-
create : (id * email_submission_creation) list option;
-
update : (id * email_submission_update) list option;
-
destroy : id list option;
-
on_success_update_email : (id * email_update) list option;
-
and email_submission_creation = {
-
envelope : envelope option;
-
send_at : utc_date option;
-
and email_submission_update = {
-
identity_id : id option;
-
envelope : envelope option;
-
undo_status : [`canceled] option;
-
(** EmailSubmission/set response. See RFC8621 Section 5.6. *)
-
type email_submission_set_response = {
-
old_state : string option;
-
created : (id * email_submission) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
(** {1:identity Identity objects} *)
-
(** Identity for sending mail. See RFC8621 Section 6. *)
-
reply_to : email_address list option;
-
bcc : email_address list option;
-
text_signature : string option;
-
html_signature : string option;
-
(** Identity/get request arguments. See RFC8621 Section 6.1. *)
-
type identity_get_arguments = {
-
properties : string list option;
-
(** Identity/get response. See RFC8621 Section 6.1. *)
-
type identity_get_response = {
-
(** Identity/changes request arguments. See RFC8621 Section 6.2. *)
-
type identity_changes_arguments = {
-
max_changes : unsigned_int option;
-
(** Identity/changes response. See RFC8621 Section 6.2. *)
-
type identity_changes_response = {
-
has_more_changes : bool;
-
(** Identity/set request arguments. See RFC8621 Section 6.3. *)
-
type identity_set_arguments = {
-
if_in_state : string option;
-
create : (id * identity_creation) list option;
-
update : (id * identity_update) list option;
-
destroy : id list option;
-
and identity_creation = {
-
reply_to : email_address list option;
-
bcc : email_address list option;
-
text_signature : string option;
-
html_signature : string option;
-
and identity_update = {
-
reply_to : email_address list option;
-
bcc : email_address list option;
-
text_signature : string option;
-
html_signature : string option;
-
(** Identity/set response. See RFC8621 Section 6.3. *)
-
type identity_set_response = {
-
old_state : string option;
-
created : (id * identity) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
(** {1:vacation_response VacationResponse objects} *)
-
(** Vacation auto-reply setting. See RFC8621 Section 7. *)
-
type vacation_response = {
-
from_date : utc_date option;
-
to_date : utc_date option;
-
subject : string option;
-
text_body : string option;
-
html_body : string option;
-
(** VacationResponse/get request arguments. See RFC8621 Section 7.2. *)
-
type vacation_response_get_arguments = {
-
properties : string list option;
-
(** VacationResponse/get response. See RFC8621 Section 7.2. *)
-
type vacation_response_get_response = {
-
list : vacation_response list;
-
(** VacationResponse/set request arguments. See RFC8621 Section 7.3. *)
-
type vacation_response_set_arguments = {
-
if_in_state : string option;
-
update : (id * vacation_response_update) list;
-
and vacation_response_update = {
-
is_enabled : bool option;
-
from_date : utc_date option;
-
to_date : utc_date option;
-
subject : string option;
-
text_body : string option;
-
html_body : string option;
-
(** VacationResponse/set response. See RFC8621 Section 7.3. *)
-
type vacation_response_set_response = {
-
old_state : string option;
-
updated : id list option;
-
not_updated : (id * set_error) list option;
-
(** {1:message_flags Message Flags and Mailbox Attributes} *)
-
(** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords *)
-
| Red (** Bit pattern 000 *)
-
| Orange (** Bit pattern 100 *)
-
| Yellow (** Bit pattern 010 *)
-
| Green (** Bit pattern 111 *)
-
| Blue (** Bit pattern 001 *)
-
| Purple (** Bit pattern 101 *)
-
| Gray (** Bit pattern 011 *)
-
(** Standard message keywords as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
-
| Notify (** Indicate a notification should be shown for this message *)
-
| Muted (** User is not interested in future replies to this thread *)
-
| Followed (** User is particularly interested in future replies to this thread *)
-
| Memo (** Message is a note-to-self about another message in the same thread *)
-
| HasMemo (** Message has an associated memo with the $memo keyword *)
-
| HasAttachment (** Message has an attachment *)
-
| HasNoAttachment (** Message does not have an attachment *)
-
| AutoSent (** Message was sent automatically as a response due to a user rule *)
-
| Unsubscribed (** User has unsubscribed from the thread this message is in *)
-
| CanUnsubscribe (** Message has an RFC8058-compliant List-Unsubscribe header *)
-
| Imported (** Message was imported from another mailbox *)
-
| IsTrusted (** Server has verified authenticity of the from name and email *)
-
| MaskedEmail (** Message was received via an alias created for an individual sender *)
-
| New (** Message should be made more prominent due to a recent action *)
-
| MailFlagBit0 (** Bit 0 of the 3-bit flag color pattern *)
-
| MailFlagBit1 (** Bit 1 of the 3-bit flag color pattern *)
-
| MailFlagBit2 (** Bit 2 of the 3-bit flag color pattern *)
-
| OtherKeyword of string (** Other non-standard keywords *)
-
(** Special mailbox attribute names as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
-
type mailbox_attribute =
-
| Snoozed (** Mailbox containing messages that have been snoozed *)
-
| Scheduled (** Mailbox containing messages scheduled to be sent later *)
-
| Memos (** Mailbox containing messages with the $memo keyword *)
-
| OtherAttribute of string (** Other non-standard mailbox attributes *)
-
(** Functions for working with flag colors based on the specification in
-
draft-ietf-mailmaint-messageflag-mailboxattribute-02, section 3.1. *)
-
(** Convert bit pattern to flag color *)
-
let flag_color_of_bits bit0 bit1 bit2 =
-
match (bit0, bit1, bit2) with
-
| (false, false, false) -> Red (* 000 *)
-
| (true, false, false) -> Orange (* 100 *)
-
| (false, true, false) -> Yellow (* 010 *)
-
| (true, true, true) -> Green (* 111 *)
-
| (false, false, true) -> Blue (* 001 *)
-
| (true, false, true) -> Purple (* 101 *)
-
| (false, true, true) -> Gray (* 011 *)
-
| (true, true, false) -> Green (* 110 - not in spec, defaulting to green *)
-
(** Get bits for a flag color *)
-
let bits_of_flag_color = function
-
| Red -> (false, false, false)
-
| Orange -> (true, false, false)
-
| Yellow -> (false, true, false)
-
| Green -> (true, true, true)
-
| Blue -> (false, false, true)
-
| Purple -> (true, false, true)
-
| Gray -> (false, true, true)
-
(** Check if a keyword list contains a flag color *)
-
let has_flag_color keywords =
-
let has_bit0 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit0" -> true
-
let has_bit1 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit1" -> true
-
let has_bit2 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit2" -> true
-
has_bit0 || has_bit1 || has_bit2
-
(** Extract flag color from keywords if present *)
-
let get_flag_color keywords =
-
(* First check if the message has the \Flagged system flag *)
-
let is_flagged = List.exists (function
-
| (Flagged, true) -> true
-
(* Get values of each bit flag *)
-
let bit0 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit0" -> true
-
let bit1 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit1" -> true
-
let bit2 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit2" -> true
-
Some (flag_color_of_bits bit0 bit1 bit2)
-
(** Convert a message keyword to its string representation *)
-
let string_of_message_keyword = function
-
| Followed -> "$followed"
-
| HasMemo -> "$hasmemo"
-
| HasAttachment -> "$hasattachment"
-
| HasNoAttachment -> "$hasnoattachment"
-
| AutoSent -> "$autosent"
-
| Unsubscribed -> "$unsubscribed"
-
| CanUnsubscribe -> "$canunsubscribe"
-
| Imported -> "$imported"
-
| IsTrusted -> "$istrusted"
-
| MaskedEmail -> "$maskedemail"
-
| MailFlagBit0 -> "$MailFlagBit0"
-
| MailFlagBit1 -> "$MailFlagBit1"
-
| MailFlagBit2 -> "$MailFlagBit2"
-
(** Parse a string into a message keyword *)
-
let message_keyword_of_string = function
-
| "$followed" -> Followed
-
| "$hasmemo" -> HasMemo
-
| "$hasattachment" -> HasAttachment
-
| "$hasnoattachment" -> HasNoAttachment
-
| "$autosent" -> AutoSent
-
| "$unsubscribed" -> Unsubscribed
-
| "$canunsubscribe" -> CanUnsubscribe
-
| "$imported" -> Imported
-
| "$istrusted" -> IsTrusted
-
| "$maskedemail" -> MaskedEmail
-
| "$MailFlagBit0" -> MailFlagBit0
-
| "$MailFlagBit1" -> MailFlagBit1
-
| "$MailFlagBit2" -> MailFlagBit2
-
(** Convert a mailbox attribute to its string representation *)
-
let string_of_mailbox_attribute = function
-
| Scheduled -> "Scheduled"
-
| OtherAttribute s -> s
-
(** Parse a string into a mailbox attribute *)
-
let mailbox_attribute_of_string = function
-
| "Scheduled" -> Scheduled
-
| s -> OtherAttribute s
-
(** Get a human-readable representation of a flag color *)
-
let human_readable_flag_color = function
-
(** Get a human-readable representation of a message keyword *)
-
let human_readable_message_keyword = function
-
| Followed -> "Followed"
-
| HasMemo -> "Has Memo"
-
| HasAttachment -> "Has Attachment"
-
| HasNoAttachment -> "No Attachment"
-
| AutoSent -> "Auto Sent"
-
| Unsubscribed -> "Unsubscribed"
-
| CanUnsubscribe -> "Can Unsubscribe"
-
| Imported -> "Imported"
-
| IsTrusted -> "Trusted"
-
| MaskedEmail -> "Masked Email"
-
| MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> "Flag Bit"
-
(** Format email keywords into a human-readable string representation *)
-
let format_email_keywords keywords =
-
(* Get flag color if present *)
-
match get_flag_color keywords with
-
| Some color -> human_readable_flag_color color
-
(* Get standard JMAP keywords *)
-
let standard_keywords = List.filter_map (fun (kw, active) ->
-
if not active then None
-
| Flagged -> Some "Flagged"
-
| Answered -> Some "Answered"
-
| Draft -> Some "Draft"
-
| Forwarded -> Some "Forwarded"
-
| Phishing -> Some "Phishing"
-
| NotJunk -> Some "Not Junk"
-
| Unread -> Some "Unread"
-
(* Get message keywords *)
-
let message_keywords = List.filter_map (fun (kw, active) ->
-
if not active then None
-
(* Try to parse as message keyword *)
-
let message_kw = message_keyword_of_string s in
-
| OtherKeyword _ -> None
-
| MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> None
-
| kw -> Some (human_readable_message_keyword kw))
-
(* Combine all human-readable labels *)
-
(if color_str <> "" then [color_str] else []) @
-
String.concat ", " all_parts
-
(** {1 JSON serialization} *)
-
(** {2 Helper functions for serialization} *)
-
let string_of_mailbox_role = function
-
| Important -> "important"
-
let mailbox_role_of_string = function
-
| "important" -> Important
-
let string_of_keyword = function
-
| Flagged -> "$flagged"
-
| Answered -> "$answered"
-
| Forwarded -> "$forwarded"
-
| Phishing -> "$phishing"
-
| NotJunk -> "$notjunk"
-
let keyword_of_string = function
-
| "$flagged" -> Flagged
-
| "$answered" -> Answered
-
| "$forwarded" -> Forwarded
-
| "$phishing" -> Phishing
-
| "$notjunk" -> NotJunk
-
(** {2 Mailbox serialization} *)
-
(** TODO:claude - Need to implement all JSON serialization functions
-
for each type we've defined. This would be a substantial amount of
-
code and likely require additional understanding of the ezjsonm API.
-
For a full implementation, we would need functions to convert between
-
OCaml types and JSON for each of:
-
- mailbox, mailbox_rights, mailbox query/update operations
-
- email, email_address, header, email_body_part
-
- email query/update operations
-
- submission operations
-
- vacation response operations
-
(** {1 API functions} *)
-
(** Authentication credentials for a JMAP server *)
-
(** Connection to a JMAP mail server *)
-
session: Jmap.Types.session;
-
config: Jmap.Api.config;
-
(** Convert JSON mail object to OCaml type *)
-
let mailbox_of_json json =
-
let id = get_string (find json ["id"]) in
-
let name = get_string (find json ["name"]) in
-
(* Handle parentId which can be null *)
-
match find_opt json ["parentId"] with
-
| Some (`String s) -> Some s
-
(* Handle role which might be null *)
-
match find_opt json ["role"] with
-
| Some (`String s) -> Some (Json.mailbox_role_of_string s)
-
let sort_order = get_int (find json ["sortOrder"]) in
-
let total_emails = get_int (find json ["totalEmails"]) in
-
let unread_emails = get_int (find json ["unreadEmails"]) in
-
let total_threads = get_int (find json ["totalThreads"]) in
-
let unread_threads = get_int (find json ["unreadThreads"]) in
-
let is_subscribed = get_bool (find json ["isSubscribed"]) in
-
let rights_json = find json ["myRights"] in
-
Types.may_read_items = get_bool (find rights_json ["mayReadItems"]);
-
may_add_items = get_bool (find rights_json ["mayAddItems"]);
-
may_remove_items = get_bool (find rights_json ["mayRemoveItems"]);
-
may_set_seen = get_bool (find rights_json ["maySetSeen"]);
-
may_set_keywords = get_bool (find rights_json ["maySetKeywords"]);
-
may_create_child = get_bool (find rights_json ["mayCreateChild"]);
-
may_rename = get_bool (find rights_json ["mayRename"]);
-
may_delete = get_bool (find rights_json ["mayDelete"]);
-
may_submit = get_bool (find rights_json ["maySubmit"]);
-
Error (Parse_error "Required field not found in mailbox object")
-
| Invalid_argument msg ->
-
Error (Parse_error msg)
-
Error (Parse_error (Printexc.to_string e))
-
(** Convert JSON email object to OCaml type *)
-
let email_of_json json =
-
let id = get_string (find json ["id"]) in
-
let blob_id = get_string (find json ["blobId"]) in
-
let thread_id = get_string (find json ["threadId"]) in
-
(* Process mailboxIds map *)
-
let mailbox_ids_json = find json ["mailboxIds"] in
-
let mailbox_ids = match mailbox_ids_json with
-
| `O items -> List.map (fun (id, v) -> (id, get_bool v)) items
-
| _ -> raise (Invalid_argument "mailboxIds is not an object")
-
(* Process keywords map *)
-
let keywords_json = find json ["keywords"] in
-
let keywords = match keywords_json with
-
| `O items -> List.map (fun (k, v) ->
-
(Json.keyword_of_string k, get_bool v)) items
-
| _ -> raise (Invalid_argument "keywords is not an object")
-
let size = get_int (find json ["size"]) in
-
let received_at = get_string (find json ["receivedAt"]) in
-
(* Handle messageId which might be an array or missing *)
-
match find_opt json ["messageId"] with
-
| Some (`A ids) -> List.map (fun id ->
-
| _ -> raise (Invalid_argument "messageId item is not a string")
-
| Some (`String s) -> [s] (* Handle single string case *)
-
| None -> [] (* Handle missing case *)
-
| _ -> raise (Invalid_argument "messageId has unexpected type")
-
(* Parse optional fields *)
-
let parse_email_addresses opt_json =
-
Some (List.map (fun addr_json ->
-
match find_opt addr_json ["name"] with
-
| Some (`String s) -> Some s
-
let email = get_string (find addr_json ["email"]) in
-
match find_opt addr_json ["parameters"] with
-
| Some (`O items) -> List.map (fun (k, v) ->
-
{ Types.name; email; parameters }
-
(* Handle optional string arrays with null handling *)
-
let parse_string_array_opt field_name =
-
match find_opt json [field_name] with
-
Some (List.filter_map (function
-
let in_reply_to = parse_string_array_opt "inReplyTo" in
-
let references = parse_string_array_opt "references" in
-
let sender = parse_email_addresses (find_opt json ["sender"]) in
-
let from = parse_email_addresses (find_opt json ["from"]) in
-
let to_ = parse_email_addresses (find_opt json ["to"]) in
-
let cc = parse_email_addresses (find_opt json ["cc"]) in
-
let bcc = parse_email_addresses (find_opt json ["bcc"]) in
-
let reply_to = parse_email_addresses (find_opt json ["replyTo"]) in
-
(* Handle optional string fields with null handling *)
-
let parse_string_opt field_name =
-
match find_opt json [field_name] with
-
| Some (`String s) -> Some s
-
let subject = parse_string_opt "subject" in
-
let sent_at = parse_string_opt "sentAt" in
-
(* Handle optional boolean fields with null handling *)
-
let parse_bool_opt field_name =
-
match find_opt json [field_name] with
-
| Some (`Bool b) -> Some b
-
let has_attachment = parse_bool_opt "hasAttachment" in
-
let preview = parse_string_opt "preview" in
-
(* TODO Body parts parsing would go here - omitting for brevity *)
-
Error (Parse_error "Required field not found in email object")
-
| Invalid_argument msg ->
-
Error (Parse_error msg)
-
Error (Parse_error (Printexc.to_string e))
-
(** Login to a JMAP server and establish a connection
-
@param uri The URI of the JMAP server
-
@param credentials Authentication credentials
-
@return A connection object if successful
-
let login ~uri ~credentials =
-
let* session_result = get_session (Uri.of_string uri)
-
~username:credentials.username
-
~authentication_token:credentials.password
-
match session_result with
-
let api_uri = Uri.of_string session.api_url in
-
username = credentials.username;
-
authentication_token = credentials.password;
-
Lwt.return (Ok { session; config })
-
| Error e -> Lwt.return (Error e)
-
(** Login to a JMAP server using an API token
-
@param uri The URI of the JMAP server
-
@param api_token The API token for authentication
-
@return A connection object if successful
-
let login_with_token ~uri ~api_token =
-
let* session_result = get_session (Uri.of_string uri)
-
match session_result with
-
let api_uri = Uri.of_string session.api_url in
-
username = ""; (* Empty username indicates we're using token auth *)
-
authentication_token = api_token;
-
Lwt.return (Ok { session; config })
-
| Error e -> Lwt.return (Error e)
-
(** Get all mailboxes for an account
-
@param conn The JMAP connection
-
@param account_id The account ID to get mailboxes for
-
@return A list of mailboxes if successful
-
let get_mailboxes conn ~account_id =
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Mailbox/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A mailbox_list) ->
-
let parse_results = List.map mailbox_of_json mailbox_list in
-
let (successes, failures) = List.partition Result.is_ok parse_results in
-
if List.length failures > 0 then
-
Error (Parse_error "Failed to parse some mailboxes")
-
Ok (List.map Result.get_ok successes)
-
| _ -> Error (Parse_error "Mailbox list not found in response")
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Get a specific mailbox by ID
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to retrieve
-
@return The mailbox if found
-
let get_mailbox conn ~account_id ~mailbox_id =
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("ids", `A [`String mailbox_id]);
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Mailbox/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A [mailbox]) -> mailbox_of_json mailbox
-
| Some (`A []) -> Error (Parse_error ("Mailbox not found: " ^ mailbox_id))
-
| _ -> Error (Parse_error "Expected single mailbox in response")
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Get messages in a mailbox
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to get messages from
-
@param limit Optional limit on number of messages to return
-
@return The list of email messages if successful
-
let get_messages_in_mailbox conn ~account_id ~mailbox_id ?limit () =
-
(* First query the emails in the mailbox *)
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("filter", `O [("inMailbox", `String mailbox_id)]);
-
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
-
| Some l -> [("limit", `Float (float_of_int l))]
-
let* query_result = make_request conn.config query_request in
-
match query_result with
-
let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/query") query_response.method_responses in
-
let args = query_method.arguments in
-
match Ezjsonm.find_opt args ["ids"] with
-
let email_ids = List.map (function
-
| _ -> raise (Invalid_argument "Email ID is not a string")
-
(* If we have IDs, fetch the actual email objects *)
-
if List.length email_ids > 0 then
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("ids", `A (List.map (fun id -> `String id) email_ids));
-
let* get_result = make_request conn.config get_request in
-
let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/get") get_response.method_responses in
-
let args = get_method.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A email_list) ->
-
let parse_results = List.map email_of_json email_list in
-
let (successes, failures) = List.partition Result.is_ok parse_results in
-
if List.length failures > 0 then
-
Lwt.return (Error (Parse_error "Failed to parse some emails"))
-
Lwt.return (Ok (List.map Result.get_ok successes))
-
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
-
| Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
(* No emails in mailbox *)
-
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
-
| Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
-
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
(** Get a single email message by ID
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID to retrieve
-
@return The email message if found
-
let get_email conn ~account_id ~email_id =
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("ids", `A [`String email_id]);
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A [email]) -> email_of_json email
-
| Some (`A []) -> Error (Parse_error ("Email not found: " ^ email_id))
-
| _ -> Error (Parse_error "Expected single email in response")
-
| Not_found -> Error (Parse_error "Email/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Helper functions for working with message flags and mailbox attributes *)
-
(** Check if an email has a specific message keyword
-
@param email The email to check
-
@param keyword The message keyword to look for
-
@return true if the email has the keyword, false otherwise
-
let has_message_keyword (email:Types.email) keyword =
-
let keyword_string = string_of_message_keyword keyword in
-
| (Custom s, true) when s = keyword_string -> true
-
(** Add a message keyword to an email
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID
-
@param keyword The message keyword to add
-
@return Success or error
-
let add_message_keyword conn ~account_id ~email_id ~keyword =
-
let keyword_string = Types.string_of_message_keyword keyword in
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
(keyword_string, `Bool true)
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["updated"] with
-
| Some (`A _ids) -> Ok ()
-
match Ezjsonm.find_opt args ["notUpdated"] with
-
Error (Parse_error ("Failed to update email: " ^ email_id))
-
| _ -> Error (Parse_error "Unexpected response format")
-
| Not_found -> Error (Parse_error "Email/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Set a flag color for an email
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID
-
@param color The flag color to set
-
@return Success or error
-
let set_flag_color conn ~account_id ~email_id ~color =
-
(* Get the bit pattern for the color *)
-
let (bit0, bit1, bit2) = Types.bits_of_flag_color color in
-
(* Build the keywords update object *)
-
("$flagged", `Bool true);
-
("$MailFlagBit0", `Bool bit0);
-
("$MailFlagBit1", `Bool bit1);
-
("$MailFlagBit2", `Bool bit2);
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("keywords", `O keywords)
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["updated"] with
-
| Some (`A _ids) -> Ok ()
-
match Ezjsonm.find_opt args ["notUpdated"] with
-
Error (Parse_error ("Failed to update email: " ^ email_id))
-
| _ -> Error (Parse_error "Unexpected response format")
-
| Not_found -> Error (Parse_error "Email/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Convert an email's keywords to typed message_keyword list
-
@param email The email to analyze
-
@return List of message keywords
-
let get_message_keywords (email:Types.email) =
-
List.filter_map (function
-
| (Custom s, true) -> Some (message_keyword_of_string s)
-
(** Get emails with a specific message keyword
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param keyword The message keyword to search for
-
@param limit Optional limit on number of emails to return
-
@return List of emails with the keyword if successful
-
let get_emails_with_keyword conn ~account_id ~keyword ?limit () =
-
let keyword_string = Types.string_of_message_keyword keyword in
-
(* Query for emails with the specified keyword *)
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("filter", `O [("hasKeyword", `String keyword_string)]);
-
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
-
| Some l -> [("limit", `Float (float_of_int l))]
-
let* query_result = make_request conn.config query_request in
-
match query_result with
-
let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/query") query_response.method_responses in
-
let args = query_method.arguments in
-
match Ezjsonm.find_opt args ["ids"] with
-
let email_ids = List.map (function
-
| _ -> raise (Invalid_argument "Email ID is not a string")
-
(* If we have IDs, fetch the actual email objects *)
-
if List.length email_ids > 0 then
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("ids", `A (List.map (fun id -> `String id) email_ids));
-
let* get_result = make_request conn.config get_request in
-
let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/get") get_response.method_responses in
-
let args = get_method.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A email_list) ->
-
let parse_results = List.map email_of_json email_list in
-
let (successes, failures) = List.partition Result.is_ok parse_results in
-
if List.length failures > 0 then
-
Lwt.return (Error (Parse_error "Failed to parse some emails"))
-
Lwt.return (Ok (List.map Result.get_ok successes))
-
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
-
| Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
(* No emails with the keyword *)
-
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
-
| Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
-
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
(** {1 Email Submission} *)
-
(** Create a new email draft
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to store the draft in (usually "drafts")
-
@param from The sender's email address
-
@param to_addresses List of recipient email addresses
-
@param subject The email subject line
-
@param text_body Plain text message body
-
@param html_body Optional HTML message body
-
@return The created email ID if successful
-
let create_email_draft conn ~account_id ~mailbox_id ~from ~to_addresses ~subject ~text_body ?html_body () =
-
(* Create email addresses *)
-
let to_addrs = List.map (fun addr -> {
-
(* Create text body part *)
-
Types.part_id = Some "part1";
-
type_ = Some "text/plain";
-
charset = Some "utf-8";
-
header_parameter_name = None;
-
header_parameter_value = None;
-
(* Create HTML body part if provided *)
-
let html_part_opt = match html_body with
-
Types.part_id = Some "part2";
-
type_ = Some "text/html";
-
charset = Some "utf-8";
-
header_parameter_name = None;
-
header_parameter_value = None;
-
(* Create body values *)
-
] @ (match html_body with
-
| Some html -> [("part2", html)]
-
let html_body_list = match html_part_opt with
-
| Some part -> Some [part]
-
let _email_creation = {
-
Types.mailbox_ids = [(mailbox_id, true)];
-
keywords = Some [(Draft, true)];
-
received_at = None; (* Server will set this *)
-
message_id = None; (* Server will generate this *)
-
from = Some [from_addr];
-
subject = Some subject;
-
body_values = Some body_values;
-
text_body = Some [text_part];
-
html_body = html_body_list;
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
("accountId", `String account_id);
-
("mailboxIds", `O [(mailbox_id, `Bool true)]);
-
("keywords", `O [("$draft", `Bool true)]);
-
("from", `A [`O [("name", `Null); ("email", `String from)]]);
-
("to", `A (List.map (fun addr ->
-
`O [("name", `Null); ("email", `String addr)]
-
("subject", `String subject);
-
("type", `String "multipart/alternative");
-
("partId", `String "part1");
-
("type", `String "text/plain")
-
("partId", `String "part2");
-
("type", `String "text/html")
-
("part1", `O [("value", `String text_body)])
-
] @ (match html_body with
-
| Some html -> [("part2", `O [("value", `String html)])]
-
| None -> [("part2", `O [("value", `String ("<html><body>" ^ text_body ^ "</body></html>"))])]
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["created"] with
-
let draft_created = List.find_opt (fun (id, _) -> id = "draft1") created in
-
(match draft_created with
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
-
| None -> Error (Parse_error "Created email not found in response"))
-
match Ezjsonm.find_opt args ["notCreated"] with
-
let error_msg = match List.find_opt (fun (id, _) -> id = "draft1") errors with
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
-
match Ezjsonm.find_opt err ["description"] with
-
| Some (`String desc) -> desc
-
"Error type: " ^ type_ ^ ", Description: " ^ description
-
| None -> "Unknown error"
-
Error (Parse_error ("Failed to create email: " ^ error_msg))
-
| _ -> Error (Parse_error "Unexpected response format")
-
| Not_found -> Error (Parse_error "Email/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Get all identities for an account
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@return A list of identities if successful
-
let get_identities conn ~account_id =
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Submission
-
("accountId", `String account_id);
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Identity/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A identities) ->
-
let parse_identity json =
-
let id = get_string (find json ["id"]) in
-
let name = get_string (find json ["name"]) in
-
let email = get_string (find json ["email"]) in
-
let parse_email_addresses field =
-
match find_opt json [field] with
-
Some (List.map (fun addr_json ->
-
match find_opt addr_json ["name"] with
-
| Some (`String s) -> Some s
-
let email = get_string (find addr_json ["email"]) in
-
match find_opt addr_json ["parameters"] with
-
| Some (`O items) -> List.map (fun (k, v) ->
-
{ Types.name; email; parameters }
-
let reply_to = parse_email_addresses "replyTo" in
-
let bcc = parse_email_addresses "bcc" in
-
match find_opt json ["textSignature"] with
-
| Some (`String s) -> Some s
-
match find_opt json ["htmlSignature"] with
-
| Some (`String s) -> Some s
-
match find_opt json ["mayDelete"] with
-
(* Create our own identity record for simplicity *)
-
let r : Types.identity = {
-
text_signature = text_signature;
-
html_signature = html_signature;
-
may_delete = may_delete
-
| Not_found -> Error (Parse_error "Required field not found in identity object")
-
| Invalid_argument msg -> Error (Parse_error msg)
-
| e -> Error (Parse_error (Printexc.to_string e))
-
let results = List.map parse_identity identities in
-
let (successes, failures) = List.partition Result.is_ok results in
-
if List.length failures > 0 then
-
Error (Parse_error "Failed to parse some identity objects")
-
Ok (List.map Result.get_ok successes)
-
| _ -> Error (Parse_error "Identity list not found in response")
-
| Not_found -> Error (Parse_error "Identity/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Find a suitable identity by email address
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email The email address to match
-
@return The identity if found, otherwise Error
-
let find_identity_by_email conn ~account_id ~email =
-
let* identities_result = get_identities conn ~account_id in
-
match identities_result with
-
| Ok identities -> begin
-
let matching_identity = List.find_opt (fun (identity:Types.identity) ->
-
if String.lowercase_ascii identity.email = String.lowercase_ascii email then
-
(* Wildcard match (e.g., *@example.com) *)
-
let parts = String.split_on_char '@' identity.email in
-
if List.length parts = 2 && List.hd parts = "*" then
-
let domain = List.nth parts 1 in
-
let email_parts = String.split_on_char '@' email in
-
if List.length email_parts = 2 then
-
List.nth email_parts 1 = domain
-
match matching_identity with
-
| Some identity -> Lwt.return (Ok identity)
-
| None -> Lwt.return (Error (Parse_error "No matching identity found"))
-
| Error e -> Lwt.return (Error e)
-
(** Submit an email for delivery
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param identity_id The identity ID to send from
-
@param email_id The email ID to submit
-
@param envelope Optional custom envelope
-
@return The submission ID if successful
-
let submit_email conn ~account_id ~identity_id ~email_id ?envelope () =
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail;
-
Capability.to_string Capability.Submission
-
name = "EmailSubmission/set";
-
("accountId", `String account_id);
-
("emailId", `String email_id);
-
("identityId", `String identity_id);
-
] @ (match envelope with
-
("email", `String env.Types.mail_from.email);
-
("parameters", match env.Types.mail_from.parameters with
-
| Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
-
("rcptTo", `A (List.map (fun (rcpt:Types.submission_address) ->
-
("email", `String rcpt.Types.email);
-
("parameters", match rcpt.Types.parameters with
-
| Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
-
("onSuccessUpdateEmail", `O [
-
("$draft", `Bool false);
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "EmailSubmission/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["created"] with
-
let submission_created = List.find_opt (fun (id, _) -> id = "submission1") created in
-
(match submission_created with
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
-
| None -> Error (Parse_error "Created submission not found in response"))
-
match Ezjsonm.find_opt args ["notCreated"] with
-
let error_msg = match List.find_opt (fun (id, _) -> id = "submission1") errors with
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
-
match Ezjsonm.find_opt err ["description"] with
-
| Some (`String desc) -> desc
-
"Error type: " ^ type_ ^ ", Description: " ^ description
-
| None -> "Unknown error"
-
Error (Parse_error ("Failed to submit email: " ^ error_msg))
-
| _ -> Error (Parse_error "Unexpected response format")
-
| Not_found -> Error (Parse_error "EmailSubmission/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** Create and submit an email in one operation
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param from The sender's email address
-
@param to_addresses List of recipient email addresses
-
@param subject The email subject line
-
@param text_body Plain text message body
-
@param html_body Optional HTML message body
-
@return The submission ID if successful
-
let create_and_submit_email conn ~account_id ~from ~to_addresses ~subject ~text_body ?html_body:_ () =
-
(* First get accounts to find the draft mailbox and identity in a single request *)
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail;
-
Capability.to_string Capability.Submission
-
("accountId", `String account_id);
-
("accountId", `String account_id)
-
make_request conn.config request
-
match initial_result with
-
| Ok initial_response -> begin
-
(* Find drafts mailbox ID *)
-
let find_drafts_result =
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Mailbox/get") initial_response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A mailboxes) -> begin
-
let draft_mailbox = List.find_opt (fun mailbox ->
-
match Ezjsonm.find_opt mailbox ["role"] with
-
| Some (`String role) -> role = "drafts"
-
match draft_mailbox with
-
| Some mb -> Ok (Ezjsonm.get_string (Ezjsonm.find mb ["id"]))
-
| None -> Error (Parse_error "No drafts mailbox found")
-
| _ -> Error (Parse_error "Mailbox list not found in response")
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
(* Find matching identity for from address *)
-
let find_identity_result =
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Identity/get") initial_response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A identities) -> begin
-
let matching_identity = List.find_opt (fun identity ->
-
match Ezjsonm.find_opt identity ["email"] with
-
| Some (`String email) ->
-
let email_lc = String.lowercase_ascii email in
-
let from_lc = String.lowercase_ascii from in
-
email_lc = from_lc || (* Exact match *)
-
(* Wildcard domain match *)
-
(let parts = String.split_on_char '@' email_lc in
-
if List.length parts = 2 && List.hd parts = "*" then
-
let domain = List.nth parts 1 in
-
let from_parts = String.split_on_char '@' from_lc in
-
if List.length from_parts = 2 then
-
List.nth from_parts 1 = domain
-
match matching_identity with
-
let identity_id = Ezjsonm.get_string (Ezjsonm.find id ["id"]) in
-
| None -> Error (Parse_error ("No matching identity found for " ^ from))
-
| _ -> Error (Parse_error "Identity list not found in response")
-
| Not_found -> Error (Parse_error "Identity/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
(* If we have both required IDs, create and submit the email in one request *)
-
match (find_drafts_result, find_identity_result) with
-
| (Ok drafts_id, Ok identity_id) -> begin
-
(* Now create and submit the email in a single request *)
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail;
-
Capability.to_string Capability.Submission
-
("accountId", `String account_id);
-
("mailboxIds", `O [(drafts_id, `Bool true)]);
-
("keywords", `O [("$draft", `Bool true)]);
-
("from", `A [`O [("email", `String from)]]);
-
("to", `A (List.map (fun addr ->
-
`O [("email", `String addr)]
-
("subject", `String subject);
-
("partId", `String "body");
-
("type", `String "text/plain")
-
("charset", `String "utf-8");
-
("value", `String text_body)
-
name = "EmailSubmission/set";
-
("accountId", `String account_id);
-
("emailId", `String "#draft");
-
("identityId", `String identity_id)
-
let* submit_result = make_request conn.config request in
-
match submit_result with
-
| Ok submit_response -> begin
-
let submission_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "EmailSubmission/set") submit_response.method_responses in
-
let args = submission_method.arguments in
-
(* Check if email was created and submission was created *)
-
match Ezjsonm.find_opt args ["created"] with
-
| Some (`O created) -> begin
-
(* Extract the submission ID *)
-
let submission_created = List.find_opt (fun (id, _) -> id = "sendIt") created in
-
match submission_created with
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
-
(* Check if there was an error in creation *)
-
match Ezjsonm.find_opt args ["notCreated"] with
-
let error_msg = match List.find_opt (fun (id, _) -> id = "sendIt") errors with
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
-
match Ezjsonm.find_opt err ["description"] with
-
| Some (`String desc) -> desc
-
"Error type: " ^ type_ ^ ", Description: " ^ description
-
| None -> "Unknown error"
-
Lwt.return (Error (Parse_error ("Failed to submit email: " ^ error_msg)))
-
| Some _ -> Lwt.return (Error (Parse_error "Email submission not found in response"))
-
| None -> Lwt.return (Error (Parse_error "Email submission not found in response"))
-
| Some (`Null) -> Lwt.return (Error (Parse_error "No created submissions in response"))
-
| Some _ -> Lwt.return (Error (Parse_error "Invalid response format for created submissions"))
-
| None -> Lwt.return (Error (Parse_error "No created submissions in response"))
-
| Not_found -> Lwt.return (Error (Parse_error "EmailSubmission/set method response not found"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e)))
-
| Error e -> Lwt.return (Error e)
-
| (Error e, _) -> Lwt.return (Error e)
-
| (_, Error e) -> Lwt.return (Error e)
-
| Error e -> Lwt.return (Error e)
-
(** Get status of an email submission
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param submission_id The email submission ID
-
@return The submission status if successful
-
let get_submission_status conn ~account_id ~submission_id =
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Submission
-
name = "EmailSubmission/get";
-
("accountId", `String account_id);
-
("ids", `A [`String submission_id]);
-
let* response_result = make_request conn.config request in
-
match response_result with
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "EmailSubmission/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A [submission]) ->
-
let parse_submission json =
-
let id = get_string (find json ["id"]) in
-
let identity_id = get_string (find json ["identityId"]) in
-
let email_id = get_string (find json ["emailId"]) in
-
let thread_id = get_string (find json ["threadId"]) in
-
match find_opt json ["envelope"] with
-
| Some (`O env) -> begin
-
let parse_address addr_json =
-
let email = get_string (find addr_json ["email"]) in
-
match find_opt addr_json ["parameters"] with
-
Some (List.map (fun (k, v) -> (k, get_string v)) params)
-
{ Types.email; parameters }
-
let mail_from = parse_address (find (`O env) ["mailFrom"]) in
-
match find (`O env) ["rcptTo"] with
-
| `A rcpts -> List.map parse_address rcpts
-
Some { Types.mail_from; rcpt_to }
-
match find_opt json ["sendAt"] with
-
| Some (`String date) -> Some date
-
match find_opt json ["undoStatus"] with
-
| Some (`String "pending") -> Some `pending
-
| Some (`String "final") -> Some `final
-
| Some (`String "canceled") -> Some `canceled
-
let parse_delivery_status deliveries =
-
Some (List.map (fun (email, status_json) ->
-
let smtp_reply = get_string (find status_json ["smtpReply"]) in
-
match find_opt status_json ["delivered"] with
-
| Some (`String d) -> Some d
-
(email, { Types.smtp_reply; delivered })
-
match find_opt json ["deliveryStatus"] with
-
| Some status -> parse_delivery_status status
-
match find_opt json ["dsnBlobIds"] with
-
| Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
-
match find_opt json ["mdnBlobIds"] with
-
| Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
-
| Not_found -> Error (Parse_error "Required field not found in submission object")
-
| Invalid_argument msg -> Error (Parse_error msg)
-
| e -> Error (Parse_error (Printexc.to_string e))
-
parse_submission submission
-
| Some (`A []) -> Error (Parse_error ("Submission not found: " ^ submission_id))
-
| _ -> Error (Parse_error "Expected single submission in response")
-
| Not_found -> Error (Parse_error "EmailSubmission/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
| Error e -> Lwt.return (Error e)
-
(** {1 Email Address Utilities} *)
-
(** Custom implementation of substring matching *)
-
let contains_substring str sub =
-
let _ = Str.search_forward (Str.regexp_string sub) str 0 in
-
with Not_found -> false
-
(** Checks if a pattern with wildcards matches a string
-
@param pattern Pattern string with * and ? wildcards
-
@param str String to match against
-
Based on simple recursive wildcard matching algorithm
-
let matches_wildcard pattern str =
-
let pattern_len = String.length pattern in
-
let str_len = String.length str in
-
(* Convert both to lowercase for case-insensitive matching *)
-
let pattern = String.lowercase_ascii pattern in
-
let str = String.lowercase_ascii str in
-
(* If there are no wildcards, do a simple substring check *)
-
if not (String.contains pattern '*' || String.contains pattern '?') then
-
contains_substring str pattern
-
(* Classic recursive matching algorithm *)
-
let rec match_from p_pos s_pos =
-
(* Pattern matched to the end *)
-
if p_pos = pattern_len then
-
(* Star matches zero or more chars *)
-
else if pattern.[p_pos] = '*' then
-
match_from (p_pos + 1) s_pos || (* Match empty string *)
-
(s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
-
(* If both have more chars and they match or ? wildcard *)
-
else if s_pos < str_len &&
-
(pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
-
match_from (p_pos + 1) (s_pos + 1)
-
(** Check if an email address matches a filter string
-
@param email The email address to check
-
@param pattern The filter pattern to match against
-
@return True if the email address matches the filter
-
let email_address_matches email pattern =
-
matches_wildcard pattern email
-
(** Check if an email matches a sender filter
-
@param email The email object to check
-
@param pattern The sender filter pattern
-
@return True if any sender address matches the filter
-
let email_matches_sender (email : Types.email) pattern =
-
(* Helper to extract emails from address list *)
-
let addresses_match addrs =
-
List.exists (fun (addr : Types.email_address) ->
-
email_address_matches addr.email pattern
-
(* Check From addresses first *)
-
match email.Types.from with
-
| Some addrs -> addresses_match addrs
-
(* If no match in From, check Sender field *)
-
if from_match then true
-
match email.Types.sender with
-
| Some addrs -> addresses_match addrs