OCaml library to handle JMAP/IMAP email keywords (including Apple)
at main 286 lines 11 kB view raw
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