Testing a Gemini codegen run

Add Email module with Keywords as variants

- Implement Email.t type with proper keyword support
- Create Keywords module with explicit variants for system and custom flags
- Add operations for manipulating email flags
- Add conversion utilities for JMAP/IMAP interoperability
- Update email-related types to use the module style with abstract types
- Convert record types to modules with accessors and constructors

The keyword implementation uses a variant type to make the different
flags more explicit and type-safe, while still supporting custom keywords
via a Custom constructor. Implementation follows RFC 8621 section 4.1.1.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+69
output/jmap_email.mli
···
val keyword_phishing : string
val keyword_junk : string
val keyword_notjunk : string
+
+
(** Functions to manipulate email flags/keywords *)
+
module Keyword_ops : sig
+
(** Add a keyword/flag to an email *)
+
val add : Types.Email.t -> Types.Keywords.keyword -> Types.Email.t
+
+
(** Remove a keyword/flag from an email *)
+
val remove : Types.Email.t -> Types.Keywords.keyword -> Types.Email.t
+
+
(** Mark an email as seen/read *)
+
val mark_as_seen : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as unseen/unread *)
+
val mark_as_unseen : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as flagged/important *)
+
val mark_as_flagged : Types.Email.t -> Types.Email.t
+
+
(** Remove flagged/important marking from an email *)
+
val unmark_flagged : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as a draft *)
+
val mark_as_draft : Types.Email.t -> Types.Email.t
+
+
(** Remove draft marking from an email *)
+
val unmark_draft : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as answered/replied *)
+
val mark_as_answered : Types.Email.t -> Types.Email.t
+
+
(** Remove answered/replied marking from an email *)
+
val unmark_answered : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as forwarded *)
+
val mark_as_forwarded : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as spam/junk *)
+
val mark_as_junk : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as not spam/junk *)
+
val mark_as_not_junk : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as phishing *)
+
val mark_as_phishing : Types.Email.t -> Types.Email.t
+
+
(** Add a custom keyword to an email *)
+
val add_custom : Types.Email.t -> string -> Types.Email.t
+
+
(** Remove a custom keyword from an email *)
+
val remove_custom : Types.Email.t -> string -> Types.Email.t
+
end
+
+
(** Conversion functions for JMAP/IMAP compatibility *)
+
module Conversion : sig
+
(** Convert a JMAP keyword variant to IMAP flag *)
+
val keyword_to_imap_flag : Types.Keywords.keyword -> string
+
+
(** Convert an IMAP flag to JMAP keyword variant *)
+
val imap_flag_to_keyword : string -> Types.Keywords.keyword
+
+
(** Check if a string is valid for use as a custom keyword according to RFC 8621 *)
+
val is_valid_custom_keyword : string -> bool
+
+
(** Get the JMAP protocol string representation of a keyword *)
+
val keyword_to_string : Types.Keywords.keyword -> string
+
+
(** Parse a JMAP protocol string into a keyword variant *)
+
val string_to_keyword : string -> Types.Keywords.keyword
+
end
+236 -32
output/jmap_email_types.mli
···
(** Represents an email address with an optional name.
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.2.3> RFC 8621, Section 4.1.2.3 *)
-
type email_address = {
-
email_addr_name : string option;
-
email_addr_email : string;
-
}
+
module Email_address : sig
+
type t
+
+
(** Get the display name for the address (if any) *)
+
val name : t -> string option
+
+
(** Get the email address *)
+
val email : t -> string
+
+
(** Create a new email address *)
+
val v :
+
?name:string ->
+
email:string ->
+
unit -> t
+
end
(** Represents a group of email addresses.
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.2.4> RFC 8621, Section 4.1.2.4 *)
-
type email_address_group = {
-
email_group_name : string option;
-
email_group_addresses : email_address list;
-
}
+
module Email_address_group : sig
+
type t
+
+
(** Get the name of the group (if any) *)
+
val name : t -> string option
+
+
(** Get the list of addresses in the group *)
+
val addresses : t -> Email_address.t list
+
+
(** Create a new address group *)
+
val v :
+
?name:string ->
+
addresses:Email_address.t list ->
+
unit -> t
+
end
(** Represents a header field (name and raw value).
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.3> RFC 8621, Section 4.1.3 *)
-
type email_header = {
-
header_name : string;
-
header_value : string; (* Raw form *)
-
}
+
module Email_header : sig
+
type t
+
+
(** Get the header field name *)
+
val name : t -> string
+
+
(** Get the raw header field value *)
+
val value : t -> string
+
+
(** Create a new header field *)
+
val v :
+
name:string ->
+
value:string ->
+
unit -> t
+
end
(** Represents a body part within an Email's MIME structure.
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.4> RFC 8621, Section 4.1.4 *)
-
type email_body_part = {
-
part_id : string option; (** null only for multipart/* *)
-
part_blob_id : id option; (** null only for multipart/* *)
-
part_size : uint;
-
part_headers : email_header list;
-
part_name : string option;
-
part_type : string;
-
part_charset : string option;
-
part_disposition : string option;
-
part_cid : string option;
-
part_language : string list option;
-
part_location : string option;
-
part_sub_parts : email_body_part list option; (** only for multipart/* *)
-
part_other_headers : Yojson.Safe.t string_map; (** Requested header:* properties *)
-
}
+
module Email_body_part : sig
+
type t
+
+
(** Get the part ID (null only for multipart types) *)
+
val id : t -> string option
+
+
(** Get the blob ID (null only for multipart types) *)
+
val blob_id : t -> id option
+
+
(** Get the size of the part in bytes *)
+
val size : t -> uint
+
+
(** Get the list of headers for this part *)
+
val headers : t -> Email_header.t list
+
+
(** Get the filename (if any) *)
+
val name : t -> string option
+
+
(** Get the MIME type *)
+
val mime_type : t -> string
+
+
(** Get the charset (if any) *)
+
val charset : t -> string option
+
+
(** Get the content disposition (if any) *)
+
val disposition : t -> string option
+
+
(** Get the content ID (if any) *)
+
val cid : t -> string option
+
+
(** Get the list of languages (if any) *)
+
val language : t -> string list option
+
+
(** Get the content location (if any) *)
+
val location : t -> string option
+
+
(** Get the sub-parts (only for multipart types) *)
+
val sub_parts : t -> t list option
+
+
(** Get any other requested headers (header properties) *)
+
val other_headers : t -> Yojson.Safe.t string_map
+
+
(** Create a new body part *)
+
val v :
+
?id:string ->
+
?blob_id:id ->
+
size:uint ->
+
headers:Email_header.t list ->
+
?name:string ->
+
mime_type:string ->
+
?charset:string ->
+
?disposition:string ->
+
?cid:string ->
+
?language:string list ->
+
?location:string ->
+
?sub_parts:t list ->
+
?other_headers:Yojson.Safe.t string_map ->
+
unit -> t
+
end
(** Represents the decoded value of a text body part.
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.4> RFC 8621, Section 4.1.4 *)
-
type email_body_value = {
-
body_value : string;
-
is_encoding_problem : bool; (* default: false *)
-
is_truncated : bool; (* default: false *)
-
}
+
module Email_body_value : sig
+
type t
+
+
(** Get the decoded text content *)
+
val value : t -> string
+
+
(** Check if there was an encoding problem *)
+
val has_encoding_problem : t -> bool
+
+
(** Check if the content was truncated *)
+
val is_truncated : t -> bool
+
+
(** Create a new body value *)
+
val v :
+
value:string ->
+
?encoding_problem:bool ->
+
?truncated:bool ->
+
unit -> t
+
end
+
+
(** Type to represent email message flags/keywords.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.1> RFC 8621, Section 4.1.1 *)
+
module Keywords : sig
+
(** Represents different types of JMAP keywords *)
+
type keyword =
+
| Draft (** "$draft": The Email is a draft the user is composing *)
+
| Seen (** "$seen": The Email has been read *)
+
| Flagged (** "$flagged": The Email has been flagged for urgent/special attention *)
+
| Answered (** "$answered": The Email has been replied to *)
+
+
(* Common extension keywords from RFC 5788 *)
+
| Forwarded (** "$forwarded": The Email has been forwarded *)
+
| Phishing (** "$phishing": The Email is likely to be phishing *)
+
| Junk (** "$junk": The Email is spam/junk *)
+
| NotJunk (** "$notjunk": The Email is explicitly marked as not spam/junk *)
+
| Custom of string (** Arbitrary user-defined keyword *)
+
+
(** A set of keywords applied to an email *)
+
type t = keyword list
+
+
(** Check if an email has the draft flag *)
+
val is_draft : t -> bool
+
+
(** Check if an email has been read *)
+
val is_seen : t -> bool
+
+
(** Check if an email has neither been read nor is a draft *)
+
val is_unread : t -> bool
+
+
(** Check if an email has been flagged *)
+
val is_flagged : t -> bool
+
+
(** Check if an email has been replied to *)
+
val is_answered : t -> bool
+
+
(** Check if an email has been forwarded *)
+
val is_forwarded : t -> bool
+
+
(** Check if an email is marked as likely phishing *)
+
val is_phishing : t -> bool
+
+
(** Check if an email is marked as junk/spam *)
+
val is_junk : t -> bool
+
+
(** Check if an email is explicitly marked as not junk/spam *)
+
val is_not_junk : t -> bool
+
+
(** Check if a specific custom keyword is set *)
+
val has_keyword : t -> string -> bool
+
+
(** Get a list of all custom keywords (excluding system keywords) *)
+
val custom_keywords : t -> string list
+
+
(** Add a keyword to the set *)
+
val add : t -> keyword -> t
+
+
(** Remove a keyword from the set *)
+
val remove : t -> keyword -> t
+
+
(** Create an empty keyword set *)
+
val empty : unit -> t
+
+
(** Create a new keyword set with the specified keywords *)
+
val of_list : keyword list -> t
+
+
(** Get the string representation of a keyword as used in the JMAP protocol *)
+
val to_string : keyword -> string
+
+
(** Parse a string into a keyword *)
+
val of_string : string -> keyword
+
+
(** Convert keyword set to string map representation as used in JMAP *)
+
val to_map : t -> bool string_map
+
end
+
+
(** Represents an Email object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1> RFC 8621, Section 4.1 *)
+
module Email : sig
+
type t
+
+
(** ID of the email *)
+
val id : t -> id
+
+
(** ID of the blob containing the raw message *)
+
val blob_id : t -> id
+
+
(** ID of the thread this email belongs to *)
+
val thread_id : t -> id
+
+
(** The set of mailbox IDs this email belongs to *)
+
val mailbox_ids : t -> bool id_map
+
+
(** The set of keywords/flags for this email *)
+
val keywords : t -> Keywords.t
+
+
(** Size of the message in bytes *)
+
val size : t -> uint
+
+
(** When the message was received by the server *)
+
val received_at : t -> date
+
+
(** Create a new Email object *)
+
val v :
+
id:id ->
+
blob_id:id ->
+
thread_id:id ->
+
mailbox_ids:bool id_map ->
+
keywords:Keywords.t ->
+
size:uint ->
+
received_at:date ->
+
unit -> t
+
end
+4 -4
output/jmap_identity.mli
···
identity_id : id; (** immutable, server-set *)
name : string; (* default: "" *)
email : string; (** immutable *)
-
reply_to : email_address list option;
-
bcc : email_address list option;
+
reply_to : Email_address.t list option;
+
bcc : Email_address.t list option;
text_signature : string; (* default: "" *)
html_signature : string; (* default: "" *)
may_delete : bool; (** server-set *)
···
type identity_create = {
identity_create_name : string option;
identity_create_email : string;
-
identity_create_reply_to : email_address list option;
-
identity_create_bcc : email_address list option;
+
identity_create_reply_to : Email_address.t list option;
+
identity_create_bcc : Email_address.t list option;
identity_create_text_signature : string option;
identity_create_html_signature : string option;
}