OCaml library to handle JMAP/IMAP email keywords (including Apple)
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Unified Mail Flags for IMAP and JMAP
7
8 This library provides a unified representation of message flags and mailbox
9 attributes that works across both IMAP
10 ({{:https://datatracker.ietf.org/doc/html/rfc9051}RFC 9051}) and JMAP
11 ({{:https://datatracker.ietf.org/doc/html/rfc8621}RFC 8621}) protocols.
12
13 {2 Overview}
14
15 The library defines three main concepts:
16
17 - {!Keyword}: Message keywords/flags like [`Seen], [`Flagged], [`Junk]
18 - {!Mailbox_attr}: Mailbox attributes and special-use roles like [`Drafts], [`Inbox]
19 - {!Flag_color}: Apple Mail flag color encoding
20
21 All types use polymorphic variants for:
22 - Type safety: The compiler catches invalid flag combinations
23 - Extensibility: Custom flags via [`Custom] and [`Extension] variants
24 - Interoperability: Easy conversion between protocol representations
25
26 {2 Protocol Mapping}
27
28 {b IMAP system flags} ([\Seen], [\Answered], etc.) map to {!standard} keywords.
29 Use {!Keyword.to_imap_string} for wire format conversion.
30
31 {b JMAP keywords} ([$seen], [$answered], etc.) are the canonical form.
32 Use {!Keyword.to_string} for JMAP format.
33
34 {b Mailbox roles} work similarly with {!Mailbox_attr.to_string} for IMAP
35 and {!Mailbox_attr.to_jmap_role} for JMAP.
36
37 {2 References}
38
39 - {{:https://www.rfc-editor.org/rfc/rfc9051}RFC 9051} - IMAP4rev2
40 - {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621} - JMAP for Mail
41 - {{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154} - IMAP Special-Use Mailboxes
42 - {{:https://datatracker.ietf.org/doc/draft-ietf-mailmaint-messageflag-mailboxattribute}
43 draft-ietf-mailmaint} - Extended keywords and attributes *)
44
45(** {1 Modules} *)
46
47(** Message keywords for both IMAP and JMAP.
48
49 See {!module:Keyword} for the full API. *)
50module Keyword : sig
51 (** {1 Keyword Types} *)
52
53 (** Standard keywords per RFC 8621 Section 4.1.1 that map to IMAP system flags. *)
54 type standard = [
55 | `Seen (** Message has been read. Maps to IMAP [\Seen]. *)
56 | `Answered (** Message has been answered. Maps to IMAP [\Answered]. *)
57 | `Flagged (** Message is flagged/starred. Maps to IMAP [\Flagged]. *)
58 | `Draft (** Message is a draft. Maps to IMAP [\Draft]. *)
59 | `Deleted (** Message marked for deletion. Maps to IMAP [\Deleted]. *)
60 | `Forwarded (** Message has been forwarded. JMAP [$forwarded]. *)
61 ]
62
63 (** Spam-related keywords for junk mail handling. *)
64 type spam = [
65 | `Phishing (** Message is a phishing attempt. *)
66 | `Junk (** Message is spam/junk. *)
67 | `NotJunk (** Message explicitly marked as not junk. *)
68 ]
69
70 (** Extended keywords per draft-ietf-mailmaint. *)
71 type extended = [
72 | `HasAttachment (** Message has attachments. *)
73 | `HasNoAttachment (** Message has no attachments. *)
74 | `Memo (** Message is a memo. *)
75 | `HasMemo (** Message has an associated memo. *)
76 | `CanUnsubscribe (** Message has unsubscribe capability. *)
77 | `Unsubscribed (** User has unsubscribed from this sender. *)
78 | `Muted (** Thread is muted. *)
79 | `Followed (** Thread is followed. *)
80 | `AutoSent (** Message was sent automatically. *)
81 | `Imported (** Message was imported from another source. *)
82 | `IsTrusted (** Sender is trusted. *)
83 | `MaskedEmail (** Message was sent to a masked email address. *)
84 | `New (** Message is new (not yet processed). *)
85 | `Notify (** User should be notified about this message. *)
86 ]
87
88 (** Apple Mail flag color bits. *)
89 type flag_bit = [
90 | `MailFlagBit0 (** Bit 0 of Apple Mail flag color encoding. *)
91 | `MailFlagBit1 (** Bit 1 of Apple Mail flag color encoding. *)
92 | `MailFlagBit2 (** Bit 2 of Apple Mail flag color encoding. *)
93 ]
94
95 (** Unified keyword type combining all categories. *)
96 type t = [ standard | spam | extended | flag_bit | `Custom of string ]
97
98 (** {1 Conversion Functions} *)
99
100 val of_string : string -> t
101 (** [of_string s] parses a keyword string.
102 Handles JMAP format ([$seen]), IMAP format ([\Seen]), and bare format ([seen]).
103 Unknown keywords become [`Custom]. *)
104
105 val to_string : t -> string
106 (** [to_string k] converts a keyword to canonical JMAP format (e.g., ["$seen"]). *)
107
108 val to_imap_string : t -> string
109 (** [to_imap_string k] converts a keyword to IMAP wire format.
110 Standard keywords use backslash ([\Seen]), others use dollar ([$Forwarded]). *)
111
112 (** {1 Predicates} *)
113
114 val is_standard : t -> bool
115 (** [is_standard k] returns [true] if [k] maps to an IMAP system flag. *)
116
117 val is_mutually_exclusive : t -> t -> bool
118 (** [is_mutually_exclusive k1 k2] returns [true] if keywords cannot both be set.
119 Mutually exclusive pairs: HasAttachment/HasNoAttachment, Junk/NotJunk, Muted/Followed. *)
120
121 (** {1 Comparison and Pretty Printing} *)
122
123 val equal : t -> t -> bool
124 val compare : t -> t -> int
125 val pp : Format.formatter -> t -> unit
126
127 (** {1 Apple Mail Flag Colors} *)
128
129 type flag_color = [
130 | `Red | `Orange | `Yellow | `Green | `Blue | `Purple | `Gray
131 ]
132
133 val flag_color_of_keywords : t list -> flag_color option
134 (** Extract Apple Mail flag color from keywords. Returns [None] for invalid encoding. *)
135
136 val flag_color_to_keywords : flag_color -> t list
137 (** Convert flag color to the keyword bits needed to represent it. *)
138end
139
140(** Mailbox attributes and special-use roles.
141
142 See {!module:Mailbox_attr} for the full API. *)
143module Mailbox_attr : sig
144 (** {1 Attribute Types} *)
145
146 (** IMAP LIST response attributes per RFC 9051 Section 7.2.2. *)
147 type list_attr = [
148 | `Noinferiors (** No child mailboxes possible. *)
149 | `Noselect (** Mailbox cannot be selected. *)
150 | `Marked (** Mailbox has new messages. *)
151 | `Unmarked (** Mailbox has no new messages. *)
152 | `Subscribed (** Mailbox is subscribed. *)
153 | `HasChildren (** Mailbox has child mailboxes. *)
154 | `HasNoChildren (** Mailbox has no children. *)
155 | `NonExistent (** Mailbox does not exist. *)
156 | `Remote (** Mailbox is on a remote server. *)
157 ]
158
159 (** Special-use mailbox roles per RFC 6154 and RFC 8621. *)
160 type special_use = [
161 | `All (** Virtual mailbox with all messages. *)
162 | `Archive (** Archive mailbox. *)
163 | `Drafts (** Drafts mailbox. *)
164 | `Flagged (** Virtual mailbox with flagged messages. *)
165 | `Important (** Important messages mailbox. *)
166 | `Inbox (** User's inbox. *)
167 | `Junk (** Spam/junk mailbox. *)
168 | `Sent (** Sent messages mailbox. *)
169 | `Subscribed (** JMAP virtual subscribed mailbox. *)
170 | `Trash (** Trash/deleted messages mailbox. *)
171 | `Snoozed (** Snoozed messages (draft-ietf-mailmaint). *)
172 | `Scheduled (** Scheduled to send (draft-ietf-mailmaint). *)
173 | `Memos (** Memo messages (draft-ietf-mailmaint). *)
174 ]
175
176 (** Unified mailbox attribute type. *)
177 type t = [ list_attr | special_use | `Extension of string ]
178
179 (** {1 Conversion Functions} *)
180
181 val of_string : string -> t
182 (** [of_string s] parses a mailbox attribute from IMAP wire format.
183 Unknown attributes become [`Extension]. *)
184
185 val to_string : t -> string
186 (** [to_string attr] converts to IMAP wire format with backslash prefix. *)
187
188 val to_jmap_role : t -> string option
189 (** [to_jmap_role attr] converts to JMAP role string (lowercase).
190 Returns [None] for LIST attributes without JMAP equivalents. *)
191
192 val of_jmap_role : string -> special_use option
193 (** [of_jmap_role s] parses a JMAP role string into a special-use attribute. *)
194
195 (** {1 Predicates} *)
196
197 val is_special_use : t -> bool
198 (** [is_special_use attr] returns [true] if attribute is a special-use role. *)
199
200 val is_selectable : t -> bool
201 (** [is_selectable attr] returns [false] for Noselect and NonExistent. *)
202
203 (** {1 Pretty Printing} *)
204
205 val pp : Format.formatter -> t -> unit
206end
207
208(** Apple Mail flag colors.
209
210 See {!module:Flag_color} for the full API. *)
211module Flag_color : sig
212 (** Flag colors encoded as 3-bit values using [$MailFlagBit*] keywords. *)
213 type t = [
214 | `Red (** Bit pattern: 000 *)
215 | `Orange (** Bit pattern: 100 *)
216 | `Yellow (** Bit pattern: 010 *)
217 | `Green (** Bit pattern: 110 *)
218 | `Blue (** Bit pattern: 001 *)
219 | `Purple (** Bit pattern: 101 *)
220 | `Gray (** Bit pattern: 011 *)
221 ]
222
223 (** {1 Bit Pattern Conversion} *)
224
225 val to_bits : t -> bool * bool * bool
226 (** [to_bits color] returns [(bit0, bit1, bit2)] tuple. *)
227
228 val of_bits : bool * bool * bool -> t option
229 (** [of_bits (b0, b1, b2)] converts bit pattern to color.
230 Returns [None] for undefined pattern (true, true, true). *)
231
232 (** {1 Keyword Conversion} *)
233
234 val to_keywords : t -> [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list
235 (** [to_keywords color] returns keyword bits for the color. *)
236
237 val of_keywords : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
238 (** [of_keywords kws] extracts color from keyword bits.
239 Returns [None] if no bits set (ambiguous) or pattern is 111 (undefined). *)
240
241 val of_keywords_default_red : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
242 (** Like {!of_keywords} but treats empty list as Red. *)
243
244 (** {1 String Conversion} *)
245
246 val to_string : t -> string
247 (** [to_string color] returns lowercase color name. *)
248
249 val of_string : string -> t option
250 (** [of_string s] parses color name (case-insensitive, accepts "grey"). *)
251
252 (** {1 Pretty Printing} *)
253
254 val pp : Format.formatter -> t -> unit
255end
256
257(** {1 Type Aliases}
258
259 Convenient type aliases for use without module qualification. *)
260
261(** Standard message keywords that map to IMAP system flags. *)
262type standard = Keyword.standard
263
264(** Spam-related keywords. *)
265type spam = Keyword.spam
266
267(** Extended keywords from draft-ietf-mailmaint. *)
268type extended = Keyword.extended
269
270(** Apple Mail flag color bit keywords. *)
271type flag_bit = Keyword.flag_bit
272
273(** Unified message keyword type. *)
274type keyword = Keyword.t
275
276(** IMAP LIST response attributes. *)
277type list_attr = Mailbox_attr.list_attr
278
279(** Special-use mailbox roles. *)
280type special_use = Mailbox_attr.special_use
281
282(** Unified mailbox attribute type. *)
283type mailbox_attr = Mailbox_attr.t
284
285(** Apple Mail flag colors. *)
286type flag_color = Flag_color.t