···11+ISC License
22+33+Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
44+55+Permission to use, copy, modify, and distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1010+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1111+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1212+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1313+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1414+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+62
README.md
···11+# mail-flag - Unified Message Flags for IMAP and JMAP
22+33+Type-safe message keywords, system flags, and mailbox attributes for email protocols.
44+55+## Key Features
66+77+- **Unified keyword types**: Standard, spam, extended, and Apple Mail flag color keywords
88+- **Protocol conversion**: Seamless mapping between IMAP system flags and JMAP keywords
99+- **Mailbox attributes**: LIST attributes and special-use roles (RFC 6154)
1010+- **Apple Mail colors**: 3-bit flag color encoding/decoding
1111+1212+## Supported Standards
1313+1414+- [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051) - IMAP4rev2
1515+- [RFC 8621](https://www.rfc-editor.org/rfc/rfc8621) - JMAP for Mail
1616+- [RFC 6154](https://www.rfc-editor.org/rfc/rfc6154) - IMAP Special-Use Mailboxes
1717+- [draft-ietf-mailmaint](https://datatracker.ietf.org/doc/draft-ietf-mailmaint-messageflag-mailboxattribute) - Extended keywords and attributes
1818+1919+## Usage
2020+2121+```ocaml
2222+open Mail_flag
2323+2424+(* Parse keywords from IMAP or JMAP format *)
2525+let seen = Keyword.of_string "\\Seen" (* IMAP system flag *)
2626+let junk = Keyword.of_string "$junk" (* JMAP keyword *)
2727+2828+(* Convert between formats *)
2929+let imap_str = Keyword.to_imap_string seen (* "\\Seen" *)
3030+let jmap_str = Keyword.to_string seen (* "$seen" *)
3131+3232+(* Check keyword properties *)
3333+let is_system = Keyword.is_standard seen (* true *)
3434+let exclusive = Keyword.is_mutually_exclusive `Junk `NotJunk (* true *)
3535+3636+(* Work with mailbox attributes *)
3737+let drafts = Mailbox_attr.of_string "\\Drafts"
3838+let role = Mailbox_attr.to_jmap_role drafts (* Some "drafts" *)
3939+4040+(* Apple Mail flag colors *)
4141+let color = Flag_color.of_keywords [`MailFlagBit0; `MailFlagBit2]
4242+(* color = Some `Purple *)
4343+```
4444+4545+## Installation
4646+4747+```
4848+opam install mail-flag
4949+```
5050+5151+## Documentation
5252+5353+API documentation is available via:
5454+5555+```
5656+opam install mail-flag
5757+odig doc mail-flag
5858+```
5959+6060+## License
6161+6262+ISC
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Apple Mail flag colors.
77+88+ See {{:https://datatracker.ietf.org/doc/draft-ietf-mailmaint-messageflag-mailboxattribute#section-3}
99+ draft-ietf-mailmaint-messageflag-mailboxattribute Section 3}.
1010+1111+ The Apple Mail flag color encoding uses three keywords to represent
1212+ colors as a 3-bit pattern:
1313+ - [$MailFlagBit0]: bit 0
1414+ - [$MailFlagBit1]: bit 1
1515+ - [$MailFlagBit2]: bit 2
1616+1717+ Bit patterns (bit0, bit1, bit2):
1818+ - Red: (false, false, false) = 000
1919+ - Orange: (true, false, false) = 100
2020+ - Yellow: (false, true, false) = 010
2121+ - Green: (true, true, false) = 110
2222+ - Blue: (false, false, true) = 001
2323+ - Purple: (true, false, true) = 101
2424+ - Gray: (false, true, true) = 011
2525+ - 111: undefined *)
2626+2727+type t = [
2828+ | `Red (** Bit pattern: 000 *)
2929+ | `Orange (** Bit pattern: 100 *)
3030+ | `Yellow (** Bit pattern: 010 *)
3131+ | `Green (** Bit pattern: 110 *)
3232+ | `Blue (** Bit pattern: 001 *)
3333+ | `Purple (** Bit pattern: 101 *)
3434+ | `Gray (** Bit pattern: 011 *)
3535+]
3636+3737+let to_bits = function
3838+ | `Red -> (false, false, false) (* 000 *)
3939+ | `Orange -> (true, false, false) (* 100 *)
4040+ | `Yellow -> (false, true, false) (* 010 *)
4141+ | `Green -> (true, true, false) (* 110 *)
4242+ | `Blue -> (false, false, true) (* 001 *)
4343+ | `Purple -> (true, false, true) (* 101 *)
4444+ | `Gray -> (false, true, true) (* 011 *)
4545+4646+let of_bits = function
4747+ | (false, false, false) -> Some `Red (* 000 *)
4848+ | (true, false, false) -> Some `Orange (* 100 *)
4949+ | (false, true, false) -> Some `Yellow (* 010 *)
5050+ | (true, true, false) -> Some `Green (* 110 *)
5151+ | (false, false, true) -> Some `Blue (* 001 *)
5252+ | (true, false, true) -> Some `Purple (* 101 *)
5353+ | (false, true, true) -> Some `Gray (* 011 *)
5454+ | (true, true, true) -> None (* 111 - undefined *)
5555+5656+let to_keywords = function
5757+ | `Red -> []
5858+ | `Orange -> [ `MailFlagBit0 ]
5959+ | `Yellow -> [ `MailFlagBit1 ]
6060+ | `Green -> [ `MailFlagBit0; `MailFlagBit1 ]
6161+ | `Blue -> [ `MailFlagBit2 ]
6262+ | `Purple -> [ `MailFlagBit0; `MailFlagBit2 ]
6363+ | `Gray -> [ `MailFlagBit1; `MailFlagBit2 ]
6464+6565+let of_keywords (keywords : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list) =
6666+ let has k = List.exists (fun x -> x = k) keywords in
6767+ let bit0 = has `MailFlagBit0 in
6868+ let bit1 = has `MailFlagBit1 in
6969+ let bit2 = has `MailFlagBit2 in
7070+ (* If no bits are set, we cannot distinguish between "no flag color"
7171+ and "Red" (which is 000). Return None to indicate ambiguity. *)
7272+ if not bit0 && not bit1 && not bit2 then None
7373+ else of_bits (bit0, bit1, bit2)
7474+7575+let of_keywords_default_red (keywords : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list) =
7676+ let has k = List.exists (fun x -> x = k) keywords in
7777+ let bit0 = has `MailFlagBit0 in
7878+ let bit1 = has `MailFlagBit1 in
7979+ let bit2 = has `MailFlagBit2 in
8080+ of_bits (bit0, bit1, bit2)
8181+8282+let to_string = function
8383+ | `Red -> "red"
8484+ | `Orange -> "orange"
8585+ | `Yellow -> "yellow"
8686+ | `Green -> "green"
8787+ | `Blue -> "blue"
8888+ | `Purple -> "purple"
8989+ | `Gray -> "gray"
9090+9191+let of_string s =
9292+ match String.lowercase_ascii s with
9393+ | "red" -> Some `Red
9494+ | "orange" -> Some `Orange
9595+ | "yellow" -> Some `Yellow
9696+ | "green" -> Some `Green
9797+ | "blue" -> Some `Blue
9898+ | "purple" -> Some `Purple
9999+ | "gray" | "grey" -> Some `Gray
100100+ | _ -> None
101101+102102+let pp ppf color = Format.pp_print_string ppf (to_string color)
+73
lib/flag_color.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Apple Mail flag colors.
77+88+ This module implements the Apple Mail flag color encoding using the
99+ [$MailFlagBit0], [$MailFlagBit1], and [$MailFlagBit2] keywords.
1010+1111+ See {{:https://datatracker.ietf.org/doc/draft-ietf-mailmaint-messageflag-mailboxattribute#section-3}
1212+ draft-ietf-mailmaint-messageflag-mailboxattribute Section 3}.
1313+1414+ Colors are encoded as a 3-bit pattern where each bit corresponds to
1515+ a keyword:
1616+ - Bit 0: [$MailFlagBit0]
1717+ - Bit 1: [$MailFlagBit1]
1818+ - Bit 2: [$MailFlagBit2]
1919+2020+ The bit patterns are:
2121+ - Red: 000 (no bits set)
2222+ - Orange: 100 (bit 0 only)
2323+ - Yellow: 010 (bit 1 only)
2424+ - Green: 110 (bits 0 and 1)
2525+ - Blue: 001 (bit 2 only)
2626+ - Purple: 101 (bits 0 and 2)
2727+ - Gray: 011 (bits 1 and 2)
2828+ - 111: undefined (all bits set) *)
2929+3030+type t = [
3131+ | `Red (** Bit pattern: 000 *)
3232+ | `Orange (** Bit pattern: 100 *)
3333+ | `Yellow (** Bit pattern: 010 *)
3434+ | `Green (** Bit pattern: 110 *)
3535+ | `Blue (** Bit pattern: 001 *)
3636+ | `Purple (** Bit pattern: 101 *)
3737+ | `Gray (** Bit pattern: 011 *)
3838+]
3939+4040+val to_bits : t -> bool * bool * bool
4141+(** [to_bits color] converts [color] to a [(bit0, bit1, bit2)] tuple
4242+ representing which [$MailFlagBit*] keywords should be set.
4343+4444+ Example: [to_bits Green] returns [(true, true, false)]. *)
4545+4646+val of_bits : bool * bool * bool -> t option
4747+(** [of_bits (bit0, bit1, bit2)] converts a bit pattern to a color.
4848+ Returns [None] for the undefined pattern [(true, true, true)] (111). *)
4949+5050+val to_keywords : t -> [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list
5151+(** [to_keywords color] returns the list of keyword bits that should be
5252+ set for [color]. Red returns an empty list since no bits are needed. *)
5353+5454+val of_keywords : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
5555+(** [of_keywords keywords] extracts a color from a list of keyword bits.
5656+ Returns [None] if the pattern is 111 (undefined) or if no bits are
5757+ present in the list (which would indicate no flag color is set,
5858+ rather than Red). Use {!of_keywords_default_red} if you want to
5959+ treat an empty list as Red. *)
6060+6161+val of_keywords_default_red : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
6262+(** [of_keywords_default_red keywords] is like {!of_keywords} but treats
6363+ an empty keyword list as Red. Returns [None] only for pattern 111. *)
6464+6565+val pp : Format.formatter -> t -> unit
6666+(** [pp ppf color] pretty-prints the color name to [ppf]. *)
6767+6868+val to_string : t -> string
6969+(** [to_string color] returns the lowercase color name. *)
7070+7171+val of_string : string -> t option
7272+(** [of_string s] parses a color name (case-insensitive).
7373+ Returns [None] if [s] is not a valid color name. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Unified Message Keywords for IMAP and JMAP
77+88+ This module provides a unified representation of message keywords that
99+ works across both IMAP ({{:https://datatracker.ietf.org/doc/html/rfc9051}RFC 9051})
1010+ and JMAP ({{:https://datatracker.ietf.org/doc/html/rfc8621}RFC 8621}) protocols.
1111+1212+ {2 Keyword Types}
1313+1414+ Keywords are organized into categories based on their specification:
1515+ - {!standard}: Core flags from RFC 8621 Section 4.1.1 that map to IMAP system flags
1616+ - {!spam}: Spam-related keywords for junk mail handling
1717+ - {!extended}: Extended keywords from draft-ietf-mailmaint
1818+ - {!flag_bit}: Apple Mail flag color bits
1919+2020+ {2 Protocol Mapping}
2121+2222+ IMAP system flags ([\Seen], [\Answered], etc.) map to JMAP keywords
2323+ ([$seen], [$answered], etc.). The {!to_string} and {!to_imap_string}
2424+ functions handle these conversions. *)
2525+2626+(** {1 Keyword Types} *)
2727+2828+(** Standard keywords per {{:https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1}RFC 8621 Section 4.1.1}.
2929+3030+ These keywords have direct mappings to IMAP system flags defined in
3131+ {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}. *)
3232+type standard = [
3333+ | `Seen (** Message has been read. Maps to IMAP [\Seen]. *)
3434+ | `Answered (** Message has been answered. Maps to IMAP [\Answered]. *)
3535+ | `Flagged (** Message is flagged/starred. Maps to IMAP [\Flagged]. *)
3636+ | `Draft (** Message is a draft. Maps to IMAP [\Draft]. *)
3737+ | `Deleted (** Message marked for deletion. IMAP only, maps to [\Deleted]. *)
3838+ | `Forwarded (** Message has been forwarded. JMAP [$forwarded] keyword. *)
3939+]
4040+4141+(** Spam-related keywords for junk mail handling.
4242+4343+ These keywords help mail clients and servers coordinate spam filtering
4444+ decisions across protocol boundaries. *)
4545+type spam = [
4646+ | `Phishing (** Message is a phishing attempt. JMAP [$phishing]. *)
4747+ | `Junk (** Message is spam/junk. JMAP [$junk]. *)
4848+ | `NotJunk (** Message explicitly marked as not junk. JMAP [$notjunk]. *)
4949+]
5050+5151+(** Extended keywords per draft-ietf-mailmaint.
5252+5353+ These keywords provide additional metadata for enhanced mail client features
5454+ beyond basic read/reply tracking. *)
5555+type extended = [
5656+ | `HasAttachment (** Message has attachments. *)
5757+ | `HasNoAttachment (** Message has no attachments. Mutually exclusive with [`HasAttachment]. *)
5858+ | `Memo (** Message is a memo. *)
5959+ | `HasMemo (** Message has an associated memo. *)
6060+ | `CanUnsubscribe (** Message has unsubscribe capability (List-Unsubscribe header). *)
6161+ | `Unsubscribed (** User has unsubscribed from this sender. *)
6262+ | `Muted (** Thread is muted. Mutually exclusive with [`Followed]. *)
6363+ | `Followed (** Thread is followed. Mutually exclusive with [`Muted]. *)
6464+ | `AutoSent (** Message was sent automatically. *)
6565+ | `Imported (** Message was imported from another source. *)
6666+ | `IsTrusted (** Sender is trusted. *)
6767+ | `MaskedEmail (** Message was sent to a masked email address. *)
6868+ | `New (** Message is new (not yet processed by client). *)
6969+ | `Notify (** User should be notified about this message. *)
7070+]
7171+7272+(** Apple Mail flag color bits.
7373+7474+ Apple Mail uses a 3-bit encoding for flag colors. The color is determined
7575+ by the combination of bits set. See {!flag_color_of_keywords} for the
7676+ mapping. *)
7777+type flag_bit = [
7878+ | `MailFlagBit0 (** Bit 0 of Apple Mail flag color encoding. *)
7979+ | `MailFlagBit1 (** Bit 1 of Apple Mail flag color encoding. *)
8080+ | `MailFlagBit2 (** Bit 2 of Apple Mail flag color encoding. *)
8181+]
8282+8383+(** Unified keyword type combining all keyword categories.
8484+8585+ Use [`Custom s] for server-specific or application-specific keywords
8686+ not covered by the standard categories. *)
8787+type t = [ standard | spam | extended | flag_bit | `Custom of string ]
8888+8989+(** {1 Conversion Functions} *)
9090+9191+val of_string : string -> t
9292+(** [of_string s] parses a keyword string.
9393+9494+ Handles both JMAP format ([$seen]) and bare format ([seen]).
9595+ Parsing is case-insensitive for known keywords.
9696+9797+ Examples:
9898+ - ["$seen"] -> [`Seen]
9999+ - ["seen"] -> [`Seen]
100100+ - ["SEEN"] -> [`Seen]
101101+ - ["\\Seen"] -> [`Seen] (IMAP system flag format)
102102+ - ["my-custom-flag"] -> [`Custom "my-custom-flag"] *)
103103+104104+val to_string : t -> string
105105+(** [to_string k] converts a keyword to canonical JMAP format.
106106+107107+ Standard and extended keywords are returned with [$] prefix in lowercase.
108108+ Apple Mail flag bits preserve their mixed case.
109109+ Custom keywords are returned as-is.
110110+111111+ Examples:
112112+ - [`Seen] -> ["$seen"]
113113+ - [`MailFlagBit0] -> ["$MailFlagBit0"]
114114+ - [`Custom "foo"] -> ["foo"] *)
115115+116116+val to_imap_string : t -> string
117117+(** [to_imap_string k] converts a keyword to IMAP format.
118118+119119+ Standard keywords that map to IMAP system flags use backslash prefix.
120120+ Other keywords use [$] prefix with appropriate casing.
121121+122122+ Examples:
123123+ - [`Seen] -> ["\\Seen"]
124124+ - [`Deleted] -> ["\\Deleted"]
125125+ - [`Forwarded] -> ["$Forwarded"]
126126+ - [`MailFlagBit0] -> ["$MailFlagBit0"] *)
127127+128128+(** {1 Predicates} *)
129129+130130+val is_standard : t -> bool
131131+(** [is_standard k] returns [true] if [k] maps to an IMAP system flag.
132132+133133+ The standard keywords are: [`Seen], [`Answered], [`Flagged], [`Draft],
134134+ and [`Deleted]. Note that [`Forwarded] is {i not} an IMAP system flag. *)
135135+136136+val is_mutually_exclusive : t -> t -> bool
137137+(** [is_mutually_exclusive k1 k2] returns [true] if keywords [k1] and [k2]
138138+ cannot both be set on the same message.
139139+140140+ Mutually exclusive pairs:
141141+ - [`HasAttachment] and [`HasNoAttachment]
142142+ - [`Junk] and [`NotJunk]
143143+ - [`Muted] and [`Followed] *)
144144+145145+(** {1 Pretty Printing} *)
146146+147147+val pp : Format.formatter -> t -> unit
148148+(** [pp ppf k] pretty-prints keyword [k] in JMAP format. *)
149149+150150+val equal : t -> t -> bool
151151+(** [equal k1 k2] tests equality of keywords. *)
152152+153153+val compare : t -> t -> int
154154+(** [compare k1 k2] provides total ordering on keywords. *)
155155+156156+(** {1 Apple Mail Flag Colors} *)
157157+158158+(** Apple Mail flag colors encoded as 3-bit values. *)
159159+type flag_color = [
160160+ | `Red (** No bits set *)
161161+ | `Orange (** Bit 0 only *)
162162+ | `Yellow (** Bit 1 only *)
163163+ | `Green (** All bits set *)
164164+ | `Blue (** Bit 2 only *)
165165+ | `Purple (** Bits 0 and 2 *)
166166+ | `Gray (** Bits 1 and 2 *)
167167+]
168168+169169+val flag_color_of_keywords : t list -> flag_color option
170170+(** [flag_color_of_keywords keywords] extracts the Apple Mail flag color
171171+ from a list of keywords.
172172+173173+ Returns [None] if bits 0 and 1 are set but not bit 2 (invalid encoding). *)
174174+175175+val flag_color_to_keywords : flag_color -> t list
176176+(** [flag_color_to_keywords color] returns the keyword bits needed to
177177+ represent the given flag color. *)
+46
lib/mail_flag.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Unified Mail Flags for IMAP and JMAP
77+88+ This library provides a unified representation of message flags and mailbox
99+ attributes that works across both IMAP (RFC 9051) and JMAP (RFC 8621) protocols.
1010+1111+ The core types use polymorphic variants for type safety and extensibility. *)
1212+1313+(** {1 Module Aliases} *)
1414+1515+module Keyword = Keyword
1616+module Mailbox_attr = Mailbox_attr
1717+module Flag_color = Flag_color
1818+1919+(** {1 Type Aliases} *)
2020+2121+(** Standard message keywords that map to IMAP system flags. *)
2222+type standard = Keyword.standard
2323+2424+(** Spam-related keywords for junk mail handling. *)
2525+type spam = Keyword.spam
2626+2727+(** Extended keywords from draft-ietf-mailmaint. *)
2828+type extended = Keyword.extended
2929+3030+(** Apple Mail flag color bit keywords. *)
3131+type flag_bit = Keyword.flag_bit
3232+3333+(** Unified message keyword type combining all categories. *)
3434+type keyword = Keyword.t
3535+3636+(** IMAP LIST response attributes. *)
3737+type list_attr = Mailbox_attr.list_attr
3838+3939+(** Special-use mailbox roles. *)
4040+type special_use = Mailbox_attr.special_use
4141+4242+(** Unified mailbox attribute type. *)
4343+type mailbox_attr = Mailbox_attr.t
4444+4545+(** Apple Mail flag colors. *)
4646+type flag_color = Flag_color.t
+286
lib/mail_flag.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Unified Mail Flags for IMAP and JMAP
77+88+ This library provides a unified representation of message flags and mailbox
99+ attributes that works across both IMAP
1010+ ({{:https://datatracker.ietf.org/doc/html/rfc9051}RFC 9051}) and JMAP
1111+ ({{:https://datatracker.ietf.org/doc/html/rfc8621}RFC 8621}) protocols.
1212+1313+ {2 Overview}
1414+1515+ The library defines three main concepts:
1616+1717+ - {!Keyword}: Message keywords/flags like [`Seen], [`Flagged], [`Junk]
1818+ - {!Mailbox_attr}: Mailbox attributes and special-use roles like [`Drafts], [`Inbox]
1919+ - {!Flag_color}: Apple Mail flag color encoding
2020+2121+ All types use polymorphic variants for:
2222+ - Type safety: The compiler catches invalid flag combinations
2323+ - Extensibility: Custom flags via [`Custom] and [`Extension] variants
2424+ - Interoperability: Easy conversion between protocol representations
2525+2626+ {2 Protocol Mapping}
2727+2828+ {b IMAP system flags} ([\Seen], [\Answered], etc.) map to {!standard} keywords.
2929+ Use {!Keyword.to_imap_string} for wire format conversion.
3030+3131+ {b JMAP keywords} ([$seen], [$answered], etc.) are the canonical form.
3232+ Use {!Keyword.to_string} for JMAP format.
3333+3434+ {b Mailbox roles} work similarly with {!Mailbox_attr.to_string} for IMAP
3535+ and {!Mailbox_attr.to_jmap_role} for JMAP.
3636+3737+ {2 References}
3838+3939+ - {{:https://www.rfc-editor.org/rfc/rfc9051}RFC 9051} - IMAP4rev2
4040+ - {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621} - JMAP for Mail
4141+ - {{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154} - IMAP Special-Use Mailboxes
4242+ - {{:https://datatracker.ietf.org/doc/draft-ietf-mailmaint-messageflag-mailboxattribute}
4343+ draft-ietf-mailmaint} - Extended keywords and attributes *)
4444+4545+(** {1 Modules} *)
4646+4747+(** Message keywords for both IMAP and JMAP.
4848+4949+ See {!module:Keyword} for the full API. *)
5050+module Keyword : sig
5151+ (** {1 Keyword Types} *)
5252+5353+ (** Standard keywords per RFC 8621 Section 4.1.1 that map to IMAP system flags. *)
5454+ type standard = [
5555+ | `Seen (** Message has been read. Maps to IMAP [\Seen]. *)
5656+ | `Answered (** Message has been answered. Maps to IMAP [\Answered]. *)
5757+ | `Flagged (** Message is flagged/starred. Maps to IMAP [\Flagged]. *)
5858+ | `Draft (** Message is a draft. Maps to IMAP [\Draft]. *)
5959+ | `Deleted (** Message marked for deletion. Maps to IMAP [\Deleted]. *)
6060+ | `Forwarded (** Message has been forwarded. JMAP [$forwarded]. *)
6161+ ]
6262+6363+ (** Spam-related keywords for junk mail handling. *)
6464+ type spam = [
6565+ | `Phishing (** Message is a phishing attempt. *)
6666+ | `Junk (** Message is spam/junk. *)
6767+ | `NotJunk (** Message explicitly marked as not junk. *)
6868+ ]
6969+7070+ (** Extended keywords per draft-ietf-mailmaint. *)
7171+ type extended = [
7272+ | `HasAttachment (** Message has attachments. *)
7373+ | `HasNoAttachment (** Message has no attachments. *)
7474+ | `Memo (** Message is a memo. *)
7575+ | `HasMemo (** Message has an associated memo. *)
7676+ | `CanUnsubscribe (** Message has unsubscribe capability. *)
7777+ | `Unsubscribed (** User has unsubscribed from this sender. *)
7878+ | `Muted (** Thread is muted. *)
7979+ | `Followed (** Thread is followed. *)
8080+ | `AutoSent (** Message was sent automatically. *)
8181+ | `Imported (** Message was imported from another source. *)
8282+ | `IsTrusted (** Sender is trusted. *)
8383+ | `MaskedEmail (** Message was sent to a masked email address. *)
8484+ | `New (** Message is new (not yet processed). *)
8585+ | `Notify (** User should be notified about this message. *)
8686+ ]
8787+8888+ (** Apple Mail flag color bits. *)
8989+ type flag_bit = [
9090+ | `MailFlagBit0 (** Bit 0 of Apple Mail flag color encoding. *)
9191+ | `MailFlagBit1 (** Bit 1 of Apple Mail flag color encoding. *)
9292+ | `MailFlagBit2 (** Bit 2 of Apple Mail flag color encoding. *)
9393+ ]
9494+9595+ (** Unified keyword type combining all categories. *)
9696+ type t = [ standard | spam | extended | flag_bit | `Custom of string ]
9797+9898+ (** {1 Conversion Functions} *)
9999+100100+ val of_string : string -> t
101101+ (** [of_string s] parses a keyword string.
102102+ Handles JMAP format ([$seen]), IMAP format ([\Seen]), and bare format ([seen]).
103103+ Unknown keywords become [`Custom]. *)
104104+105105+ val to_string : t -> string
106106+ (** [to_string k] converts a keyword to canonical JMAP format (e.g., ["$seen"]). *)
107107+108108+ val to_imap_string : t -> string
109109+ (** [to_imap_string k] converts a keyword to IMAP wire format.
110110+ Standard keywords use backslash ([\Seen]), others use dollar ([$Forwarded]). *)
111111+112112+ (** {1 Predicates} *)
113113+114114+ val is_standard : t -> bool
115115+ (** [is_standard k] returns [true] if [k] maps to an IMAP system flag. *)
116116+117117+ val is_mutually_exclusive : t -> t -> bool
118118+ (** [is_mutually_exclusive k1 k2] returns [true] if keywords cannot both be set.
119119+ Mutually exclusive pairs: HasAttachment/HasNoAttachment, Junk/NotJunk, Muted/Followed. *)
120120+121121+ (** {1 Comparison and Pretty Printing} *)
122122+123123+ val equal : t -> t -> bool
124124+ val compare : t -> t -> int
125125+ val pp : Format.formatter -> t -> unit
126126+127127+ (** {1 Apple Mail Flag Colors} *)
128128+129129+ type flag_color = [
130130+ | `Red | `Orange | `Yellow | `Green | `Blue | `Purple | `Gray
131131+ ]
132132+133133+ val flag_color_of_keywords : t list -> flag_color option
134134+ (** Extract Apple Mail flag color from keywords. Returns [None] for invalid encoding. *)
135135+136136+ val flag_color_to_keywords : flag_color -> t list
137137+ (** Convert flag color to the keyword bits needed to represent it. *)
138138+end
139139+140140+(** Mailbox attributes and special-use roles.
141141+142142+ See {!module:Mailbox_attr} for the full API. *)
143143+module Mailbox_attr : sig
144144+ (** {1 Attribute Types} *)
145145+146146+ (** IMAP LIST response attributes per RFC 9051 Section 7.2.2. *)
147147+ type list_attr = [
148148+ | `Noinferiors (** No child mailboxes possible. *)
149149+ | `Noselect (** Mailbox cannot be selected. *)
150150+ | `Marked (** Mailbox has new messages. *)
151151+ | `Unmarked (** Mailbox has no new messages. *)
152152+ | `Subscribed (** Mailbox is subscribed. *)
153153+ | `HasChildren (** Mailbox has child mailboxes. *)
154154+ | `HasNoChildren (** Mailbox has no children. *)
155155+ | `NonExistent (** Mailbox does not exist. *)
156156+ | `Remote (** Mailbox is on a remote server. *)
157157+ ]
158158+159159+ (** Special-use mailbox roles per RFC 6154 and RFC 8621. *)
160160+ type special_use = [
161161+ | `All (** Virtual mailbox with all messages. *)
162162+ | `Archive (** Archive mailbox. *)
163163+ | `Drafts (** Drafts mailbox. *)
164164+ | `Flagged (** Virtual mailbox with flagged messages. *)
165165+ | `Important (** Important messages mailbox. *)
166166+ | `Inbox (** User's inbox. *)
167167+ | `Junk (** Spam/junk mailbox. *)
168168+ | `Sent (** Sent messages mailbox. *)
169169+ | `Subscribed (** JMAP virtual subscribed mailbox. *)
170170+ | `Trash (** Trash/deleted messages mailbox. *)
171171+ | `Snoozed (** Snoozed messages (draft-ietf-mailmaint). *)
172172+ | `Scheduled (** Scheduled to send (draft-ietf-mailmaint). *)
173173+ | `Memos (** Memo messages (draft-ietf-mailmaint). *)
174174+ ]
175175+176176+ (** Unified mailbox attribute type. *)
177177+ type t = [ list_attr | special_use | `Extension of string ]
178178+179179+ (** {1 Conversion Functions} *)
180180+181181+ val of_string : string -> t
182182+ (** [of_string s] parses a mailbox attribute from IMAP wire format.
183183+ Unknown attributes become [`Extension]. *)
184184+185185+ val to_string : t -> string
186186+ (** [to_string attr] converts to IMAP wire format with backslash prefix. *)
187187+188188+ val to_jmap_role : t -> string option
189189+ (** [to_jmap_role attr] converts to JMAP role string (lowercase).
190190+ Returns [None] for LIST attributes without JMAP equivalents. *)
191191+192192+ val of_jmap_role : string -> special_use option
193193+ (** [of_jmap_role s] parses a JMAP role string into a special-use attribute. *)
194194+195195+ (** {1 Predicates} *)
196196+197197+ val is_special_use : t -> bool
198198+ (** [is_special_use attr] returns [true] if attribute is a special-use role. *)
199199+200200+ val is_selectable : t -> bool
201201+ (** [is_selectable attr] returns [false] for Noselect and NonExistent. *)
202202+203203+ (** {1 Pretty Printing} *)
204204+205205+ val pp : Format.formatter -> t -> unit
206206+end
207207+208208+(** Apple Mail flag colors.
209209+210210+ See {!module:Flag_color} for the full API. *)
211211+module Flag_color : sig
212212+ (** Flag colors encoded as 3-bit values using [$MailFlagBit*] keywords. *)
213213+ type t = [
214214+ | `Red (** Bit pattern: 000 *)
215215+ | `Orange (** Bit pattern: 100 *)
216216+ | `Yellow (** Bit pattern: 010 *)
217217+ | `Green (** Bit pattern: 110 *)
218218+ | `Blue (** Bit pattern: 001 *)
219219+ | `Purple (** Bit pattern: 101 *)
220220+ | `Gray (** Bit pattern: 011 *)
221221+ ]
222222+223223+ (** {1 Bit Pattern Conversion} *)
224224+225225+ val to_bits : t -> bool * bool * bool
226226+ (** [to_bits color] returns [(bit0, bit1, bit2)] tuple. *)
227227+228228+ val of_bits : bool * bool * bool -> t option
229229+ (** [of_bits (b0, b1, b2)] converts bit pattern to color.
230230+ Returns [None] for undefined pattern (true, true, true). *)
231231+232232+ (** {1 Keyword Conversion} *)
233233+234234+ val to_keywords : t -> [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list
235235+ (** [to_keywords color] returns keyword bits for the color. *)
236236+237237+ val of_keywords : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
238238+ (** [of_keywords kws] extracts color from keyword bits.
239239+ Returns [None] if no bits set (ambiguous) or pattern is 111 (undefined). *)
240240+241241+ val of_keywords_default_red : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
242242+ (** Like {!of_keywords} but treats empty list as Red. *)
243243+244244+ (** {1 String Conversion} *)
245245+246246+ val to_string : t -> string
247247+ (** [to_string color] returns lowercase color name. *)
248248+249249+ val of_string : string -> t option
250250+ (** [of_string s] parses color name (case-insensitive, accepts "grey"). *)
251251+252252+ (** {1 Pretty Printing} *)
253253+254254+ val pp : Format.formatter -> t -> unit
255255+end
256256+257257+(** {1 Type Aliases}
258258+259259+ Convenient type aliases for use without module qualification. *)
260260+261261+(** Standard message keywords that map to IMAP system flags. *)
262262+type standard = Keyword.standard
263263+264264+(** Spam-related keywords. *)
265265+type spam = Keyword.spam
266266+267267+(** Extended keywords from draft-ietf-mailmaint. *)
268268+type extended = Keyword.extended
269269+270270+(** Apple Mail flag color bit keywords. *)
271271+type flag_bit = Keyword.flag_bit
272272+273273+(** Unified message keyword type. *)
274274+type keyword = Keyword.t
275275+276276+(** IMAP LIST response attributes. *)
277277+type list_attr = Mailbox_attr.list_attr
278278+279279+(** Special-use mailbox roles. *)
280280+type special_use = Mailbox_attr.special_use
281281+282282+(** Unified mailbox attribute type. *)
283283+type mailbox_attr = Mailbox_attr.t
284284+285285+(** Apple Mail flag colors. *)
286286+type flag_color = Flag_color.t
+149
lib/mailbox_attr.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Implementation of unified mailbox attributes and roles. *)
77+88+type list_attr = [
99+ | `Noinferiors
1010+ | `Noselect
1111+ | `Marked
1212+ | `Unmarked
1313+ | `Subscribed
1414+ | `HasChildren
1515+ | `HasNoChildren
1616+ | `NonExistent
1717+ | `Remote
1818+]
1919+2020+type special_use = [
2121+ | `All
2222+ | `Archive
2323+ | `Drafts
2424+ | `Flagged
2525+ | `Important
2626+ | `Inbox
2727+ | `Junk
2828+ | `Sent
2929+ | `Subscribed
3030+ | `Trash
3131+ | `Snoozed
3232+ | `Scheduled
3333+ | `Memos
3434+]
3535+3636+type t = [ list_attr | special_use | `Extension of string ]
3737+3838+(** Normalize attribute string by removing backslash prefix and converting to lowercase. *)
3939+let normalize s =
4040+ let s = String.lowercase_ascii s in
4141+ if String.length s > 0 && s.[0] = '\\' then
4242+ String.sub s 1 (String.length s - 1)
4343+ else
4444+ s
4545+4646+let of_string s =
4747+ match normalize s with
4848+ (* LIST attributes *)
4949+ | "noinferiors" -> `Noinferiors
5050+ | "noselect" -> `Noselect
5151+ | "marked" -> `Marked
5252+ | "unmarked" -> `Unmarked
5353+ | "subscribed" -> `Subscribed
5454+ | "haschildren" -> `HasChildren
5555+ | "hasnochildren" -> `HasNoChildren
5656+ | "nonexistent" -> `NonExistent
5757+ | "remote" -> `Remote
5858+ (* Special-use roles *)
5959+ | "all" -> `All
6060+ | "archive" -> `Archive
6161+ | "drafts" -> `Drafts
6262+ | "flagged" -> `Flagged
6363+ | "important" -> `Important
6464+ | "inbox" -> `Inbox
6565+ | "junk" | "spam" -> `Junk
6666+ | "sent" -> `Sent
6767+ | "trash" -> `Trash
6868+ | "snoozed" -> `Snoozed
6969+ | "scheduled" -> `Scheduled
7070+ | "memos" -> `Memos
7171+ | other -> `Extension other
7272+7373+let to_string = function
7474+ (* LIST attributes *)
7575+ | `Noinferiors -> "\\Noinferiors"
7676+ | `Noselect -> "\\Noselect"
7777+ | `Marked -> "\\Marked"
7878+ | `Unmarked -> "\\Unmarked"
7979+ | `Subscribed -> "\\Subscribed"
8080+ | `HasChildren -> "\\HasChildren"
8181+ | `HasNoChildren -> "\\HasNoChildren"
8282+ | `NonExistent -> "\\NonExistent"
8383+ | `Remote -> "\\Remote"
8484+ (* Special-use roles *)
8585+ | `All -> "\\All"
8686+ | `Archive -> "\\Archive"
8787+ | `Drafts -> "\\Drafts"
8888+ | `Flagged -> "\\Flagged"
8989+ | `Important -> "\\Important"
9090+ | `Inbox -> "\\Inbox"
9191+ | `Junk -> "\\Junk"
9292+ | `Sent -> "\\Sent"
9393+ | `Trash -> "\\Trash"
9494+ | `Snoozed -> "\\Snoozed"
9595+ | `Scheduled -> "\\Scheduled"
9696+ | `Memos -> "\\Memos"
9797+ | `Extension s ->
9898+ if String.length s > 0 && s.[0] = '\\' then s
9999+ else "\\" ^ s
100100+101101+let to_jmap_role = function
102102+ (* Special-use roles have JMAP equivalents *)
103103+ | `All -> Some "all"
104104+ | `Archive -> Some "archive"
105105+ | `Drafts -> Some "drafts"
106106+ | `Flagged -> Some "flagged"
107107+ | `Important -> Some "important"
108108+ | `Inbox -> Some "inbox"
109109+ | `Junk -> Some "junk"
110110+ | `Sent -> Some "sent"
111111+ | `Trash -> Some "trash"
112112+ | `Snoozed -> Some "snoozed"
113113+ | `Scheduled -> Some "scheduled"
114114+ | `Memos -> Some "memos"
115115+ (* LIST attributes and extensions have no JMAP role *)
116116+ | `Noinferiors | `Noselect | `Marked | `Unmarked | `Subscribed
117117+ | `HasChildren | `HasNoChildren | `NonExistent | `Remote
118118+ | `Extension _ -> None
119119+120120+let of_jmap_role s =
121121+ match String.lowercase_ascii s with
122122+ | "all" -> Some `All
123123+ | "archive" -> Some `Archive
124124+ | "drafts" -> Some `Drafts
125125+ | "flagged" -> Some `Flagged
126126+ | "important" -> Some `Important
127127+ | "inbox" -> Some `Inbox
128128+ | "junk" -> Some `Junk
129129+ | "sent" -> Some `Sent
130130+ | "trash" -> Some `Trash
131131+ | "snoozed" -> Some `Snoozed
132132+ | "scheduled" -> Some `Scheduled
133133+ | "memos" -> Some `Memos
134134+ | "subscribed" -> Some `Subscribed
135135+ | _ -> None
136136+137137+let is_special_use = function
138138+ | `All | `Archive | `Drafts | `Flagged | `Important | `Inbox
139139+ | `Junk | `Sent | `Trash | `Snoozed | `Scheduled | `Memos -> true
140140+ | `Subscribed -> true (* Also a JMAP role *)
141141+ | `Noinferiors | `Noselect | `Marked | `Unmarked
142142+ | `HasChildren | `HasNoChildren | `NonExistent | `Remote
143143+ | `Extension _ -> false
144144+145145+let is_selectable = function
146146+ | `Noselect | `NonExistent -> false
147147+ | _ -> true
148148+149149+let pp ppf attr = Fmt.string ppf (to_string attr)
+188
lib/mailbox_attr.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Unified Mailbox Attributes and Roles
77+88+ This module provides a unified representation of mailbox attributes
99+ across IMAP and JMAP protocols. It combines IMAP LIST response attributes
1010+ ({{:https://www.rfc-editor.org/rfc/rfc9051#section-7.2.2}RFC 9051 Section 7.2.2}),
1111+ special-use mailbox flags ({{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154}),
1212+ and JMAP mailbox roles ({{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}).
1313+1414+ {2 References}
1515+ - {{:https://www.rfc-editor.org/rfc/rfc9051}RFC 9051} - IMAP4rev2
1616+ - {{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154} - IMAP LIST Extension for Special-Use Mailboxes
1717+ - {{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258} - IMAP4 LIST Command Extensions
1818+ - {{:https://www.rfc-editor.org/rfc/rfc8457}RFC 8457} - IMAP \$Important Keyword and \Important Special-Use Attribute
1919+ - {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621} - JMAP for Mail *)
2020+2121+(** {1 IMAP LIST Attributes}
2222+2323+ Attributes returned in IMAP LIST responses per
2424+ {{:https://www.rfc-editor.org/rfc/rfc9051#section-7.2.2}RFC 9051 Section 7.2.2}. *)
2525+2626+type list_attr = [
2727+ | `Noinferiors
2828+ (** [\Noinferiors] - No child mailboxes are possible under this mailbox.
2929+ The mailbox cannot have inferior (child) mailboxes, either because the
3030+ underlying storage doesn't support it or because the mailbox name is at
3131+ the hierarchy depth limit for this mailbox store. *)
3232+ | `Noselect
3333+ (** [\Noselect] - This mailbox cannot be selected. It exists only to hold
3434+ child mailboxes and is not a valid destination for messages. Stratum
3535+ only, not a real mailbox. *)
3636+ | `Marked
3737+ (** [\Marked] - The mailbox has been marked "interesting" by the server.
3838+ This typically indicates the mailbox contains new messages since the
3939+ last time it was selected. *)
4040+ | `Unmarked
4141+ (** [\Unmarked] - The mailbox is not "interesting". The mailbox does not
4242+ contain new messages since the last time it was selected. *)
4343+ | `Subscribed
4444+ (** [\Subscribed] - The mailbox is subscribed. Returned when the
4545+ SUBSCRIBED selection option is specified or implied. *)
4646+ | `HasChildren
4747+ (** [\HasChildren] - The mailbox has child mailboxes. Part of the
4848+ CHILDREN return option ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}). *)
4949+ | `HasNoChildren
5050+ (** [\HasNoChildren] - The mailbox has no child mailboxes. Part of the
5151+ CHILDREN return option ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}). *)
5252+ | `NonExistent
5353+ (** [\NonExistent] - The mailbox name does not refer to an existing mailbox.
5454+ This attribute is returned when a mailbox is part of the hierarchy but
5555+ doesn't actually exist ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}).
5656+ Implies [\Noselect]. *)
5757+ | `Remote
5858+ (** [\Remote] - The mailbox is located on a remote server.
5959+ ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}) *)
6060+]
6161+6262+(** {1 Special-Use Roles}
6363+6464+ Special-use mailbox roles per {{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154}
6565+ and {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}. These identify
6666+ mailboxes with specific purposes. *)
6767+6868+type special_use = [
6969+ | `All
7070+ (** [\All] - A virtual mailbox containing all messages in the user's
7171+ message store. Implementations may omit some messages. *)
7272+ | `Archive
7373+ (** [\Archive] - A mailbox used to archive messages. The meaning of
7474+ "archived" may vary by server. *)
7575+ | `Drafts
7676+ (** [\Drafts] - A mailbox used to hold draft messages, typically messages
7777+ being composed but not yet sent. *)
7878+ | `Flagged
7979+ (** [\Flagged] - A virtual mailbox containing all messages marked with
8080+ the [\Flagged] flag. *)
8181+ | `Important
8282+ (** [\Important] - A mailbox used to hold messages deemed important to
8383+ the user. ({{:https://www.rfc-editor.org/rfc/rfc8457}RFC 8457}) *)
8484+ | `Inbox
8585+ (** [inbox] - The user's inbox. This is a JMAP role
8686+ ({{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}) without a direct
8787+ IMAP special-use equivalent since INBOX is always special in IMAP. *)
8888+ | `Junk
8989+ (** [\Junk] - A mailbox used to hold messages that have been identified
9090+ as spam or junk mail. Also known as "Spam" folder. *)
9191+ | `Sent
9292+ (** [\Sent] - A mailbox used to hold copies of messages that have been
9393+ sent. *)
9494+ | `Subscribed
9595+ (** [subscribed] - A JMAP virtual mailbox role
9696+ ({{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}) representing
9797+ all subscribed mailboxes. *)
9898+ | `Trash
9999+ (** [\Trash] - A mailbox used to hold messages that have been deleted or
100100+ marked for deletion. *)
101101+ | `Snoozed
102102+ (** [snoozed] - A mailbox for messages that have been snoozed until a
103103+ later time. (draft-ietf-mailmaint-special-use-extensions) *)
104104+ | `Scheduled
105105+ (** [scheduled] - A mailbox for messages scheduled to be sent at a
106106+ future time. (draft-ietf-mailmaint-special-use-extensions) *)
107107+ | `Memos
108108+ (** [memos] - A mailbox for memo/note messages.
109109+ (draft-ietf-mailmaint-special-use-extensions) *)
110110+]
111111+112112+(** {1 Unified Attribute Type} *)
113113+114114+type t = [ list_attr | special_use | `Extension of string ]
115115+(** The unified mailbox attribute type combining LIST attributes, special-use
116116+ roles, and server-specific extensions. Extensions are represented with
117117+ their original string form (without leading backslash if present). *)
118118+119119+(** {1 Conversion Functions} *)
120120+121121+val of_string : string -> t
122122+(** [of_string s] parses a mailbox attribute from its IMAP wire format.
123123+ The input may optionally include the leading backslash. Parsing is
124124+ case-insensitive. Unknown attributes are returned as [`Extension s].
125125+126126+ Examples:
127127+ - [of_string "\\Drafts"] returns [`Drafts]
128128+ - [of_string "drafts"] returns [`Drafts]
129129+ - [of_string "\\X-Custom"] returns [`Extension "X-Custom"] *)
130130+131131+val to_string : t -> string
132132+(** [to_string attr] converts an attribute to its IMAP wire format with
133133+ the leading backslash prefix for standard attributes.
134134+135135+ Examples:
136136+ - [to_string `Drafts] returns ["\\Drafts"]
137137+ - [to_string `HasChildren] returns ["\\HasChildren"]
138138+ - [to_string (`Extension "X-Custom")] returns ["\\X-Custom"] *)
139139+140140+val to_jmap_role : t -> string option
141141+(** [to_jmap_role attr] converts a special-use attribute to its JMAP role
142142+ string (lowercase). Returns [None] for LIST attributes that don't
143143+ correspond to JMAP roles.
144144+145145+ Examples:
146146+ - [to_jmap_role `Drafts] returns [Some "drafts"]
147147+ - [to_jmap_role `Inbox] returns [Some "inbox"]
148148+ - [to_jmap_role `Noselect] returns [None] *)
149149+150150+val of_jmap_role : string -> special_use option
151151+(** [of_jmap_role s] parses a JMAP role string into a special-use attribute.
152152+ Returns [None] if the role string is not recognized. The input should
153153+ be lowercase as per JMAP conventions.
154154+155155+ Examples:
156156+ - [of_jmap_role "drafts"] returns [Some `Drafts]
157157+ - [of_jmap_role "inbox"] returns [Some `Inbox]
158158+ - [of_jmap_role "unknown"] returns [None] *)
159159+160160+(** {1 Predicates} *)
161161+162162+val is_special_use : t -> bool
163163+(** [is_special_use attr] returns [true] if the attribute is a special-use
164164+ role (as opposed to a LIST attribute or extension).
165165+166166+ Examples:
167167+ - [is_special_use `Drafts] returns [true]
168168+ - [is_special_use `Noselect] returns [false]
169169+ - [is_special_use (`Extension "x")] returns [false] *)
170170+171171+val is_selectable : t -> bool
172172+(** [is_selectable attr] returns [false] if the attribute indicates the
173173+ mailbox cannot be selected. This is [true] for [`Noselect] and
174174+ [`NonExistent] attributes, and [false] for all others.
175175+176176+ Note: A mailbox may have multiple attributes. To determine if a mailbox
177177+ is selectable, check that no attribute returns [false] from this function.
178178+179179+ Examples:
180180+ - [is_selectable `Noselect] returns [false]
181181+ - [is_selectable `NonExistent] returns [false]
182182+ - [is_selectable `Drafts] returns [true]
183183+ - [is_selectable `HasChildren] returns [true] *)
184184+185185+(** {1 Pretty Printing} *)
186186+187187+val pp : Format.formatter -> t -> unit
188188+(** [pp ppf attr] pretty-prints the attribute in IMAP wire format. *)