···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** Apple Mail flag colors.
7+8+ This module implements the Apple Mail flag color encoding using the
9+ [$MailFlagBit0], [$MailFlagBit1], and [$MailFlagBit2] keywords.
10+11+ See {{:https://datatracker.ietf.org/doc/draft-ietf-mailmaint-messageflag-mailboxattribute#section-3}
12+ draft-ietf-mailmaint-messageflag-mailboxattribute Section 3}.
13+14+ Colors are encoded as a 3-bit pattern where each bit corresponds to
15+ a keyword:
16+ - Bit 0: [$MailFlagBit0]
17+ - Bit 1: [$MailFlagBit1]
18+ - Bit 2: [$MailFlagBit2]
19+20+ The bit patterns are:
21+ - Red: 000 (no bits set)
22+ - Orange: 100 (bit 0 only)
23+ - Yellow: 010 (bit 1 only)
24+ - Green: 110 (bits 0 and 1)
25+ - Blue: 001 (bit 2 only)
26+ - Purple: 101 (bits 0 and 2)
27+ - Gray: 011 (bits 1 and 2)
28+ - 111: undefined (all bits set) *)
29+30+type t =
31+ | Red (** Bit pattern: 000 *)
32+ | Orange (** Bit pattern: 100 *)
33+ | Yellow (** Bit pattern: 010 *)
34+ | Green (** Bit pattern: 110 *)
35+ | Blue (** Bit pattern: 001 *)
36+ | Purple (** Bit pattern: 101 *)
37+ | Gray (** Bit pattern: 011 *)
38+39+val to_bits : t -> bool * bool * bool
40+(** [to_bits color] converts [color] to a [(bit0, bit1, bit2)] tuple
41+ representing which [$MailFlagBit*] keywords should be set.
42+43+ Example: [to_bits Green] returns [(true, true, false)]. *)
44+45+val of_bits : bool * bool * bool -> t option
46+(** [of_bits (bit0, bit1, bit2)] converts a bit pattern to a color.
47+ Returns [None] for the undefined pattern [(true, true, true)] (111). *)
48+49+val to_keywords : t -> [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list
50+(** [to_keywords color] returns the list of keyword bits that should be
51+ set for [color]. Red returns an empty list since no bits are needed. *)
52+53+val of_keywords : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
54+(** [of_keywords keywords] extracts a color from a list of keyword bits.
55+ Returns [None] if the pattern is 111 (undefined) or if no bits are
56+ present in the list (which would indicate no flag color is set,
57+ rather than Red). Use {!of_keywords_default_red} if you want to
58+ treat an empty list as Red. *)
59+60+val of_keywords_default_red : [ `MailFlagBit0 | `MailFlagBit1 | `MailFlagBit2 ] list -> t option
61+(** [of_keywords_default_red keywords] is like {!of_keywords} but treats
62+ an empty keyword list as Red. Returns [None] only for pattern 111. *)
63+64+val pp : Format.formatter -> t -> unit
65+(** [pp ppf color] pretty-prints the color name to [ppf]. *)
66+67+val to_string : t -> string
68+(** [to_string color] returns the lowercase color name. *)
69+70+val of_string : string -> t option
71+(** [of_string s] parses a color name (case-insensitive).
72+ Returns [None] if [s] is not a valid color name. *)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** IMAP Wire Format Conversion
7+8+ Implementation of IMAP wire format conversion for message flags and
9+ mailbox attributes. See {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}. *)
10+11+type flag =
12+ | System of [ `Seen | `Answered | `Flagged | `Deleted | `Draft ]
13+ | Keyword of Keyword.t
14+15+(** Check if a string represents an IMAP system flag.
16+ Returns the system flag variant if recognized, None otherwise. *)
17+let parse_system_flag s =
18+ let s = String.lowercase_ascii s in
19+ (* Remove backslash prefix if present *)
20+ let s = if String.length s > 0 && s.[0] = '\\' then
21+ String.sub s 1 (String.length s - 1)
22+ else s
23+ in
24+ match s with
25+ | "seen" -> Some `Seen
26+ | "answered" -> Some `Answered
27+ | "flagged" -> Some `Flagged
28+ | "deleted" -> Some `Deleted
29+ | "draft" -> Some `Draft
30+ | _ -> None
31+32+let flag_of_string s =
33+ match parse_system_flag s with
34+ | Some sys -> System sys
35+ | None -> Keyword (Keyword.of_string s)
36+37+let flag_to_string = function
38+ | System `Seen -> "\\Seen"
39+ | System `Answered -> "\\Answered"
40+ | System `Flagged -> "\\Flagged"
41+ | System `Deleted -> "\\Deleted"
42+ | System `Draft -> "\\Draft"
43+ | Keyword k -> Keyword.to_imap_string k
44+45+let flags_of_keywords keywords =
46+ List.map (fun k ->
47+ match k with
48+ | `Seen -> System `Seen
49+ | `Answered -> System `Answered
50+ | `Flagged -> System `Flagged
51+ | `Deleted -> System `Deleted
52+ | `Draft -> System `Draft
53+ | other -> Keyword other
54+ ) keywords
55+56+let keywords_of_flags flags =
57+ List.map (fun flag ->
58+ match flag with
59+ | System `Seen -> `Seen
60+ | System `Answered -> `Answered
61+ | System `Flagged -> `Flagged
62+ | System `Deleted -> `Deleted
63+ | System `Draft -> `Draft
64+ | Keyword k -> k
65+ ) flags
66+67+let attr_of_string = Mailbox_attr.of_string
68+let attr_to_string = Mailbox_attr.to_string
69+70+let pp_flag ppf flag = Fmt.string ppf (flag_to_string flag)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** IMAP Wire Format Conversion
7+8+ Converts between mail-flag types and IMAP protocol format.
9+ See {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}
10+ for the flag format specification.
11+12+ {2 IMAP Flag Format}
13+14+ IMAP uses two types of message flags:
15+ - {b System flags} prefixed with backslash: [\Seen], [\Answered], [\Flagged], [\Deleted], [\Draft]
16+ - {b Keywords} prefixed with dollar sign: [$Forwarded], [$Junk], etc.
17+18+ This module handles the conversion between the internal {!Keyword.t} representation
19+ and the IMAP wire format. *)
20+21+(** {1 Message Flags} *)
22+23+(** IMAP message flag - either system flag or keyword.
24+25+ System flags are the five flags defined in
26+ {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}:
27+ [\Seen], [\Answered], [\Flagged], [\Deleted], [\Draft].
28+29+ Keywords are user-defined or server-defined flags that start with [$]. *)
30+type flag =
31+ | System of [ `Seen | `Answered | `Flagged | `Deleted | `Draft ]
32+ | Keyword of Keyword.t
33+34+val flag_of_string : string -> flag
35+(** [flag_of_string s] parses an IMAP flag string.
36+37+ System flags are recognized with or without the backslash prefix,
38+ case-insensitively. Keywords are parsed using {!Keyword.of_string}.
39+40+ Examples:
41+ - ["\\Seen"] -> [System `Seen]
42+ - ["Seen"] -> [System `Seen]
43+ - ["$forwarded"] -> [Keyword `Forwarded]
44+ - ["$custom"] -> [Keyword (`Custom "custom")] *)
45+46+val flag_to_string : flag -> string
47+(** [flag_to_string flag] converts a flag to IMAP wire format.
48+49+ System flags are returned with backslash prefix.
50+ Keywords are returned with dollar sign prefix.
51+52+ Examples:
53+ - [System `Seen] -> ["\\Seen"]
54+ - [Keyword `Forwarded] -> ["$Forwarded"] *)
55+56+val flags_of_keywords : Keyword.t list -> flag list
57+(** [flags_of_keywords keywords] converts a list of keywords to IMAP flags.
58+59+ Keywords that correspond to IMAP system flags ([`Seen], [`Answered],
60+ [`Flagged], [`Deleted], [`Draft]) are converted to [System] flags.
61+ All other keywords remain as [Keyword] flags.
62+63+ Example:
64+ {[
65+ flags_of_keywords [`Seen; `Forwarded; `Custom "label"]
66+ (* returns [System `Seen; Keyword `Forwarded; Keyword (`Custom "label")] *)
67+ ]} *)
68+69+val keywords_of_flags : flag list -> Keyword.t list
70+(** [keywords_of_flags flags] converts IMAP flags to keywords.
71+72+ System flags are converted to their corresponding standard keywords.
73+ Keyword flags are returned as-is.
74+75+ Example:
76+ {[
77+ keywords_of_flags [System `Seen; Keyword `Forwarded]
78+ (* returns [`Seen; `Forwarded] *)
79+ ]} *)
80+81+(** {1 Mailbox Attributes} *)
82+83+val attr_of_string : string -> Mailbox_attr.t
84+(** [attr_of_string s] parses an IMAP mailbox attribute.
85+86+ Delegates to {!Mailbox_attr.of_string}. The input may optionally
87+ include the leading backslash. Parsing is case-insensitive.
88+89+ Examples:
90+ - ["\\Drafts"] -> [`Drafts]
91+ - ["HasChildren"] -> [`HasChildren] *)
92+93+val attr_to_string : Mailbox_attr.t -> string
94+(** [attr_to_string attr] converts an attribute to IMAP wire format.
95+96+ Delegates to {!Mailbox_attr.to_string}. Returns the attribute with
97+ leading backslash prefix.
98+99+ Examples:
100+ - [`Drafts] -> ["\\Drafts"]
101+ - [`HasChildren] -> ["\\HasChildren"] *)
102+103+(** {1 Pretty Printing} *)
104+105+val pp_flag : Format.formatter -> flag -> unit
106+(** [pp_flag ppf flag] pretty-prints a flag in IMAP wire format. *)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** JMAP Wire Format Conversion
7+8+ Converts between mail-flag types and JMAP JSON format.
9+ See {{:https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1}RFC 8621 Section 4.1.1}
10+ for the keywords format specification.
11+12+ {2 JMAP Keywords Format}
13+14+ In JMAP, message keywords are represented as a JSON object where each key
15+ is a keyword string (with [$] prefix for standard keywords) and the value
16+ is always [true]:
17+18+ {v
19+ {
20+ "$seen": true,
21+ "$flagged": true,
22+ "$forwarded": true,
23+ "my-custom-label": true
24+ }
25+ v}
26+27+ Keywords with [false] values are simply absent from the object. This module
28+ provides conversion functions between the internal {!Keyword.t} list
29+ representation and the association list format used for JSON encoding. *)
30+31+(** {1 Keywords as JSON} *)
32+33+val keywords_to_assoc : Keyword.t list -> (string * bool) list
34+(** [keywords_to_assoc keywords] converts a keyword list to JMAP keywords
35+ object entries.
36+37+ Each keyword is converted to a [(string, true)] pair using
38+ {!Keyword.to_string} for the string representation.
39+40+ Example:
41+ {[
42+ keywords_to_assoc [`Seen; `Flagged; `Custom "label"]
43+ (* returns [("$seen", true); ("$flagged", true); ("label", true)] *)
44+ ]} *)
45+46+val keywords_of_assoc : (string * bool) list -> Keyword.t list
47+(** [keywords_of_assoc assoc] parses JMAP keywords from object entries.
48+49+ Only entries with [true] value are included in the result.
50+ Entries with [false] value are ignored (they represent the absence
51+ of the keyword).
52+53+ Example:
54+ {[
55+ keywords_of_assoc [("$seen", true); ("$draft", false); ("label", true)]
56+ (* returns [`Seen; `Custom "label"] *)
57+ ]} *)
58+59+(** {1 Mailbox Roles} *)
60+61+val role_to_string : Mailbox_attr.special_use -> string
62+(** [role_to_string role] converts a special-use attribute to JMAP role string.
63+64+ JMAP roles are lowercase strings without any prefix.
65+66+ Examples:
67+ - [`Drafts] -> ["drafts"]
68+ - [`Inbox] -> ["inbox"]
69+ - [`Junk] -> ["junk"] *)
70+71+val role_of_string : string -> Mailbox_attr.special_use option
72+(** [role_of_string s] parses a JMAP role string into a special-use attribute.
73+74+ Returns [None] if the role string is not recognized. The input should
75+ be lowercase as per JMAP conventions, but parsing is case-insensitive.
76+77+ Examples:
78+ - ["drafts"] -> [Some `Drafts]
79+ - ["inbox"] -> [Some `Inbox]
80+ - ["unknown"] -> [None] *)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** Unified Message Keywords for IMAP and JMAP
7+8+ This module provides a unified representation of message keywords that
9+ works across both IMAP ({{:https://datatracker.ietf.org/doc/html/rfc9051}RFC 9051})
10+ and JMAP ({{:https://datatracker.ietf.org/doc/html/rfc8621}RFC 8621}) protocols.
11+12+ {2 Keyword Types}
13+14+ Keywords are organized into categories based on their specification:
15+ - {!standard}: Core flags from RFC 8621 Section 4.1.1 that map to IMAP system flags
16+ - {!spam}: Spam-related keywords for junk mail handling
17+ - {!extended}: Extended keywords from draft-ietf-mailmaint
18+ - {!flag_bit}: Apple Mail flag color bits
19+20+ {2 Protocol Mapping}
21+22+ IMAP system flags ([\Seen], [\Answered], etc.) map to JMAP keywords
23+ ([$seen], [$answered], etc.). The {!to_string} and {!to_imap_string}
24+ functions handle these conversions. *)
25+26+(** {1 Keyword Types} *)
27+28+(** Standard keywords per {{:https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1}RFC 8621 Section 4.1.1}.
29+30+ These keywords have direct mappings to IMAP system flags defined in
31+ {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}. *)
32+type standard = [
33+ | `Seen (** Message has been read. Maps to IMAP [\Seen]. *)
34+ | `Answered (** Message has been answered. Maps to IMAP [\Answered]. *)
35+ | `Flagged (** Message is flagged/starred. Maps to IMAP [\Flagged]. *)
36+ | `Draft (** Message is a draft. Maps to IMAP [\Draft]. *)
37+ | `Deleted (** Message marked for deletion. IMAP only, maps to [\Deleted]. *)
38+ | `Forwarded (** Message has been forwarded. JMAP [$forwarded] keyword. *)
39+]
40+41+(** Spam-related keywords for junk mail handling.
42+43+ These keywords help mail clients and servers coordinate spam filtering
44+ decisions across protocol boundaries. *)
45+type spam = [
46+ | `Phishing (** Message is a phishing attempt. JMAP [$phishing]. *)
47+ | `Junk (** Message is spam/junk. JMAP [$junk]. *)
48+ | `NotJunk (** Message explicitly marked as not junk. JMAP [$notjunk]. *)
49+]
50+51+(** Extended keywords per draft-ietf-mailmaint.
52+53+ These keywords provide additional metadata for enhanced mail client features
54+ beyond basic read/reply tracking. *)
55+type extended = [
56+ | `HasAttachment (** Message has attachments. *)
57+ | `HasNoAttachment (** Message has no attachments. Mutually exclusive with [`HasAttachment]. *)
58+ | `Memo (** Message is a memo. *)
59+ | `HasMemo (** Message has an associated memo. *)
60+ | `CanUnsubscribe (** Message has unsubscribe capability (List-Unsubscribe header). *)
61+ | `Unsubscribed (** User has unsubscribed from this sender. *)
62+ | `Muted (** Thread is muted. Mutually exclusive with [`Followed]. *)
63+ | `Followed (** Thread is followed. Mutually exclusive with [`Muted]. *)
64+ | `AutoSent (** Message was sent automatically. *)
65+ | `Imported (** Message was imported from another source. *)
66+ | `IsTrusted (** Sender is trusted. *)
67+ | `MaskedEmail (** Message was sent to a masked email address. *)
68+ | `New (** Message is new (not yet processed by client). *)
69+ | `Notify (** User should be notified about this message. *)
70+]
71+72+(** Apple Mail flag color bits.
73+74+ Apple Mail uses a 3-bit encoding for flag colors. The color is determined
75+ by the combination of bits set. See {!flag_color_of_keywords} for the
76+ mapping. *)
77+type flag_bit = [
78+ | `MailFlagBit0 (** Bit 0 of Apple Mail flag color encoding. *)
79+ | `MailFlagBit1 (** Bit 1 of Apple Mail flag color encoding. *)
80+ | `MailFlagBit2 (** Bit 2 of Apple Mail flag color encoding. *)
81+]
82+83+(** Unified keyword type combining all keyword categories.
84+85+ Use [`Custom s] for server-specific or application-specific keywords
86+ not covered by the standard categories. *)
87+type t = [ standard | spam | extended | flag_bit | `Custom of string ]
88+89+(** {1 Conversion Functions} *)
90+91+val of_string : string -> t
92+(** [of_string s] parses a keyword string.
93+94+ Handles both JMAP format ([$seen]) and bare format ([seen]).
95+ Parsing is case-insensitive for known keywords.
96+97+ Examples:
98+ - ["$seen"] -> [`Seen]
99+ - ["seen"] -> [`Seen]
100+ - ["SEEN"] -> [`Seen]
101+ - ["\\Seen"] -> [`Seen] (IMAP system flag format)
102+ - ["my-custom-flag"] -> [`Custom "my-custom-flag"] *)
103+104+val to_string : t -> string
105+(** [to_string k] converts a keyword to canonical JMAP format.
106+107+ Standard and extended keywords are returned with [$] prefix in lowercase.
108+ Apple Mail flag bits preserve their mixed case.
109+ Custom keywords are returned as-is.
110+111+ Examples:
112+ - [`Seen] -> ["$seen"]
113+ - [`MailFlagBit0] -> ["$MailFlagBit0"]
114+ - [`Custom "foo"] -> ["foo"] *)
115+116+val to_imap_string : t -> string
117+(** [to_imap_string k] converts a keyword to IMAP format.
118+119+ Standard keywords that map to IMAP system flags use backslash prefix.
120+ Other keywords use [$] prefix with appropriate casing.
121+122+ Examples:
123+ - [`Seen] -> ["\\Seen"]
124+ - [`Deleted] -> ["\\Deleted"]
125+ - [`Forwarded] -> ["$Forwarded"]
126+ - [`MailFlagBit0] -> ["$MailFlagBit0"] *)
127+128+(** {1 Predicates} *)
129+130+val is_standard : t -> bool
131+(** [is_standard k] returns [true] if [k] maps to an IMAP system flag.
132+133+ The standard keywords are: [`Seen], [`Answered], [`Flagged], [`Draft],
134+ and [`Deleted]. Note that [`Forwarded] is {i not} an IMAP system flag. *)
135+136+val is_mutually_exclusive : t -> t -> bool
137+(** [is_mutually_exclusive k1 k2] returns [true] if keywords [k1] and [k2]
138+ cannot both be set on the same message.
139+140+ Mutually exclusive pairs:
141+ - [`HasAttachment] and [`HasNoAttachment]
142+ - [`Junk] and [`NotJunk]
143+ - [`Muted] and [`Followed] *)
144+145+(** {1 Pretty Printing} *)
146+147+val pp : Format.formatter -> t -> unit
148+(** [pp ppf k] pretty-prints keyword [k] in JMAP format. *)
149+150+val equal : t -> t -> bool
151+(** [equal k1 k2] tests equality of keywords. *)
152+153+val compare : t -> t -> int
154+(** [compare k1 k2] provides total ordering on keywords. *)
155+156+(** {1 Apple Mail Flag Colors} *)
157+158+(** Apple Mail flag colors encoded as 3-bit values. *)
159+type flag_color = [
160+ | `Red (** No bits set *)
161+ | `Orange (** Bit 0 only *)
162+ | `Yellow (** Bit 1 only *)
163+ | `Green (** All bits set *)
164+ | `Blue (** Bit 2 only *)
165+ | `Purple (** Bits 0 and 2 *)
166+ | `Gray (** Bits 1 and 2 *)
167+]
168+169+val flag_color_of_keywords : t list -> flag_color option
170+(** [flag_color_of_keywords keywords] extracts the Apple Mail flag color
171+ from a list of keywords.
172+173+ Returns [None] if bits 0 and 1 are set but not bit 2 (invalid encoding). *)
174+175+val flag_color_to_keywords : flag_color -> t list
176+(** [flag_color_to_keywords color] returns the keyword bits needed to
177+ represent the given flag color. *)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** Unified Mailbox Attributes and Roles
7+8+ This module provides a unified representation of mailbox attributes
9+ across IMAP and JMAP protocols. It combines IMAP LIST response attributes
10+ ({{:https://www.rfc-editor.org/rfc/rfc9051#section-7.2.2}RFC 9051 Section 7.2.2}),
11+ special-use mailbox flags ({{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154}),
12+ and JMAP mailbox roles ({{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}).
13+14+ {2 References}
15+ - {{:https://www.rfc-editor.org/rfc/rfc9051}RFC 9051} - IMAP4rev2
16+ - {{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154} - IMAP LIST Extension for Special-Use Mailboxes
17+ - {{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258} - IMAP4 LIST Command Extensions
18+ - {{:https://www.rfc-editor.org/rfc/rfc8457}RFC 8457} - IMAP \$Important Keyword and \Important Special-Use Attribute
19+ - {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621} - JMAP for Mail *)
20+21+(** {1 IMAP LIST Attributes}
22+23+ Attributes returned in IMAP LIST responses per
24+ {{:https://www.rfc-editor.org/rfc/rfc9051#section-7.2.2}RFC 9051 Section 7.2.2}. *)
25+26+type list_attr = [
27+ | `Noinferiors
28+ (** [\Noinferiors] - No child mailboxes are possible under this mailbox.
29+ The mailbox cannot have inferior (child) mailboxes, either because the
30+ underlying storage doesn't support it or because the mailbox name is at
31+ the hierarchy depth limit for this mailbox store. *)
32+ | `Noselect
33+ (** [\Noselect] - This mailbox cannot be selected. It exists only to hold
34+ child mailboxes and is not a valid destination for messages. Stratum
35+ only, not a real mailbox. *)
36+ | `Marked
37+ (** [\Marked] - The mailbox has been marked "interesting" by the server.
38+ This typically indicates the mailbox contains new messages since the
39+ last time it was selected. *)
40+ | `Unmarked
41+ (** [\Unmarked] - The mailbox is not "interesting". The mailbox does not
42+ contain new messages since the last time it was selected. *)
43+ | `Subscribed
44+ (** [\Subscribed] - The mailbox is subscribed. Returned when the
45+ SUBSCRIBED selection option is specified or implied. *)
46+ | `HasChildren
47+ (** [\HasChildren] - The mailbox has child mailboxes. Part of the
48+ CHILDREN return option ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}). *)
49+ | `HasNoChildren
50+ (** [\HasNoChildren] - The mailbox has no child mailboxes. Part of the
51+ CHILDREN return option ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}). *)
52+ | `NonExistent
53+ (** [\NonExistent] - The mailbox name does not refer to an existing mailbox.
54+ This attribute is returned when a mailbox is part of the hierarchy but
55+ doesn't actually exist ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}).
56+ Implies [\Noselect]. *)
57+ | `Remote
58+ (** [\Remote] - The mailbox is located on a remote server.
59+ ({{:https://www.rfc-editor.org/rfc/rfc5258}RFC 5258}) *)
60+]
61+62+(** {1 Special-Use Roles}
63+64+ Special-use mailbox roles per {{:https://www.rfc-editor.org/rfc/rfc6154}RFC 6154}
65+ and {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}. These identify
66+ mailboxes with specific purposes. *)
67+68+type special_use = [
69+ | `All
70+ (** [\All] - A virtual mailbox containing all messages in the user's
71+ message store. Implementations may omit some messages. *)
72+ | `Archive
73+ (** [\Archive] - A mailbox used to archive messages. The meaning of
74+ "archived" may vary by server. *)
75+ | `Drafts
76+ (** [\Drafts] - A mailbox used to hold draft messages, typically messages
77+ being composed but not yet sent. *)
78+ | `Flagged
79+ (** [\Flagged] - A virtual mailbox containing all messages marked with
80+ the [\Flagged] flag. *)
81+ | `Important
82+ (** [\Important] - A mailbox used to hold messages deemed important to
83+ the user. ({{:https://www.rfc-editor.org/rfc/rfc8457}RFC 8457}) *)
84+ | `Inbox
85+ (** [inbox] - The user's inbox. This is a JMAP role
86+ ({{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}) without a direct
87+ IMAP special-use equivalent since INBOX is always special in IMAP. *)
88+ | `Junk
89+ (** [\Junk] - A mailbox used to hold messages that have been identified
90+ as spam or junk mail. Also known as "Spam" folder. *)
91+ | `Sent
92+ (** [\Sent] - A mailbox used to hold copies of messages that have been
93+ sent. *)
94+ | `Subscribed
95+ (** [subscribed] - A JMAP virtual mailbox role
96+ ({{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}) representing
97+ all subscribed mailboxes. *)
98+ | `Trash
99+ (** [\Trash] - A mailbox used to hold messages that have been deleted or
100+ marked for deletion. *)
101+ | `Snoozed
102+ (** [snoozed] - A mailbox for messages that have been snoozed until a
103+ later time. (draft-ietf-mailmaint-special-use-extensions) *)
104+ | `Scheduled
105+ (** [scheduled] - A mailbox for messages scheduled to be sent at a
106+ future time. (draft-ietf-mailmaint-special-use-extensions) *)
107+ | `Memos
108+ (** [memos] - A mailbox for memo/note messages.
109+ (draft-ietf-mailmaint-special-use-extensions) *)
110+]
111+112+(** {1 Unified Attribute Type} *)
113+114+type t = [ list_attr | special_use | `Extension of string ]
115+(** The unified mailbox attribute type combining LIST attributes, special-use
116+ roles, and server-specific extensions. Extensions are represented with
117+ their original string form (without leading backslash if present). *)
118+119+(** {1 Conversion Functions} *)
120+121+val of_string : string -> t
122+(** [of_string s] parses a mailbox attribute from its IMAP wire format.
123+ The input may optionally include the leading backslash. Parsing is
124+ case-insensitive. Unknown attributes are returned as [`Extension s].
125+126+ Examples:
127+ - [of_string "\\Drafts"] returns [`Drafts]
128+ - [of_string "drafts"] returns [`Drafts]
129+ - [of_string "\\X-Custom"] returns [`Extension "X-Custom"] *)
130+131+val to_string : t -> string
132+(** [to_string attr] converts an attribute to its IMAP wire format with
133+ the leading backslash prefix for standard attributes.
134+135+ Examples:
136+ - [to_string `Drafts] returns ["\\Drafts"]
137+ - [to_string `HasChildren] returns ["\\HasChildren"]
138+ - [to_string (`Extension "X-Custom")] returns ["\\X-Custom"] *)
139+140+val to_jmap_role : t -> string option
141+(** [to_jmap_role attr] converts a special-use attribute to its JMAP role
142+ string (lowercase). Returns [None] for LIST attributes that don't
143+ correspond to JMAP roles.
144+145+ Examples:
146+ - [to_jmap_role `Drafts] returns [Some "drafts"]
147+ - [to_jmap_role `Inbox] returns [Some "inbox"]
148+ - [to_jmap_role `Noselect] returns [None] *)
149+150+val of_jmap_role : string -> special_use option
151+(** [of_jmap_role s] parses a JMAP role string into a special-use attribute.
152+ Returns [None] if the role string is not recognized. The input should
153+ be lowercase as per JMAP conventions.
154+155+ Examples:
156+ - [of_jmap_role "drafts"] returns [Some `Drafts]
157+ - [of_jmap_role "inbox"] returns [Some `Inbox]
158+ - [of_jmap_role "unknown"] returns [None] *)
159+160+(** {1 Predicates} *)
161+162+val is_special_use : t -> bool
163+(** [is_special_use attr] returns [true] if the attribute is a special-use
164+ role (as opposed to a LIST attribute or extension).
165+166+ Examples:
167+ - [is_special_use `Drafts] returns [true]
168+ - [is_special_use `Noselect] returns [false]
169+ - [is_special_use (`Extension "x")] returns [false] *)
170+171+val is_selectable : t -> bool
172+(** [is_selectable attr] returns [false] if the attribute indicates the
173+ mailbox cannot be selected. This is [true] for [`Noselect] and
174+ [`NonExistent] attributes, and [false] for all others.
175+176+ Note: A mailbox may have multiple attributes. To determine if a mailbox
177+ is selectable, check that no attribute returns [false] from this function.
178+179+ Examples:
180+ - [is_selectable `Noselect] returns [false]
181+ - [is_selectable `NonExistent] returns [false]
182+ - [is_selectable `Drafts] returns [true]
183+ - [is_selectable `HasChildren] returns [true] *)
184+185+(** {1 Pretty Printing} *)
186+187+val pp : Format.formatter -> t -> unit
188+(** [pp ppf attr] pretty-prints the attribute in IMAP wire format. *)
+26
mail-flag/mail-flag.opam
···00000000000000000000000000
···1+# This file is generated by dune, edit dune-project instead
2+opam-version: "2.0"
3+synopsis: "Unified message flags and mailbox attributes for IMAP/JMAP"
4+description:
5+ "Type-safe message keywords, system flags, and mailbox attributes for email protocols. Supports RFC 9051 (IMAP4rev2), RFC 8621 (JMAP Mail), RFC 6154 (Special-Use), and draft-ietf-mailmaint extensions."
6+depends: [
7+ "dune" {>= "3.0"}
8+ "ocaml" {>= "5.0"}
9+ "fmt" {>= "0.9"}
10+ "alcotest" {with-test}
11+ "odoc" {with-doc}
12+]
13+build: [
14+ ["dune" "subst"] {dev}
15+ [
16+ "dune"
17+ "build"
18+ "-p"
19+ name
20+ "-j"
21+ jobs
22+ "@install"
23+ "@runtest" {with-test}
24+ "@doc" {with-doc}
25+ ]
26+]
···13module Com : sig
14 module Atproto : sig
15 module Repo : sig
16- module StrongRef : sig
17-18-type main = {
19- cid : string;
20- uri : string;
21-}
22-23-(** Jsont codec for {!type:main}. *)
24-val main_jsont : main Jsont.t
25-26- end
27 module Defs : sig
2829type commit_meta = {
···3334(** Jsont codec for {!type:commit_meta}. *)
35val commit_meta_jsont : commit_meta Jsont.t
00000000000000000000000003637 end
38 module ListRecords : sig
···70val output_jsont : output Jsont.t
7172 end
73- module GetRecord : sig
74-(** Get a single record from a repository. Does not require auth. *)
75-76-(** Query/procedure parameters. *)
77-type params = {
78- cid : string option; (** The CID of the version of the record. If not specified, then return the most recent version. *)
79- collection : string; (** The NSID of the record collection. *)
80- repo : string; (** The handle or DID of the repo. *)
81- rkey : string; (** The Record Key. *)
82-}
83-84-(** Jsont codec for {!type:params}. *)
85-val params_jsont : params Jsont.t
86-8788-type output = {
89- cid : string option;
90 uri : string;
91- value : Jsont.json;
92}
9394-(** Jsont codec for {!type:output}. *)
95-val output_jsont : output Jsont.t
9697 end
98 module PutRecord : sig
···124val output_jsont : output Jsont.t
125126 end
127- module DeleteRecord : sig
128-(** Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS. *)
129130131type input = {
132 collection : string; (** The NSID of the record collection. *)
0133 repo : string; (** The handle or DID of the repo (aka, current account). *)
134- rkey : string; (** The Record Key. *)
135 swap_commit : string option; (** Compare and swap with the previous commit by CID. *)
136- swap_record : string option; (** Compare and swap with the previous record by CID. *)
137}
138139(** Jsont codec for {!type:input}. *)
···141142143type output = {
0144 commit : Defs.commit_meta option;
00145}
146147(** Jsont codec for {!type:output}. *)
148val output_jsont : output Jsont.t
149150 end
151- module CreateRecord : sig
152-(** Create a single new repository record. Requires auth, implemented by PDS. *)
153154155type input = {
156 collection : string; (** The NSID of the record collection. *)
157- record : Jsont.json; (** The record itself. Must contain a $type field. *)
158 repo : string; (** The handle or DID of the repo (aka, current account). *)
159- rkey : string option; (** The Record Key. *)
160 swap_commit : string option; (** Compare and swap with the previous commit by CID. *)
161- validate : bool option; (** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. *)
162}
163164(** Jsont codec for {!type:input}. *)
···166167168type output = {
169- cid : string;
170 commit : Defs.commit_meta option;
171- uri : string;
172- validation_status : string option;
173}
174175(** Jsont codec for {!type:output}. *)
···13module Com : sig
14 module Atproto : sig
15 module Repo : sig
0000000000016 module Defs : sig
1718type commit_meta = {
···2223(** Jsont codec for {!type:commit_meta}. *)
24val commit_meta_jsont : commit_meta Jsont.t
25+26+ end
27+ module GetRecord : sig
28+(** Get a single record from a repository. Does not require auth. *)
29+30+(** Query/procedure parameters. *)
31+type params = {
32+ cid : string option; (** The CID of the version of the record. If not specified, then return the most recent version. *)
33+ collection : string; (** The NSID of the record collection. *)
34+ repo : string; (** The handle or DID of the repo. *)
35+ rkey : string; (** The Record Key. *)
36+}
37+38+(** Jsont codec for {!type:params}. *)
39+val params_jsont : params Jsont.t
40+41+42+type output = {
43+ cid : string option;
44+ uri : string;
45+ value : Jsont.json;
46+}
47+48+(** Jsont codec for {!type:output}. *)
49+val output_jsont : output Jsont.t
5051 end
52 module ListRecords : sig
···84val output_jsont : output Jsont.t
8586 end
87+ module StrongRef : sig
00000000000008889+type main = {
90+ cid : string;
91 uri : string;
092}
9394+(** Jsont codec for {!type:main}. *)
95+val main_jsont : main Jsont.t
9697 end
98 module PutRecord : sig
···124val output_jsont : output Jsont.t
125126 end
127+ module CreateRecord : sig
128+(** Create a single new repository record. Requires auth, implemented by PDS. *)
129130131type input = {
132 collection : string; (** The NSID of the record collection. *)
133+ record : Jsont.json; (** The record itself. Must contain a $type field. *)
134 repo : string; (** The handle or DID of the repo (aka, current account). *)
135+ rkey : string option; (** The Record Key. *)
136 swap_commit : string option; (** Compare and swap with the previous commit by CID. *)
137+ validate : bool option; (** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. *)
138}
139140(** Jsont codec for {!type:input}. *)
···142143144type output = {
145+ cid : string;
146 commit : Defs.commit_meta option;
147+ uri : string;
148+ validation_status : string option;
149}
150151(** Jsont codec for {!type:output}. *)
152val output_jsont : output Jsont.t
153154 end
155+ module DeleteRecord : sig
156+(** Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS. *)
157158159type input = {
160 collection : string; (** The NSID of the record collection. *)
0161 repo : string; (** The handle or DID of the repo (aka, current account). *)
162+ rkey : string; (** The Record Key. *)
163 swap_commit : string option; (** Compare and swap with the previous commit by CID. *)
164+ swap_record : string option; (** Compare and swap with the previous record by CID. *)
165}
166167(** Jsont codec for {!type:input}. *)
···169170171type output = {
0172 commit : Defs.commit_meta option;
00173}
174175(** Jsont codec for {!type:output}. *)
···1213module Com : sig
14 module Atproto : sig
00000000000000000000000000000000000000000000000015 module Label : sig
16 module Defs : sig
17(** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. *)
···9394 end
95 end
96- module Moderation : sig
97- module Defs : sig
98-(** Tag describing a type of subject that might be reported. *)
99-100-type subject_type = string
101-val subject_type_jsont : subject_type Jsont.t
102-103-(** Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`. *)
104-105-type reason_violation = string
106-val reason_violation_jsont : reason_violation Jsont.t
107-108-109-type reason_type = string
110-val reason_type_jsont : reason_type Jsont.t
111-112-(** Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`. *)
113-114-type reason_spam = string
115-val reason_spam_jsont : reason_spam Jsont.t
116-117-(** Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`. *)
118-119-type reason_sexual = string
120-val reason_sexual_jsont : reason_sexual Jsont.t
121-122-(** Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`. *)
123-124-type reason_rude = string
125-val reason_rude_jsont : reason_rude Jsont.t
126-127-(** Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`. *)
128-129-type reason_other = string
130-val reason_other_jsont : reason_other Jsont.t
131-132-(** Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`. *)
133-134-type reason_misleading = string
135-val reason_misleading_jsont : reason_misleading Jsont.t
136-137-(** Appeal a previously taken moderation action *)
138-139-type reason_appeal = string
140-val reason_appeal_jsont : reason_appeal Jsont.t
141-142- end
143- end
144 end
145end
146module App : sig
147 module Bsky : sig
148- module AuthManageLabelerService : sig
149150type main = unit
151val main_jsont : main Jsont.t
···157val main_jsont : main Jsont.t
158159 end
160- module AuthManageModeration : sig
161-162-type main = unit
163-val main_jsont : main Jsont.t
164-165- end
166- module Richtext : sig
167- module Facet : sig
168-(** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). *)
169-170-type tag = {
171- tag : string;
172-}
173-174-(** Jsont codec for {!type:tag}. *)
175-val tag_jsont : tag Jsont.t
176-177-(** Facet feature for mention of another account. The text is usually a handle, including a '\@' prefix, but the facet reference is a DID. *)
178-179-type mention = {
180- did : string;
181-}
182-183-(** Jsont codec for {!type:mention}. *)
184-val mention_jsont : mention Jsont.t
185-186-(** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. *)
187-188-type link = {
189- uri : string;
190-}
191-192-(** Jsont codec for {!type:link}. *)
193-val link_jsont : link Jsont.t
194-195-(** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. *)
196-197-type byte_slice = {
198- byte_end : int;
199- byte_start : int;
200-}
201-202-(** Jsont codec for {!type:byte_slice}. *)
203-val byte_slice_jsont : byte_slice Jsont.t
204-205-(** Annotation of a sub-string within rich text. *)
206-207-type main = {
208- features : Jsont.json list;
209- index : byte_slice;
210-}
211-212-(** Jsont codec for {!type:main}. *)
213-val main_jsont : main Jsont.t
214-215- end
216- end
217 module AuthManageFeedDeclarations : sig
218219type main = unit
220val main_jsont : main Jsont.t
221222 end
223- module AuthFullApp : sig
224225type main = unit
226val main_jsont : main Jsont.t
227228 end
229- module AuthManageNotifications : sig
230231type main = unit
232val main_jsont : main Jsont.t
···238val main_jsont : main Jsont.t
239240 end
241- module Ageassurance : sig
242- module Defs : sig
243-(** The status of the Age Assurance process. *)
244-245-type status = string
246-val status_jsont : status Jsont.t
247-248-(** Additional metadata needed to compute Age Assurance state client-side. *)
249-250-type state_metadata = {
251- account_created_at : string option; (** The account creation timestamp. *)
252-}
253-254-(** Jsont codec for {!type:state_metadata}. *)
255-val state_metadata_jsont : state_metadata Jsont.t
256-257-(** Object used to store Age Assurance data in stash. *)
258-259-type event = {
260- access : string; (** The access level granted based on Age Assurance data we've processed. *)
261- attempt_id : string; (** The unique identifier for this instance of the Age Assurance flow, in UUID format. *)
262- complete_ip : string option; (** The IP address used when completing the Age Assurance flow. *)
263- complete_ua : string option; (** The user agent used when completing the Age Assurance flow. *)
264- country_code : string; (** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. *)
265- created_at : string; (** The date and time of this write operation. *)
266- email : string option; (** The email used for Age Assurance. *)
267- init_ip : string option; (** The IP address used when initiating the Age Assurance flow. *)
268- init_ua : string option; (** The user agent used when initiating the Age Assurance flow. *)
269- region_code : string option; (** The ISO 3166-2 region code provided when beginning the Age Assurance flow. *)
270- status : string; (** The status of the Age Assurance process. *)
271-}
272-273-(** Jsont codec for {!type:event}. *)
274-val event_jsont : event Jsont.t
275-276-(** The Age Assurance configuration for a specific region. *)
277-278-type config_region = {
279- country_code : string; (** The ISO 3166-1 alpha-2 country code this configuration applies to. *)
280- min_access_age : int; (** The minimum age (as a whole integer) required to use Bluesky in this region. *)
281- region_code : string option; (** The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. *)
282- rules : Jsont.json list; (** The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item. *)
283-}
284-285-(** Jsont codec for {!type:config_region}. *)
286-val config_region_jsont : config_region Jsont.t
287-288-(** The access level granted based on Age Assurance data we've processed. *)
289-290-type access = string
291-val access_jsont : access Jsont.t
292-293-(** The user's computed Age Assurance state. *)
294-295-type state = {
296- access : access;
297- last_initiated_at : string option; (** The timestamp when this state was last updated. *)
298- status : status;
299-}
300-301-(** Jsont codec for {!type:state}. *)
302-val state_jsont : state Jsont.t
303-304-(** Age Assurance rule that applies if the user has declared themselves under a certain age. *)
305-306-type config_region_rule_if_declared_under_age = {
307- access : access;
308- age : int; (** The age threshold as a whole integer. *)
309-}
310-311-(** Jsont codec for {!type:config_region_rule_if_declared_under_age}. *)
312-val config_region_rule_if_declared_under_age_jsont : config_region_rule_if_declared_under_age Jsont.t
313-314-(** Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. *)
315-316-type config_region_rule_if_declared_over_age = {
317- access : access;
318- age : int; (** The age threshold as a whole integer. *)
319-}
320-321-(** Jsont codec for {!type:config_region_rule_if_declared_over_age}. *)
322-val config_region_rule_if_declared_over_age_jsont : config_region_rule_if_declared_over_age Jsont.t
323-324-(** Age Assurance rule that applies if the user has been assured to be under a certain age. *)
325-326-type config_region_rule_if_assured_under_age = {
327- access : access;
328- age : int; (** The age threshold as a whole integer. *)
329-}
330-331-(** Jsont codec for {!type:config_region_rule_if_assured_under_age}. *)
332-val config_region_rule_if_assured_under_age_jsont : config_region_rule_if_assured_under_age Jsont.t
333-334-(** Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. *)
335-336-type config_region_rule_if_assured_over_age = {
337- access : access;
338- age : int; (** The age threshold as a whole integer. *)
339-}
340-341-(** Jsont codec for {!type:config_region_rule_if_assured_over_age}. *)
342-val config_region_rule_if_assured_over_age_jsont : config_region_rule_if_assured_over_age Jsont.t
343-344-(** Age Assurance rule that applies if the account is older than a certain date. *)
345-346-type config_region_rule_if_account_older_than = {
347- access : access;
348- date : string; (** The date threshold as a datetime string. *)
349-}
350-351-(** Jsont codec for {!type:config_region_rule_if_account_older_than}. *)
352-val config_region_rule_if_account_older_than_jsont : config_region_rule_if_account_older_than Jsont.t
353-354-(** Age Assurance rule that applies if the account is equal-to or newer than a certain date. *)
355-356-type config_region_rule_if_account_newer_than = {
357- access : access;
358- date : string; (** The date threshold as a datetime string. *)
359-}
360-361-(** Jsont codec for {!type:config_region_rule_if_account_newer_than}. *)
362-val config_region_rule_if_account_newer_than_jsont : config_region_rule_if_account_newer_than Jsont.t
363-364-(** Age Assurance rule that applies by default. *)
365-366-type config_region_rule_default = {
367- access : access;
368-}
369-370-(** Jsont codec for {!type:config_region_rule_default}. *)
371-val config_region_rule_default_jsont : config_region_rule_default Jsont.t
372-373-374-type config = {
375- regions : config_region list; (** The per-region Age Assurance configuration. *)
376-}
377-378-(** Jsont codec for {!type:config}. *)
379-val config_jsont : config Jsont.t
380-381- end
382- module Begin : sig
383-(** Initiate Age Assurance for an account. *)
384-385-386-type input = {
387- country_code : string; (** An ISO 3166-1 alpha-2 code of the user's location. *)
388- email : string; (** The user's email address to receive Age Assurance instructions. *)
389- language : string; (** The user's preferred language for communication during the Age Assurance process. *)
390- region_code : string option; (** An optional ISO 3166-2 code of the user's region or state within the country. *)
391-}
392-393-(** Jsont codec for {!type:input}. *)
394-val input_jsont : input Jsont.t
395-396-397-type output = Defs.state
398-399-(** Jsont codec for {!type:output}. *)
400-val output_jsont : output Jsont.t
401-402- end
403- module GetState : sig
404-(** Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side. *)
405-406-(** Query/procedure parameters. *)
407-type params = {
408- country_code : string;
409- region_code : string option;
410-}
411-412-(** Jsont codec for {!type:params}. *)
413-val params_jsont : params Jsont.t
414-415-416-type output = {
417- metadata : Defs.state_metadata;
418- state : Defs.state;
419-}
420-421-(** Jsont codec for {!type:output}. *)
422-val output_jsont : output Jsont.t
423-424- end
425- module GetConfig : sig
426-(** Returns Age Assurance configuration for use on the client. *)
427-428-429-type output = Defs.config
430-431-(** Jsont codec for {!type:output}. *)
432-val output_jsont : output Jsont.t
433-434- end
435- end
436- module Labeler : sig
437- module Defs : sig
438-439-type labeler_viewer_state = {
440- like : string option;
441-}
442-443-(** Jsont codec for {!type:labeler_viewer_state}. *)
444-val labeler_viewer_state_jsont : labeler_viewer_state Jsont.t
445-446-447-type labeler_policies = {
448- label_value_definitions : Com.Atproto.Label.Defs.label_value_definition list option; (** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. *)
449- label_values : Com.Atproto.Label.Defs.label_value list; (** The label values which this labeler publishes. May include global or custom labels. *)
450-}
451-452-(** Jsont codec for {!type:labeler_policies}. *)
453-val labeler_policies_jsont : labeler_policies Jsont.t
454-455-456-type labeler_view_detailed = {
457- cid : string;
458- creator : Jsont.json;
459- indexed_at : string;
460- labels : Com.Atproto.Label.Defs.label list option;
461- like_count : int option;
462- policies : Jsont.json;
463- reason_types : Com.Atproto.Moderation.Defs.reason_type list option; (** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. *)
464- subject_collections : string list option; (** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. *)
465- subject_types : Com.Atproto.Moderation.Defs.subject_type list option; (** The set of subject types (account, record, etc) this service accepts reports on. *)
466- uri : string;
467- viewer : Jsont.json option;
468-}
469-470-(** Jsont codec for {!type:labeler_view_detailed}. *)
471-val labeler_view_detailed_jsont : labeler_view_detailed Jsont.t
472-473-474-type labeler_view = {
475- cid : string;
476- creator : Jsont.json;
477- indexed_at : string;
478- labels : Com.Atproto.Label.Defs.label list option;
479- like_count : int option;
480- uri : string;
481- viewer : Jsont.json option;
482-}
483-484-(** Jsont codec for {!type:labeler_view}. *)
485-val labeler_view_jsont : labeler_view Jsont.t
486-487- end
488- module Service : sig
489-(** A declaration of the existence of labeler service. *)
490-491-type main = {
492- created_at : string;
493- labels : Com.Atproto.Label.Defs.self_labels option;
494- policies : Jsont.json;
495- reason_types : Com.Atproto.Moderation.Defs.reason_type list option; (** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. *)
496- subject_collections : string list option; (** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. *)
497- subject_types : Com.Atproto.Moderation.Defs.subject_type list option; (** The set of subject types (account, record, etc) this service accepts reports on. *)
498-}
499-500-(** Jsont codec for {!type:main}. *)
501-val main_jsont : main Jsont.t
502-503- end
504- module GetServices : sig
505-(** Get information about a list of labeler services. *)
506-507-(** Query/procedure parameters. *)
508-type params = {
509- detailed : bool option;
510- dids : string list;
511-}
512-513-(** Jsont codec for {!type:params}. *)
514-val params_jsont : params Jsont.t
515-516-517-type output = {
518- views : Jsont.json list;
519-}
520-521-(** Jsont codec for {!type:output}. *)
522-val output_jsont : output Jsont.t
523-524- end
525- end
526- module AuthCreatePosts : sig
527528type main = unit
529val main_jsont : main Jsont.t
530531 end
532- module Video : sig
533- module GetUploadLimits : sig
534-(** Get video upload limits for the authenticated user. *)
535-536-537-type output = {
538- can_upload : bool;
539- error : string option;
540- message : string option;
541- remaining_daily_bytes : int option;
542- remaining_daily_videos : int option;
543-}
544-545-(** Jsont codec for {!type:output}. *)
546-val output_jsont : output Jsont.t
547-548- end
549- module Defs : sig
550-551-type job_status = {
552- blob : Atp.Blob_ref.t option;
553- did : string;
554- error : string option;
555- job_id : string;
556- message : string option;
557- progress : int option; (** Progress within the current processing state. *)
558- state : string; (** The state of the video processing job. All values not listed as a known value indicate that the job is in process. *)
559-}
560-561-(** Jsont codec for {!type:job_status}. *)
562-val job_status_jsont : job_status Jsont.t
563-564- end
565- module UploadVideo : sig
566-(** Upload a video to be processed then stored on the PDS. *)
567-568-569-type input = unit
570-val input_jsont : input Jsont.t
571-572-573-type output = {
574- job_status : Defs.job_status;
575-}
576-577-(** Jsont codec for {!type:output}. *)
578-val output_jsont : output Jsont.t
579-580- end
581- module GetJobStatus : sig
582-(** Get status details for a video processing job. *)
583584(** Query/procedure parameters. *)
585type params = {
586- job_id : string;
0587}
588589(** Jsont codec for {!type:params}. *)
···591592593type output = {
594- job_status : Defs.job_status;
595}
596597(** Jsont codec for {!type:output}. *)
598val output_jsont : output Jsont.t
599600 end
601- end
602- module Embed : sig
603- module External : sig
604-605-type view_external = {
606- description : string;
607- thumb : string option;
608- title : string;
609- uri : string;
610-}
611-612-(** Jsont codec for {!type:view_external}. *)
613-val view_external_jsont : view_external Jsont.t
614-615-616-type external_ = {
617- description : string;
618- thumb : Atp.Blob_ref.t option;
619- title : string;
620- uri : string;
621-}
622-623-(** Jsont codec for {!type:external_}. *)
624-val external__jsont : external_ Jsont.t
625-626-627-type view = {
628- external_ : Jsont.json;
629-}
630-631-(** Jsont codec for {!type:view}. *)
632-val view_jsont : view Jsont.t
633-634-(** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). *)
635-636-type main = {
637- external_ : Jsont.json;
638-}
639-640-(** Jsont codec for {!type:main}. *)
641-val main_jsont : main Jsont.t
642-643- end
644- module Defs : sig
645-(** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. *)
646-647-type aspect_ratio = {
648- height : int;
649- width : int;
650-}
651-652-(** Jsont codec for {!type:aspect_ratio}. *)
653-val aspect_ratio_jsont : aspect_ratio Jsont.t
654-655- end
656- module Images : sig
657-658-type view_image = {
659- alt : string; (** Alt text description of the image, for accessibility. *)
660- aspect_ratio : Jsont.json option;
661- fullsize : string; (** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. *)
662- thumb : string; (** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. *)
663-}
664-665-(** Jsont codec for {!type:view_image}. *)
666-val view_image_jsont : view_image Jsont.t
667-668-669-type image = {
670- alt : string; (** Alt text description of the image, for accessibility. *)
671- aspect_ratio : Jsont.json option;
672- image : Atp.Blob_ref.t;
673-}
674-675-(** Jsont codec for {!type:image}. *)
676-val image_jsont : image Jsont.t
677-678-679-type view = {
680- images : Jsont.json list;
681-}
682-683-(** Jsont codec for {!type:view}. *)
684-val view_jsont : view Jsont.t
685-686-687-type main = {
688- images : Jsont.json list;
689-}
690-691-(** Jsont codec for {!type:main}. *)
692-val main_jsont : main Jsont.t
693-694- end
695- module Video : sig
696-697-type view = {
698- alt : string option;
699- aspect_ratio : Jsont.json option;
700- cid : string;
701- playlist : string;
702- thumbnail : string option;
703-}
704-705-(** Jsont codec for {!type:view}. *)
706-val view_jsont : view Jsont.t
707-708-709-type caption = {
710- file : Atp.Blob_ref.t;
711- lang : string;
712-}
713-714-(** Jsont codec for {!type:caption}. *)
715-val caption_jsont : caption Jsont.t
716-717-718-type main = {
719- alt : string option; (** Alt text description of the video, for accessibility. *)
720- aspect_ratio : Jsont.json option;
721- captions : Jsont.json list option;
722- video : Atp.Blob_ref.t; (** The mp4 video file. May be up to 100mb, formerly limited to 50mb. *)
723-}
724-725-(** Jsont codec for {!type:main}. *)
726-val main_jsont : main Jsont.t
727-728- end
729- module RecordWithMedia : sig
730-731-type view = {
732- media : Jsont.json;
733- record : Jsont.json;
734-}
735-736-(** Jsont codec for {!type:view}. *)
737-val view_jsont : view Jsont.t
738-739-740-type main = {
741- media : Jsont.json;
742- record : Jsont.json;
743-}
744-745-(** Jsont codec for {!type:main}. *)
746-val main_jsont : main Jsont.t
747-748- end
749- module Record : sig
750-751-type view_record = {
752- author : Jsont.json;
753- cid : string;
754- embeds : Jsont.json list option;
755- indexed_at : string;
756- labels : Com.Atproto.Label.Defs.label list option;
757- like_count : int option;
758- quote_count : int option;
759- reply_count : int option;
760- repost_count : int option;
761- uri : string;
762- value : Jsont.json; (** The record data itself. *)
763-}
764-765-(** Jsont codec for {!type:view_record}. *)
766-val view_record_jsont : view_record Jsont.t
767-768-769-type view_not_found = {
770- not_found : bool;
771- uri : string;
772-}
773-774-(** Jsont codec for {!type:view_not_found}. *)
775-val view_not_found_jsont : view_not_found Jsont.t
776-777-778-type view_detached = {
779- detached : bool;
780- uri : string;
781-}
782-783-(** Jsont codec for {!type:view_detached}. *)
784-val view_detached_jsont : view_detached Jsont.t
785-786-787-type view_blocked = {
788- author : Jsont.json;
789- blocked : bool;
790- uri : string;
791-}
792-793-(** Jsont codec for {!type:view_blocked}. *)
794-val view_blocked_jsont : view_blocked Jsont.t
795-796-797-type view = {
798- record : Jsont.json;
799-}
800-801-(** Jsont codec for {!type:view}. *)
802-val view_jsont : view Jsont.t
803-804-805-type main = {
806- record : Com.Atproto.Repo.StrongRef.main;
807-}
808-809-(** Jsont codec for {!type:main}. *)
810-val main_jsont : main Jsont.t
811-812- end
813- end
814- module Notification : sig
815 module UpdateSeen : sig
816(** Notify server that the requesting account has seen notifications. Requires auth. *)
817···824val input_jsont : input Jsont.t
825826 end
827- module RegisterPush : sig
828-(** Register to receive push notifications, via a specified service, for the requesting account. Requires auth. *)
829-830-831-type input = {
832- age_restricted : bool option; (** Set to true when the actor is age restricted *)
833- app_id : string;
834- platform : string;
835- service_did : string;
836- token : string;
837-}
838-839-(** Jsont codec for {!type:input}. *)
840-val input_jsont : input Jsont.t
841-842- end
843 module ListNotifications : sig
844845type notification = {
···883val output_jsont : output Jsont.t
884885 end
886- module GetUnreadCount : sig
887-(** Count the number of unread notifications for the requesting account. Requires auth. *)
888889-(** Query/procedure parameters. *)
890-type params = {
891- priority : bool option;
892- seen_at : string option;
893}
894895-(** Jsont codec for {!type:params}. *)
896-val params_jsont : params Jsont.t
0000897898899-type output = {
900- count : int;
901}
902903-(** Jsont codec for {!type:output}. *)
904-val output_jsont : output Jsont.t
905906 end
907- module UnregisterPush : sig
908-(** The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth. *)
909910911type input = {
0912 app_id : string;
913 platform : string;
914 service_did : string;
···919val input_jsont : input Jsont.t
920921 end
922- module PutPreferences : sig
923-(** Set notification-related preferences for an account. Requires auth. *)
924925926type input = {
927- priority : bool;
000928}
929930(** Jsont codec for {!type:input}. *)
···10041005(** Jsont codec for {!type:preferences}. *)
1006val preferences_jsont : preferences Jsont.t
1007-1008- end
1009- module Declaration : sig
1010-(** A declaration of the user's choices related to notifications that can be produced by them. *)
1011-1012-type main = {
1013- allow_subscriptions : string; (** A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'. *)
1014-}
1015-1016-(** Jsont codec for {!type:main}. *)
1017-val main_jsont : main Jsont.t
10181019 end
1020 module ListActivitySubscriptions : sig
···11121113 end
1114 end
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001115 module Actor : sig
1116 module Status : sig
1117(** Advertises an account as currently offering live content. *)
···1126 duration_minutes : int option; (** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. *)
1127 embed : Jsont.json option; (** An optional embed associated with the status. *)
1128 status : string; (** The status for the account. *)
1129-}
1130-1131-(** Jsont codec for {!type:main}. *)
1132-val main_jsont : main Jsont.t
1133-1134- end
1135- module Profile : sig
1136-(** A declaration of a Bluesky account profile. *)
1137-1138-type main = {
1139- avatar : Atp.Blob_ref.t option; (** Small image to be displayed next to posts from account. AKA, 'profile picture' *)
1140- banner : Atp.Blob_ref.t option; (** Larger horizontal image to display behind profile view. *)
1141- created_at : string option;
1142- description : string option; (** Free-form profile description text. *)
1143- display_name : string option;
1144- joined_via_starter_pack : Com.Atproto.Repo.StrongRef.main option;
1145- labels : Com.Atproto.Label.Defs.self_labels option; (** Self-label values, specific to the Bluesky application, on the overall account. *)
1146- pinned_post : Com.Atproto.Repo.StrongRef.main option;
1147- pronouns : string option; (** Free-form pronouns text. *)
1148- website : string option;
1149}
11501151(** Jsont codec for {!type:main}. *)
···1515val known_followers_jsont : known_followers Jsont.t
15161517 end
1518- module GetPreferences : sig
1519-(** Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth. *)
15201521-(** Query/procedure parameters. *)
1522-type params = unit
1523-1524-(** Jsont codec for {!type:params}. *)
1525-val params_jsont : params Jsont.t
1526-1527-1528-type output = {
1529- preferences : Jsont.json;
001530}
15311532-(** Jsont codec for {!type:output}. *)
1533-val output_jsont : output Jsont.t
15341535 end
1536- module SearchActorsTypeahead : sig
1537-(** Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth. *)
15381539(** Query/procedure parameters. *)
1540type params = {
1541- limit : int option;
1542- q : string option; (** Search query prefix; not a full query string. *)
1543- term : string option; (** DEPRECATED: use 'q' instead. *)
1544}
15451546(** Jsont codec for {!type:params}. *)
···154815491550type output = {
1551- actors : Jsont.json list;
1552}
15531554(** Jsont codec for {!type:output}. *)
1555val output_jsont : output Jsont.t
15561557 end
1558- module GetProfile : sig
1559-(** Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth. *)
15601561(** Query/procedure parameters. *)
1562-type params = {
1563- actor : string; (** Handle or DID of account to fetch profile of. *)
1564-}
15651566(** Jsont codec for {!type:params}. *)
1567val params_jsont : params Jsont.t
156815691570-type output = Jsont.json
0015711572(** Jsont codec for {!type:output}. *)
1573val output_jsont : output Jsont.t
15741575 end
1576- module SearchActors : sig
1577-(** Find actors (profiles) matching search criteria. Does not require auth. *)
15781579(** Query/procedure parameters. *)
1580type params = {
1581 cursor : string option;
1582 limit : int option;
1583- q : string option; (** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. *)
1584- term : string option; (** DEPRECATED: use 'q' instead. *)
1585}
15861587(** Jsont codec for {!type:params}. *)
···1591type output = {
1592 actors : Jsont.json list;
1593 cursor : string option;
01594}
15951596(** Jsont codec for {!type:output}. *)
1597val output_jsont : output Jsont.t
15981599 end
1600- module GetSuggestions : sig
1601-(** Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding. *)
16021603(** Query/procedure parameters. *)
1604type params = {
1605- cursor : string option;
1606- limit : int option;
1607}
16081609(** Jsont codec for {!type:params}. *)
1610val params_jsont : params Jsont.t
161116121613-type output = {
1614- actors : Jsont.json list;
1615- cursor : string option;
1616- rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
1617-}
16181619(** Jsont codec for {!type:output}. *)
1620val output_jsont : output Jsont.t
16211622 end
1623- module GetProfiles : sig
1624-(** Get detailed profile views of multiple actors. *)
16251626(** Query/procedure parameters. *)
1627type params = {
1628- actors : string list;
0001629}
16301631(** Jsont codec for {!type:params}. *)
···163316341635type output = {
1636- profiles : Jsont.json list;
01637}
16381639(** Jsont codec for {!type:output}. *)
···1652val input_jsont : input Jsont.t
16531654 end
1655- end
1656- module Contact : sig
1657- module Defs : sig
16581659-type sync_status = {
1660- matches_count : int; (** Number of existing contact matches resulting of the user imports and of their imported contacts having imported the user. Matches stop being counted when the user either follows the matched contact or dismisses the match. *)
1661- synced_at : string; (** Last date when contacts where imported. *)
001662}
16631664-(** Jsont codec for {!type:sync_status}. *)
1665-val sync_status_jsont : sync_status Jsont.t
16661667-(** A stash object to be sent via bsync representing a notification to be created. *)
16681669-type notification = {
1670- from : string; (** The DID of who this notification comes from. *)
1671- to_ : string; (** The DID of who this notification should go to. *)
1672-}
1673-1674-(** Jsont codec for {!type:notification}. *)
1675-val notification_jsont : notification Jsont.t
1676-1677-(** Associates a profile with the positional index of the contact import input in the call to `app.bsky.contact.importContacts`, so clients can know which phone caused a particular match. *)
1678-1679-type match_and_contact_index = {
1680- contact_index : int; (** The index of this match in the import contact input. *)
1681- match_ : Jsont.json; (** Profile of the matched user. *)
1682}
16831684-(** Jsont codec for {!type:match_and_contact_index}. *)
1685-val match_and_contact_index_jsont : match_and_contact_index Jsont.t
16861687 end
1688- module RemoveData : sig
1689-(** Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication. *)
1690-1691-1692-type input = unit
1693-1694-(** Jsont codec for {!type:input}. *)
1695-val input_jsont : input Jsont.t
169616971698-type output = unit
00000016991700(** Jsont codec for {!type:output}. *)
1701val output_jsont : output Jsont.t
17021703 end
1704- module DismissMatch : sig
1705-(** Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication. *)
1706-17071708-type input = {
1709- subject : string; (** The subject's DID to dismiss the match with. *)
0000001710}
17111712-(** Jsont codec for {!type:input}. *)
1713-val input_jsont : input Jsont.t
1714-1715-1716-type output = unit
1717-1718-(** Jsont codec for {!type:output}. *)
1719-val output_jsont : output Jsont.t
17201721 end
1722- module GetMatches : sig
1723-(** Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication. *)
17241725(** Query/procedure parameters. *)
1726type params = {
1727- cursor : string option;
1728- limit : int option;
1729}
17301731(** Jsont codec for {!type:params}. *)
···173317341735type output = {
1736- cursor : string option;
1737- matches : Jsont.json list;
1738}
17391740(** Jsont codec for {!type:output}. *)
1741val output_jsont : output Jsont.t
17421743 end
1744- module VerifyPhone : sig
1745-(** Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication. *)
174617471748-type input = {
1749- code : string; (** The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`. *)
1750- phone : string; (** The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`. *)
1751-}
1752-1753-(** Jsont codec for {!type:input}. *)
1754val input_jsont : input Jsont.t
175517561757type output = {
1758- token : string; (** JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call. *)
1759}
17601761(** Jsont codec for {!type:output}. *)
1762val output_jsont : output Jsont.t
17631764 end
1765- module StartPhoneVerification : sig
1766-(** Starts a phone verification flow. The phone passed will receive a code via SMS that should be passed to `app.bsky.contact.verifyPhone`. Requires authentication. *)
00176700017681769-type input = {
1770- phone : string; (** The phone number to receive the code via SMS. *)
000001771}
17721773-(** Jsont codec for {!type:input}. *)
1774-val input_jsont : input Jsont.t
1775017761777-type output = unit
0017781779-(** Jsont codec for {!type:output}. *)
1780-val output_jsont : output Jsont.t
17811782- end
1783- module SendNotification : sig
1784-(** System endpoint to send notifications related to contact imports. Requires role authentication. *)
17851786-1787-type input = {
1788- from : string; (** The DID of who this notification comes from. *)
1789- to_ : string; (** The DID of who this notification should go to. *)
1790}
17911792-(** Jsont codec for {!type:input}. *)
1793-val input_jsont : input Jsont.t
1794017951796-type output = unit
00017971798-(** Jsont codec for {!type:output}. *)
1799-val output_jsont : output Jsont.t
18001801 end
1802- module GetSyncStatus : sig
1803-(** Gets the user's current contact import status. Requires authentication. *)
18041805-(** Query/procedure parameters. *)
1806-type params = unit
18071808-(** Jsont codec for {!type:params}. *)
1809-val params_jsont : params Jsont.t
0018100018111812-type output = {
1813- sync_status : Defs.sync_status option; (** If present, indicates the user has imported their contacts. If not present, indicates the user never used the feature or called `app.bsky.contact.removeData` and didn't import again since. *)
001814}
18151816-(** Jsont codec for {!type:output}. *)
1817-val output_jsont : output Jsont.t
18181819- end
1820- module ImportContacts : sig
1821-(** Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication. *)
18221823-1824-type input = {
1825- contacts : string list; (** List of phone numbers in global E.164 format (e.g., '+12125550123'). Phone numbers that cannot be normalized into a valid phone number will be discarded. Should not repeat the 'phone' input used in `app.bsky.contact.verifyPhone`. *)
1826- token : string; (** JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`. *)
000000001827}
18281829-(** Jsont codec for {!type:input}. *)
1830-val input_jsont : input Jsont.t
1831018321833-type output = {
1834- matches_and_contact_indexes : Defs.match_and_contact_index list; (** The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list. *)
0001835}
18361837-(** Jsont codec for {!type:output}. *)
1838-val output_jsont : output Jsont.t
0018391840- end
1841- end
1842- module Graph : sig
1843- module Starterpack : sig
18441845-type feed_item = {
1846- uri : string;
001847}
18481849-(** Jsont codec for {!type:feed_item}. *)
1850-val feed_item_jsont : feed_item Jsont.t
18511852-(** Record defining a starter pack of actors and feeds for new users. *)
18531854-type main = {
1855- created_at : string;
1856- description : string option;
1857- description_facets : Richtext.Facet.main list option;
1858- feeds : Jsont.json list option;
1859- list_ : string; (** Reference (AT-URI) to the list record. *)
1860- name : string; (** Display name for starter pack; can not be empty. *)
1861}
18621863-(** Jsont codec for {!type:main}. *)
1864-val main_jsont : main Jsont.t
18651866- end
1867- module GetFollows : sig
1868-(** Enumerates accounts which a specified account (actor) follows. *)
18691870-(** Query/procedure parameters. *)
1871-type params = {
1872- actor : string;
1873- cursor : string option;
1874- limit : int option;
1875}
18761877-(** Jsont codec for {!type:params}. *)
1878-val params_jsont : params Jsont.t
1879018801881-type output = {
1882- cursor : string option;
1883- follows : Jsont.json list;
1884- subject : Jsont.json;
1885}
18861887-(** Jsont codec for {!type:output}. *)
1888-val output_jsont : output Jsont.t
18891890- end
1891- module GetSuggestedFollowsByActor : sig
1892-(** Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account. *)
18931894-(** Query/procedure parameters. *)
1895-type params = {
1896- actor : string;
1897}
18981899-(** Jsont codec for {!type:params}. *)
1900-val params_jsont : params Jsont.t
1901019021903-type output = {
1904- is_fallback : bool option; (** If true, response has fallen-back to generic results, and is not scoped using relativeToDid *)
1905- rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
1906- suggestions : Jsont.json list;
1907}
19081909-(** Jsont codec for {!type:output}. *)
1910-val output_jsont : output Jsont.t
19111912- end
1913- module Block : sig
1914-(** Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details. *)
19151916-type main = {
1917- created_at : string;
1918- subject : string; (** DID of the account to be blocked. *)
1919}
19201921-(** Jsont codec for {!type:main}. *)
1922-val main_jsont : main Jsont.t
19231924- end
1925- module Listblock : sig
1926-(** Record representing a block relationship against an entire an entire list of accounts (actors). *)
19271928-type main = {
1929- created_at : string;
1930- subject : string; (** Reference (AT-URI) to the mod list record. *)
1931}
19321933-(** Jsont codec for {!type:main}. *)
1934-val main_jsont : main Jsont.t
19351936- end
1937- module MuteThread : sig
1938-(** Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth. *)
19391940-1941-type input = {
1942- root : string;
1943}
19441945-(** Jsont codec for {!type:input}. *)
1946-val input_jsont : input Jsont.t
19471948 end
1949- module GetFollowers : sig
1950-(** Enumerates accounts which follow a specified account (actor). *)
19511952(** Query/procedure parameters. *)
1953type params = {
1954- actor : string;
1955- cursor : string option;
1956- limit : int option;
1957}
19581959(** Jsont codec for {!type:params}. *)
···196119621963type output = {
1964- cursor : string option;
1965- followers : Jsont.json list;
1966- subject : Jsont.json;
1967}
19681969(** Jsont codec for {!type:output}. *)
1970val output_jsont : output Jsont.t
19711972 end
1973- module UnmuteThread : sig
1974-(** Unmutes the specified thread. Requires auth. *)
197519761977type input = {
1978- root : string;
0001979}
19801981(** Jsont codec for {!type:input}. *)
1982val input_jsont : input Jsont.t
19831984- end
1985- module Follow : sig
1986-(** Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView. *)
19871988-type main = {
1989- created_at : string;
1990- subject : string;
1991- via : Com.Atproto.Repo.StrongRef.main option;
1992-}
19931994-(** Jsont codec for {!type:main}. *)
1995-val main_jsont : main Jsont.t
19961997 end
1998- module UnmuteActor : sig
1999-(** Unmutes the specified account. Requires auth. *)
200020012002-type input = {
2003- actor : string;
2004-}
2005-2006-(** Jsont codec for {!type:input}. *)
2007-val input_jsont : input Jsont.t
2008-2009- end
2010- module MuteActorList : sig
2011-(** Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth. *)
2012-2013-2014-type input = {
2015- list_ : string;
2016-}
20172018-(** Jsont codec for {!type:input}. *)
2019-val input_jsont : input Jsont.t
20202021 end
2022- module UnmuteActorList : sig
2023-(** Unmutes the specified list of accounts. Requires auth. *)
2024-020252026-type input = {
2027- list_ : string;
02028}
20292030-(** Jsont codec for {!type:input}. *)
2031-val input_jsont : input Jsont.t
20322033 end
2034- module GetKnownFollowers : sig
2035-(** Enumerates accounts which follow a specified account (actor) and are followed by the viewer. *)
20362037-(** Query/procedure parameters. *)
2038-type params = {
2039- actor : string;
2040- cursor : string option;
2041- limit : int option;
2042}
20432044-(** Jsont codec for {!type:params}. *)
2045-val params_jsont : params Jsont.t
204620472048-type output = {
2049- cursor : string option;
2050- followers : Jsont.json list;
2051- subject : Jsont.json;
02052}
20532054-(** Jsont codec for {!type:output}. *)
2055-val output_jsont : output Jsont.t
2056-2057- end
2058- module MuteActor : sig
2059-(** Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth. *)
206020612062-type input = {
2063- actor : string;
2064}
20652066-(** Jsont codec for {!type:input}. *)
2067-val input_jsont : input Jsont.t
20682069- end
2070- module Listitem : sig
2071-(** Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records. *)
20722073type main = {
2074- created_at : string;
2075- list_ : string; (** Reference (AT-URI) to the list record (app.bsky.graph.list). *)
2076- subject : string; (** The account which is included on the list. *)
2077}
20782079(** Jsont codec for {!type:main}. *)
2080val main_jsont : main Jsont.t
20812082 end
2083- module Defs : sig
20842085-type starter_pack_view_basic = {
2086- cid : string;
2087- creator : Jsont.json;
2088- indexed_at : string;
2089- joined_all_time_count : int option;
2090- joined_week_count : int option;
2091- labels : Com.Atproto.Label.Defs.label list option;
2092- list_item_count : int option;
2093- record : Jsont.json;
2094- uri : string;
2095}
20962097-(** Jsont codec for {!type:starter_pack_view_basic}. *)
2098-val starter_pack_view_basic_jsont : starter_pack_view_basic Jsont.t
20992100-(** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) *)
21012102-type relationship = {
2103- blocked_by : string option; (** if the actor is blocked by this DID, contains the AT-URI of the block record *)
2104- blocked_by_list : string option; (** if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record *)
2105- blocking : string option; (** if the actor blocks this DID, this is the AT-URI of the block record *)
2106- blocking_by_list : string option; (** if the actor blocks this DID via a block list, this is the AT-URI of the listblock record *)
2107- did : string;
2108- followed_by : string option; (** if the actor is followed by this DID, contains the AT-URI of the follow record *)
2109- following : string option; (** if the actor follows this DID, this is the AT-URI of the follow record *)
2110}
21112112-(** Jsont codec for {!type:relationship}. *)
2113-val relationship_jsont : relationship Jsont.t
21142115-(** A list of actors used for only for reference purposes such as within a starter pack. *)
21162117-type referencelist = string
2118-val referencelist_jsont : referencelist Jsont.t
021192120-(** indicates that a handle or DID could not be resolved *)
021212122-type not_found_actor = {
2123- actor : string;
2124- not_found : bool;
2125}
21262127-(** Jsont codec for {!type:not_found_actor}. *)
2128-val not_found_actor_jsont : not_found_actor Jsont.t
00021292130-(** A list of actors to apply an aggregate moderation action (mute/block) on. *)
00000021312132-type modlist = string
2133-val modlist_jsont : modlist Jsont.t
213421352136-type list_viewer_state = {
2137- blocked : string option;
2138- muted : bool option;
2139}
21402141-(** Jsont codec for {!type:list_viewer_state}. *)
2142-val list_viewer_state_jsont : list_viewer_state Jsont.t
214321442145-type list_purpose = string
2146-val list_purpose_jsont : list_purpose Jsont.t
000021470021482149-type list_item_view = {
2150- subject : Jsont.json;
2151- uri : string;
0002152}
21532154-(** Jsont codec for {!type:list_item_view}. *)
2155-val list_item_view_jsont : list_item_view Jsont.t
021562157-(** A list of actors used for curation purposes such as list feeds or interaction gating. *)
00021582159-type curatelist = string
2160-val curatelist_jsont : curatelist Jsont.t
21610021622163-type list_view_basic = {
2164- avatar : string option;
2165 cid : string;
2166- indexed_at : string option;
02167 labels : Com.Atproto.Label.Defs.label list option;
2168- list_item_count : int option;
2169- name : string;
2170- purpose : Jsont.json;
02171 uri : string;
2172- viewer : Jsont.json option;
2173}
21742175-(** Jsont codec for {!type:list_view_basic}. *)
2176-val list_view_basic_jsont : list_view_basic Jsont.t
217721782179-type list_view = {
2180- avatar : string option;
2181- cid : string;
2182- creator : Jsont.json;
2183- description : string option;
2184- description_facets : Richtext.Facet.main list option;
2185- indexed_at : string;
2186- labels : Com.Atproto.Label.Defs.label list option;
2187- list_item_count : int option;
2188- name : string;
2189- purpose : Jsont.json;
2190 uri : string;
2191- viewer : Jsont.json option;
2192}
21932194-(** Jsont codec for {!type:list_view}. *)
2195-val list_view_jsont : list_view Jsont.t
219621972198-type starter_pack_view = {
2199- cid : string;
2200- creator : Jsont.json;
2201- feeds : Jsont.json list option;
2202- indexed_at : string;
2203- joined_all_time_count : int option;
2204- joined_week_count : int option;
2205- labels : Com.Atproto.Label.Defs.label list option;
2206- list_ : Jsont.json option;
2207- list_items_sample : Jsont.json list option;
2208- record : Jsont.json;
2209 uri : string;
2210}
22112212-(** Jsont codec for {!type:starter_pack_view}. *)
2213-val starter_pack_view_jsont : starter_pack_view Jsont.t
22142215- end
2216- module GetBlocks : sig
2217-(** Enumerates which accounts the requesting account is currently blocking. Requires auth. *)
22182219-(** Query/procedure parameters. *)
2220-type params = {
2221- cursor : string option;
2222- limit : int option;
2223}
22242225-(** Jsont codec for {!type:params}. *)
2226-val params_jsont : params Jsont.t
222722282229-type output = {
2230- blocks : Jsont.json list;
2231- cursor : string option;
2232}
22332234-(** Jsont codec for {!type:output}. *)
2235-val output_jsont : output Jsont.t
22362237- end
2238- module Verification : sig
2239-(** Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted. *)
22402241type main = {
2242- created_at : string; (** Date of when the verification was created. *)
2243- display_name : string; (** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. *)
2244- handle : string; (** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. *)
2245- subject : string; (** DID of the subject the verification applies to. *)
2246}
22472248(** Jsont codec for {!type:main}. *)
2249val main_jsont : main Jsont.t
22502251 end
2252- module GetMutes : sig
2253-(** Enumerates accounts that the requesting account (actor) currently has muted. Requires auth. *)
0022542255-(** Query/procedure parameters. *)
2256-type params = {
2257- cursor : string option;
2258- limit : int option;
2259}
22602261-(** Jsont codec for {!type:params}. *)
2262-val params_jsont : params Jsont.t
226322642265-type output = {
2266- cursor : string option;
2267- mutes : Jsont.json list;
2268-}
22692270(** Jsont codec for {!type:output}. *)
2271val output_jsont : output Jsont.t
22722273 end
2274- module GetRelationships : sig
2275-(** Enumerates public relationships between one account, and a list of other accounts. Does not require auth. *)
022762277-(** Query/procedure parameters. *)
2278-type params = {
2279- actor : string; (** Primary account requesting relationships for. *)
2280- others : string list option; (** List of 'other' accounts to be related back to the primary. *)
2281}
22822283-(** Jsont codec for {!type:params}. *)
2284-val params_jsont : params Jsont.t
228522862287-type output = {
2288- actor : string option;
2289- relationships : Jsont.json list;
2290-}
22912292(** Jsont codec for {!type:output}. *)
2293val output_jsont : output Jsont.t
22942295 end
2296- module GetStarterPacksWithMembership : sig
2297-(** A starter pack and an optional list item indicating membership of a target user to that starter pack. *)
2298-2299-type starter_pack_with_membership = {
2300- list_item : Jsont.json option;
2301- starter_pack : Jsont.json;
2302-}
2303-2304-(** Jsont codec for {!type:starter_pack_with_membership}. *)
2305-val starter_pack_with_membership_jsont : starter_pack_with_membership Jsont.t
2306-2307-(** Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth. *)
23082309(** Query/procedure parameters. *)
2310type params = {
2311- actor : string; (** The account (actor) to check for membership. *)
2312 cursor : string option;
2313 limit : int option;
2314}
···23192320type output = {
2321 cursor : string option;
2322- starter_packs_with_membership : Jsont.json list;
2323}
23242325(** Jsont codec for {!type:output}. *)
2326val output_jsont : output Jsont.t
23272328 end
2329- module GetActorStarterPacks : sig
2330-(** Get a list of starter packs created by the actor. *)
023312332-(** Query/procedure parameters. *)
2333-type params = {
2334- actor : string;
2335- cursor : string option;
2336- limit : int option;
2337}
23382339-(** Jsont codec for {!type:params}. *)
2340-val params_jsont : params Jsont.t
234123422343type output = {
2344- cursor : string option;
2345- starter_packs : Jsont.json list;
2346}
23472348(** Jsont codec for {!type:output}. *)
2349val output_jsont : output Jsont.t
23502351 end
2352- module List : sig
2353-(** Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists. *)
2354-2355-type main = {
2356- avatar : Atp.Blob_ref.t option;
2357- created_at : string;
2358- description : string option;
2359- description_facets : Richtext.Facet.main list option;
2360- labels : Com.Atproto.Label.Defs.self_labels option;
2361- name : string; (** Display name for list; can not be empty. *)
2362- purpose : Jsont.json; (** Defines the purpose of the list (aka, moderation-oriented or curration-oriented) *)
2363-}
23642365-(** Jsont codec for {!type:main}. *)
2366-val main_jsont : main Jsont.t
23672368- end
2369- module SearchStarterPacks : sig
2370-(** Find starter packs matching search criteria. Does not require auth. *)
23712372-(** Query/procedure parameters. *)
2373-type params = {
2374- cursor : string option;
2375- limit : int option;
2376- q : string; (** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. *)
2377-}
2378-2379-(** Jsont codec for {!type:params}. *)
2380-val params_jsont : params Jsont.t
238123822383-type output = {
2384- cursor : string option;
2385- starter_packs : Jsont.json list;
2386-}
23872388(** Jsont codec for {!type:output}. *)
2389val output_jsont : output Jsont.t
23902391 end
2392- module GetList : sig
2393-(** Gets a 'view' (with additional context) of a specified list. *)
23942395-(** Query/procedure parameters. *)
2396-type params = {
2397- cursor : string option;
2398- limit : int option;
2399- list_ : string; (** Reference (AT-URI) of the list record to hydrate. *)
000000002400}
24012402-(** Jsont codec for {!type:params}. *)
2403-val params_jsont : params Jsont.t
2404024052406-type output = {
2407- cursor : string option;
2408- items : Jsont.json list;
2409- list_ : Jsont.json;
2410}
24112412-(** Jsont codec for {!type:output}. *)
2413-val output_jsont : output Jsont.t
24142415 end
2416- module GetListBlocks : sig
2417-(** Get mod lists that the requesting account (actor) is blocking. Requires auth. *)
024182419-(** Query/procedure parameters. *)
2420-type params = {
2421- cursor : string option;
2422- limit : int option;
2423}
24242425-(** Jsont codec for {!type:params}. *)
2426-val params_jsont : params Jsont.t
242724282429-type output = {
2430- cursor : string option;
2431- lists : Jsont.json list;
2432-}
24332434(** Jsont codec for {!type:output}. *)
2435val output_jsont : output Jsont.t
24362437 end
2438- module GetStarterPack : sig
2439-(** Gets a view of a starter pack. *)
24402441(** Query/procedure parameters. *)
2442-type params = {
2443- starter_pack : string; (** Reference (AT-URI) of the starter pack record. *)
2444-}
24452446(** Jsont codec for {!type:params}. *)
2447val params_jsont : params Jsont.t
244824492450type output = {
2451- starter_pack : Jsont.json;
2452}
24532454(** Jsont codec for {!type:output}. *)
2455val output_jsont : output Jsont.t
24562457 end
2458- module GetListsWithMembership : sig
2459-(** A list and an optional list item indicating membership of a target user to that list. *)
24602461-type list_with_membership = {
2462- list_ : Jsont.json;
2463- list_item : Jsont.json option;
2464-}
24652466-(** Jsont codec for {!type:list_with_membership}. *)
2467-val list_with_membership_jsont : list_with_membership Jsont.t
2468-2469-(** Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth. *)
2470-2471-(** Query/procedure parameters. *)
2472-type params = {
2473- actor : string; (** The account (actor) to check for membership. *)
2474- cursor : string option;
2475- limit : int option;
2476- purposes : string list option; (** Optional filter by list purpose. If not specified, all supported types are returned. *)
2477}
24782479-(** Jsont codec for {!type:params}. *)
2480-val params_jsont : params Jsont.t
248124822483type output = {
2484- cursor : string option;
2485- lists_with_membership : Jsont.json list;
2486}
24872488(** Jsont codec for {!type:output}. *)
2489val output_jsont : output Jsont.t
24902491 end
2492- module GetListMutes : sig
2493-(** Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth. *)
0024942495-(** Query/procedure parameters. *)
2496-type params = {
2497- cursor : string option;
2498- limit : int option;
2499}
25002501-(** Jsont codec for {!type:params}. *)
2502-val params_jsont : params Jsont.t
25030025042505-type output = {
2506- cursor : string option;
2507- lists : Jsont.json list;
2508}
25092510-(** Jsont codec for {!type:output}. *)
2511-val output_jsont : output Jsont.t
25122513- end
2514- module GetStarterPacks : sig
2515-(** Get views for a list of starter packs. *)
25162517-(** Query/procedure parameters. *)
2518-type params = {
2519- uris : string list;
2520}
25212522-(** Jsont codec for {!type:params}. *)
2523-val params_jsont : params Jsont.t
00252425252526type output = {
2527- starter_packs : Jsont.json list;
002528}
25292530(** Jsont codec for {!type:output}. *)
2531val output_jsont : output Jsont.t
25322533 end
2534- module GetLists : sig
2535-(** Enumerates the lists created by a specified account (actor). *)
25362537-(** Query/procedure parameters. *)
2538-type params = {
2539- actor : string; (** The account (actor) to enumerate lists from. *)
2540- cursor : string option;
2541- limit : int option;
2542- purposes : string list option; (** Optional filter by list purpose. If not specified, all supported types are returned. *)
2543-}
25442545-(** Jsont codec for {!type:params}. *)
2546-val params_jsont : params Jsont.t
2547025482549-type output = {
2550- cursor : string option;
2551- lists : Jsont.json list;
2552}
25532554-(** Jsont codec for {!type:output}. *)
2555-val output_jsont : output Jsont.t
25562557- end
2558- end
2559- module Feed : sig
2560- module Post : sig
2561-(** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. *)
25622563-type text_slice = {
2564- end_ : int;
2565- start : int;
2566-}
25672568-(** Jsont codec for {!type:text_slice}. *)
2569-val text_slice_jsont : text_slice Jsont.t
2570025712572-type reply_ref = {
2573- parent : Com.Atproto.Repo.StrongRef.main;
2574- root : Com.Atproto.Repo.StrongRef.main;
2575-}
25762577-(** Jsont codec for {!type:reply_ref}. *)
2578-val reply_ref_jsont : reply_ref Jsont.t
25792580-(** Deprecated: use facets instead. *)
25812582-type entity = {
2583- index : Jsont.json;
2584- type_ : string; (** Expected values are 'mention' and 'link'. *)
2585- value : string;
02586}
25872588-(** Jsont codec for {!type:entity}. *)
2589-val entity_jsont : entity Jsont.t
25902591-(** Record containing a Bluesky post. *)
0025922593type main = {
2594- created_at : string; (** Client-declared timestamp when this post was originally created. *)
2595- embed : Jsont.json option;
2596- entities : Jsont.json list option; (** DEPRECATED: replaced by app.bsky.richtext.facet. *)
2597- facets : Richtext.Facet.main list option; (** Annotations of text (mentions, URLs, hashtags, etc) *)
2598- labels : Com.Atproto.Label.Defs.self_labels option; (** Self-label values for this post. Effectively content warnings. *)
2599- langs : string list option; (** Indicates human language of post primary text content. *)
2600- reply : Jsont.json option;
2601- tags : string list option; (** Additional hashtags, in addition to any included in post text and facets. *)
2602- text : string; (** The primary post content. May be an empty string, if there are embeds. *)
2603}
26042605(** Jsont codec for {!type:main}. *)
···2642val output_jsont : output Jsont.t
26432644 end
2645- module Postgate : sig
2646-(** Disables embedding of this post. *)
2647-2648-type disable_rule = unit
2649-2650-(** Jsont codec for {!type:disable_rule}. *)
2651-val disable_rule_jsont : disable_rule Jsont.t
2652-2653-(** Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository. *)
2654-2655-type main = {
2656- created_at : string;
2657- detached_embedding_uris : string list option; (** List of AT-URIs embedding this post that the author has detached from. *)
2658- embedding_rules : Jsont.json list option; (** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. *)
2659- post : string; (** Reference (AT-URI) to the post record. *)
2660-}
2661-2662-(** Jsont codec for {!type:main}. *)
2663-val main_jsont : main Jsont.t
2664-2665- end
2666 module GetRepostedBy : sig
2667(** Get a list of reposts for a given post. *)
2668···2689val output_jsont : output Jsont.t
26902691 end
2692- module DescribeFeedGenerator : sig
026932694-type links = {
2695- privacy_policy : string option;
2696- terms_of_service : string option;
2697-}
2698-2699-(** Jsont codec for {!type:links}. *)
2700-val links_jsont : links Jsont.t
2701-2702-2703-type feed = {
2704- uri : string;
2705-}
2706-2707-(** Jsont codec for {!type:feed}. *)
2708-val feed_jsont : feed Jsont.t
2709-2710-(** Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View). *)
2711-2712-2713-type output = {
2714 did : string;
2715- feeds : Jsont.json list;
2716- links : Jsont.json option;
2717}
27182719-(** Jsont codec for {!type:output}. *)
2720-val output_jsont : output Jsont.t
27212722 end
2723- module Threadgate : sig
2724-(** Allow replies from actors mentioned in your post. *)
2725-2726-type mention_rule = unit
2727-2728-(** Jsont codec for {!type:mention_rule}. *)
2729-val mention_rule_jsont : mention_rule Jsont.t
2730-2731-(** Allow replies from actors on a list. *)
2732-2733-type list_rule = {
2734- list_ : string;
2735-}
2736-2737-(** Jsont codec for {!type:list_rule}. *)
2738-val list_rule_jsont : list_rule Jsont.t
2739-2740-(** Allow replies from actors you follow. *)
2741-2742-type following_rule = unit
2743-2744-(** Jsont codec for {!type:following_rule}. *)
2745-val following_rule_jsont : following_rule Jsont.t
27462747-(** Allow replies from actors who follow you. *)
27482749-type follower_rule = unit
027502751-(** Jsont codec for {!type:follower_rule}. *)
2752-val follower_rule_jsont : follower_rule Jsont.t
2753-2754-(** Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository. *)
27552756type main = {
2757- allow : Jsont.json list option; (** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. *)
2758 created_at : string;
2759- hidden_replies : string list option; (** List of hidden reply URIs. *)
02760 post : string; (** Reference (AT-URI) to the post record. *)
2761-}
2762-2763-(** Jsont codec for {!type:main}. *)
2764-val main_jsont : main Jsont.t
2765-2766- end
2767- module Like : sig
2768-(** Record declaring a 'like' of a piece of subject content. *)
2769-2770-type main = {
2771- created_at : string;
2772- subject : Com.Atproto.Repo.StrongRef.main;
2773- via : Com.Atproto.Repo.StrongRef.main option;
2774}
27752776(** Jsont codec for {!type:main}. *)
···3048val feed_view_post_jsont : feed_view_post Jsont.t
30493050 end
3051- module Repost : sig
3052-(** Record representing a 'repost' of an existing Bluesky post. *)
00000000000000000000000000000030533054type main = {
3055- created_at : string;
3056- subject : Com.Atproto.Repo.StrongRef.main;
3057- via : Com.Atproto.Repo.StrongRef.main option;
0000003058}
30593060(** Jsont codec for {!type:main}. *)
3061val main_jsont : main Jsont.t
30623063 end
3064- module Generator : sig
3065-(** Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository. *)
30663067-type main = {
3068- accepts_interactions : bool option; (** Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions *)
3069- avatar : Atp.Blob_ref.t option;
3070- content_mode : string option;
3071- created_at : string;
3072- description : string option;
3073- description_facets : Richtext.Facet.main list option;
3074- did : string;
3075- display_name : string;
3076- labels : Com.Atproto.Label.Defs.self_labels option; (** Self-label values *)
03077}
30783079-(** Jsont codec for {!type:main}. *)
3080-val main_jsont : main Jsont.t
30813082 end
3083- module GetPostThread : sig
3084-(** Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests. *)
30853086(** Query/procedure parameters. *)
3087type params = {
3088- depth : int option; (** How many levels of reply depth should be included in response. *)
3089- parent_height : int option; (** How many levels of parent (and grandparent, etc) post to include. *)
3090- uri : string; (** Reference (AT-URI) to post record. *)
03091}
30923093(** Jsont codec for {!type:params}. *)
···309530963097type output = {
3098- thread : Jsont.json;
3099- threadgate : Jsont.json option;
003100}
31013102(** Jsont codec for {!type:output}. *)
3103val output_jsont : output Jsont.t
31043105 end
3106- module GetFeed : sig
3107-(** Get a hydrated feed from an actor's selected feed generator. Implemented by App View. *)
31083109(** Query/procedure parameters. *)
3110type params = {
03111 cursor : string option;
3112- feed : string;
03113 limit : int option;
3114}
3115···3126val output_jsont : output Jsont.t
31273128 end
3129- module GetQuotes : sig
3130-(** Get a list of quotes for a given post. *)
31313132(** Query/procedure parameters. *)
3133type params = {
3134- cid : string option; (** If supplied, filters to quotes of specific version (by CID) of the post record. *)
3135 cursor : string option;
3136 limit : int option;
3137- uri : string; (** Reference (AT-URI) of post record *)
3138}
31393140(** Jsont codec for {!type:params}. *)
···314231433144type output = {
3145- cid : string option;
3146 cursor : string option;
3147- posts : Jsont.json list;
3148- uri : string;
3149}
31503151(** Jsont codec for {!type:output}. *)
···3175val output_jsont : output Jsont.t
31763177 end
00000000000000000000003178 module GetActorLikes : sig
3179(** Get a list of posts liked by an actor. Requires auth, actor must be the requesting account. *)
3180···3198val output_jsont : output Jsont.t
31993200 end
3201- module GetFeedSkeleton : sig
3202-(** Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service. *)
32033204(** Query/procedure parameters. *)
3205type params = {
03206 cursor : string option;
3207- feed : string; (** Reference to feed generator record describing the specific feed being requested. *)
3208 limit : int option;
3209}
3210···32143215type output = {
3216 cursor : string option;
3217- feed : Jsont.json list;
3218- req_id : string option; (** Unique identifier per request that may be passed back alongside interactions. *)
3219}
32203221(** Jsont codec for {!type:output}. *)
···3255val output_jsont : output Jsont.t
32563257 end
00000000000000000000000000000000000000000000000000000000000000000000003258 module GetFeedGenerators : sig
3259(** Get information about a list of feed generators. *)
3260···3293val output_jsont : output Jsont.t
32943295 end
3296- module GetAuthorFeed : sig
3297-(** Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth. *)
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000032983299(** Query/procedure parameters. *)
3300type params = {
3301 actor : string;
3302 cursor : string option;
3303- filter : string option; (** Combinations of post/repost types to include in response. *)
3304- include_pins : bool option;
3305 limit : int option;
3306}
3307···33113312type output = {
3313 cursor : string option;
3314- feed : Jsont.json list;
03315}
33163317(** Jsont codec for {!type:output}. *)
3318val output_jsont : output Jsont.t
33193320 end
3321- module GetFeedGenerator : sig
3322-(** Get information about a feed generator. Implemented by AppView. *)
00000000000000000000000033233324(** Query/procedure parameters. *)
3325type params = {
3326- feed : string; (** AT-URI of the feed generator record. *)
03327}
33283329(** Jsont codec for {!type:params}. *)
···333133323333type output = {
3334- is_online : bool; (** Indicates whether the feed generator service has been online recently, or else seems to be inactive. *)
3335- is_valid : bool; (** Indicates whether the feed generator service is compatible with the record declaration. *)
3336- view : Jsont.json;
3337}
33383339(** Jsont codec for {!type:output}. *)
3340val output_jsont : output Jsont.t
33413342 end
3343- module GetSuggestedFeeds : sig
3344-(** Get a list of suggested feeds (feed generators) for the requesting account. *)
000000000000000000000000000000000000000000000000000000000000033453346(** Query/procedure parameters. *)
3347type params = {
03348 cursor : string option;
3349 limit : int option;
3350}
···33553356type output = {
3357 cursor : string option;
3358- feeds : Jsont.json list;
03359}
33603361(** Jsont codec for {!type:output}. *)
3362val output_jsont : output Jsont.t
33633364 end
3365- module GetActorFeeds : sig
3366-(** Get a list of feeds (feed generator records) created by the actor (in the actor's repo). *)
00000000000000000000000000000000000000000000000033673368(** Query/procedure parameters. *)
3369type params = {
···33783379type output = {
3380 cursor : string option;
3381- feeds : Jsont.json list;
03382}
33833384(** Jsont codec for {!type:output}. *)
3385val output_jsont : output Jsont.t
33863387 end
3388- module GetPosts : sig
3389-(** Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'. *)
00000000000033903391(** Query/procedure parameters. *)
3392type params = {
3393- uris : string list; (** List of post AT-URIs to return hydrated views for. *)
03394}
33953396(** Jsont codec for {!type:params}. *)
···339833993400type output = {
3401- posts : Jsont.json list;
03402}
34033404(** Jsont codec for {!type:output}. *)
3405val output_jsont : output Jsont.t
34063407 end
3408- module GetTimeline : sig
3409-(** Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed. *)
000000000034103411(** Query/procedure parameters. *)
3412type params = {
3413- algorithm : string option; (** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. *)
3414 cursor : string option;
3415 limit : int option;
03416}
34173418(** Jsont codec for {!type:params}. *)
···34213422type output = {
3423 cursor : string option;
3424- feed : Jsont.json list;
3425}
34263427(** Jsont codec for {!type:output}. *)
3428val output_jsont : output Jsont.t
34293430 end
3431- end
3432- module Bookmark : sig
3433- module DeleteBookmark : sig
3434-(** Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication. *)
000034350034363437-type input = {
3438- uri : string;
003439}
34403441-(** Jsont codec for {!type:input}. *)
3442-val input_jsont : input Jsont.t
34433444 end
3445- module CreateBookmark : sig
3446-(** Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication. *)
344700000034483449-type input = {
3450- cid : string;
3451- uri : string;
00003452}
34533454-(** Jsont codec for {!type:input}. *)
3455-val input_jsont : input Jsont.t
34563457 end
3458- module Defs : sig
034593460-type bookmark_view = {
3461- created_at : string option;
3462- item : Jsont.json;
3463- subject : Com.Atproto.Repo.StrongRef.main; (** A strong ref to the bookmarked record. *)
03464}
34653466-(** Jsont codec for {!type:bookmark_view}. *)
3467-val bookmark_view_jsont : bookmark_view Jsont.t
34683469-(** Object used to store bookmark data in stash. *)
34703471-type bookmark = {
3472- subject : Com.Atproto.Repo.StrongRef.main; (** A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported. *)
03473}
34743475-(** Jsont codec for {!type:bookmark}. *)
3476-val bookmark_jsont : bookmark Jsont.t
34773478 end
3479- module GetBookmarks : sig
3480-(** Gets views of records bookmarked by the authenticated user. Requires authentication. *)
000000000034813482(** Query/procedure parameters. *)
3483type params = {
03484 cursor : string option;
3485 limit : int option;
3486}
···349034913492type output = {
3493- bookmarks : Defs.bookmark_view list;
3494 cursor : string option;
03495}
34963497(** Jsont codec for {!type:output}. *)
3498val output_jsont : output Jsont.t
34993500 end
3501- end
3502- module Unspecced : sig
3503- module GetSuggestedUsersSkeleton : sig
3504-(** Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers *)
35053506(** Query/procedure parameters. *)
3507type params = {
3508- category : string option; (** Category of users to get suggestions for. *)
03509 limit : int option;
3510- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3511}
35123513(** Jsont codec for {!type:params}. *)
···351535163517type output = {
3518- dids : string list;
3519- rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
3520}
35213522(** Jsont codec for {!type:output}. *)
3523val output_jsont : output Jsont.t
35243525 end
3526- module GetOnboardingSuggestedStarterPacks : sig
3527-(** Get a list of suggested starterpacks for onboarding *)
35283529(** Query/procedure parameters. *)
3530type params = {
0000000000000000000003531 limit : int option;
3532}
3533···353635373538type output = {
0000000000000000000003539 starter_packs : Jsont.json list;
3540}
3541···3543val output_jsont : output Jsont.t
35443545 end
3546- module GetPopularFeedGenerators : sig
3547-(** An unspecced view of globally popular feed generators. *)
35483549(** Query/procedure parameters. *)
3550type params = {
3551 cursor : string option;
3552 limit : int option;
3553- query : string option;
3554}
35553556(** Jsont codec for {!type:params}. *)
···35593560type output = {
3561 cursor : string option;
3562- feeds : Jsont.json list;
03563}
35643565(** Jsont codec for {!type:output}. *)
3566val output_jsont : output Jsont.t
35673568 end
3569- module GetSuggestedStarterPacksSkeleton : sig
3570-(** Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks *)
0000000000000000000000000000000000000000000000000000000000000000035713572(** Query/procedure parameters. *)
3573type params = {
03574 limit : int option;
3575- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3576}
35773578(** Jsont codec for {!type:params}. *)
···358035813582type output = {
3583- starter_packs : string list;
03584}
35853586(** Jsont codec for {!type:output}. *)
3587val output_jsont : output Jsont.t
35883589 end
003590 module GetSuggestedFeeds : sig
3591(** Get a list of suggested feeds *)
3592···3607val output_jsont : output Jsont.t
36083609 end
3610- module GetSuggestedFeedsSkeleton : sig
3611-(** Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds *)
36123613(** Query/procedure parameters. *)
3614type params = {
03615 limit : int option;
3616- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3617}
36183619(** Jsont codec for {!type:params}. *)
···362136223623type output = {
3624- feeds : string list;
03625}
36263627(** Jsont codec for {!type:output}. *)
3628val output_jsont : output Jsont.t
36293630 end
3631- module GetConfig : sig
036323633-type live_now_config = {
3634- did : string;
3635- domains : string list;
003636}
36373638-(** Jsont codec for {!type:live_now_config}. *)
3639-val live_now_config_jsont : live_now_config Jsont.t
3640-3641-(** Get miscellaneous runtime configuration. *)
364236433644type output = {
3645- check_email_confirmed : bool option;
3646- live_now : live_now_config list option;
3647}
36483649(** Jsont codec for {!type:output}. *)
···3670val output_jsont : output Jsont.t
36713672 end
3673- module GetSuggestedUsers : sig
3674-(** Get a list of suggested users *)
36753676(** Query/procedure parameters. *)
3677type params = {
3678- category : string option; (** Category of users to get suggestions for. *)
3679 limit : int option;
03680}
36813682(** Jsont codec for {!type:params}. *)
···368436853686type output = {
3687- actors : Jsont.json list;
3688- rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
3689}
36903691(** Jsont codec for {!type:output}. *)
3692val output_jsont : output Jsont.t
36933694 end
3695- module GetOnboardingSuggestedStarterPacksSkeleton : sig
3696-(** Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks *)
36973698(** Query/procedure parameters. *)
3699type params = {
···370637073708type output = {
3709- starter_packs : string list;
000000000000000000003710}
37113712(** Jsont codec for {!type:output}. *)
···3839val age_assurance_event_jsont : age_assurance_event Jsont.t
38403841 end
3842- module GetTaggedSuggestions : sig
38433844-type suggestion = {
3845- subject : string;
3846- subject_type : string;
3847- tag : string;
3848}
38493850-(** Jsont codec for {!type:suggestion}. *)
3851-val suggestion_jsont : suggestion Jsont.t
38523853-(** Get a list of suggestions (feeds and users) tagged with categories *)
3854-3855-(** Query/procedure parameters. *)
3856-type params = unit
3857-3858-(** Jsont codec for {!type:params}. *)
3859-val params_jsont : params Jsont.t
386038613862type output = {
3863- suggestions : suggestion list;
03864}
38653866(** Jsont codec for {!type:output}. *)
3867val output_jsont : output Jsont.t
38683869 end
3870- module SearchPostsSkeleton : sig
3871-(** Backend Posts search, returns only skeleton *)
38723873(** Query/procedure parameters. *)
3874type params = {
3875- author : string option; (** Filter to posts by the given account. Handles are resolved to DID before query-time. *)
3876- cursor : string option; (** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. *)
3877- domain : string option; (** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. *)
3878- lang : string option; (** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. *)
3879 limit : int option;
3880- mentions : string option; (** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. *)
3881- q : string; (** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. *)
3882- since : string option; (** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). *)
3883- sort : string option; (** Specifies the ranking order of results. *)
3884- tag : string list option; (** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. *)
3885- until : string option; (** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). *)
3886- url : string option; (** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. *)
3887- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries. *)
3888}
38893890(** Jsont codec for {!type:params}. *)
···38933894type output = {
3895 cursor : string option;
3896- hits_total : int option; (** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. *)
3897- posts : Defs.skeleton_search_post list;
3898}
38993900(** Jsont codec for {!type:output}. *)
3901val output_jsont : output Jsont.t
39023903 end
3904- module GetPostThreadV2 : sig
39053906-type thread_item = {
3907- depth : int; (** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. *)
3908- uri : string;
3909- value : Jsont.json;
3910}
39113912-(** Jsont codec for {!type:thread_item}. *)
3913-val thread_item_jsont : thread_item Jsont.t
39143915-(** (NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests. *)
39163917(** Query/procedure parameters. *)
3918-type params = {
3919- above : bool option; (** Whether to include parents above the anchor. *)
3920- anchor : string; (** Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post. *)
3921- below : int option; (** How many levels of replies to include below the anchor. *)
3922- branching_factor : int option; (** Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated). *)
3923- sort : string option; (** Sorting for the thread replies. *)
3924-}
39253926(** Jsont codec for {!type:params}. *)
3927val params_jsont : params Jsont.t
392839293930type output = {
3931- has_other_replies : bool; (** Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them. *)
3932- thread : thread_item list; (** A flat list of thread items. The depth of each item is indicated by the depth property inside the item. *)
3933- threadgate : Jsont.json option;
3934}
39353936(** Jsont codec for {!type:output}. *)
3937val output_jsont : output Jsont.t
39383939 end
3940- module GetTrendingTopics : sig
3941-(** Get a list of trending topics *)
39423943(** Query/procedure parameters. *)
3944type params = {
3945 limit : int option;
3946- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. *)
3947}
39483949(** Jsont codec for {!type:params}. *)
···395139523953type output = {
3954- suggested : Defs.trending_topic list;
3955- topics : Defs.trending_topic list;
3956}
39573958(** Jsont codec for {!type:output}. *)
3959val output_jsont : output Jsont.t
39603961 end
3962- module GetTrendsSkeleton : sig
3963-(** Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends *)
0000000000000000000039643965(** Query/procedure parameters. *)
3966type params = {
3967 limit : int option;
3968- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3969}
39703971(** Jsont codec for {!type:params}. *)
···397339743975type output = {
3976- trends : Defs.skeleton_trend list;
03977}
39783979(** Jsont codec for {!type:output}. *)
···4010val output_jsont : output Jsont.t
40114012 end
4013- module InitAgeAssurance : sig
4014-(** Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age. *)
0000000040150040164017-type input = {
4018- country_code : string; (** An ISO 3166-1 alpha-2 code of the user's location. *)
4019- email : string; (** The user's email address to receive assurance instructions. *)
4020- language : string; (** The user's preferred language for communication during the assurance process. *)
004021}
40224023-(** Jsont codec for {!type:input}. *)
4024-val input_jsont : input Jsont.t
0000402540264027type output = Defs.age_assurance_state
···4055val output_jsont : output Jsont.t
40564057 end
00000000000000000000000000000000004058 module SearchActorsSkeleton : sig
4059(** Backend Actors (profile) search, returns only skeleton. *)
4060···4081val output_jsont : output Jsont.t
40824083 end
4084- module GetAgeAssuranceState : sig
4085-(** Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required. *)
4086000040874088-type output = Defs.age_assurance_state
00000040894090(** Jsont codec for {!type:output}. *)
4091val output_jsont : output Jsont.t
40924093 end
4094- module GetSuggestionsSkeleton : sig
4095-(** Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions *)
000000000040964097(** Query/procedure parameters. *)
4098type params = {
4099- cursor : string option;
4100- limit : int option;
4101- relative_to_did : string option; (** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. *)
4102- viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. *)
04103}
41044105(** Jsont codec for {!type:params}. *)
···410741084109type output = {
4110- actors : Defs.skeleton_search_actor list;
4111- cursor : string option;
4112- rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
4113- relative_to_did : string option; (** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. *)
4114}
41154116(** Jsont codec for {!type:output}. *)
4117val output_jsont : output Jsont.t
41184119 end
4120- module GetTrends : sig
4121-(** Get the current trends on the network *)
41224123(** Query/procedure parameters. *)
4124type params = {
4125 limit : int option;
04126}
41274128(** Jsont codec for {!type:params}. *)
···413041314132type output = {
4133- trends : Defs.trend_view list;
4134}
41354136(** Jsont codec for {!type:output}. *)
···1213module Com : sig
14 module Atproto : sig
15+ module Moderation : sig
16+ module Defs : sig
17+(** Tag describing a type of subject that might be reported. *)
18+19+type subject_type = string
20+val subject_type_jsont : subject_type Jsont.t
21+22+(** Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`. *)
23+24+type reason_violation = string
25+val reason_violation_jsont : reason_violation Jsont.t
26+27+28+type reason_type = string
29+val reason_type_jsont : reason_type Jsont.t
30+31+(** Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`. *)
32+33+type reason_spam = string
34+val reason_spam_jsont : reason_spam Jsont.t
35+36+(** Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`. *)
37+38+type reason_sexual = string
39+val reason_sexual_jsont : reason_sexual Jsont.t
40+41+(** Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`. *)
42+43+type reason_rude = string
44+val reason_rude_jsont : reason_rude Jsont.t
45+46+(** Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`. *)
47+48+type reason_other = string
49+val reason_other_jsont : reason_other Jsont.t
50+51+(** Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`. *)
52+53+type reason_misleading = string
54+val reason_misleading_jsont : reason_misleading Jsont.t
55+56+(** Appeal a previously taken moderation action *)
57+58+type reason_appeal = string
59+val reason_appeal_jsont : reason_appeal Jsont.t
60+61+ end
62+ end
63 module Label : sig
64 module Defs : sig
65(** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. *)
···141142 end
143 end
000000000000000000000000000000000000000000000000144 end
145end
146module App : sig
147 module Bsky : sig
148+ module AuthFullApp : sig
149150type main = unit
151val main_jsont : main Jsont.t
···157val main_jsont : main Jsont.t
158159 end
000000000000000000000000000000000000000000000000000000000160 module AuthManageFeedDeclarations : sig
161162type main = unit
163val main_jsont : main Jsont.t
164165 end
166+ module AuthManageNotifications : sig
167168type main = unit
169val main_jsont : main Jsont.t
170171 end
172+ module AuthManageModeration : sig
173174type main = unit
175val main_jsont : main Jsont.t
···181val main_jsont : main Jsont.t
182183 end
184+ module AuthManageLabelerService : sig
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000185186type main = unit
187val main_jsont : main Jsont.t
188189 end
190+ module Notification : sig
191+ module GetUnreadCount : sig
192+(** Count the number of unread notifications for the requesting account. Requires auth. *)
000000000000000000000000000000000000000000000000193194(** Query/procedure parameters. *)
195type params = {
196+ priority : bool option;
197+ seen_at : string option;
198}
199200(** Jsont codec for {!type:params}. *)
···202203204type output = {
205+ count : int;
206}
207208(** Jsont codec for {!type:output}. *)
209val output_jsont : output Jsont.t
210211 end
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000212 module UpdateSeen : sig
213(** Notify server that the requesting account has seen notifications. Requires auth. *)
214···221val input_jsont : input Jsont.t
222223 end
0000000000000000224 module ListNotifications : sig
225226type notification = {
···264val output_jsont : output Jsont.t
265266 end
267+ module Declaration : sig
268+(** A declaration of the user's choices related to notifications that can be produced by them. *)
269270+type main = {
271+ allow_subscriptions : string; (** A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'. *)
00272}
273274+(** Jsont codec for {!type:main}. *)
275+val main_jsont : main Jsont.t
276+277+ end
278+ module PutPreferences : sig
279+(** Set notification-related preferences for an account. Requires auth. *)
280281282+type input = {
283+ priority : bool;
284}
285286+(** Jsont codec for {!type:input}. *)
287+val input_jsont : input Jsont.t
288289 end
290+ module RegisterPush : sig
291+(** Register to receive push notifications, via a specified service, for the requesting account. Requires auth. *)
292293294type input = {
295+ age_restricted : bool option; (** Set to true when the actor is age restricted *)
296 app_id : string;
297 platform : string;
298 service_did : string;
···303val input_jsont : input Jsont.t
304305 end
306+ module UnregisterPush : sig
307+(** The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth. *)
308309310type input = {
311+ app_id : string;
312+ platform : string;
313+ service_did : string;
314+ token : string;
315}
316317(** Jsont codec for {!type:input}. *)
···391392(** Jsont codec for {!type:preferences}. *)
393val preferences_jsont : preferences Jsont.t
00000000000394395 end
396 module ListActivitySubscriptions : sig
···488489 end
490 end
491+ module Labeler : sig
492+ module Defs : sig
493+494+type labeler_viewer_state = {
495+ like : string option;
496+}
497+498+(** Jsont codec for {!type:labeler_viewer_state}. *)
499+val labeler_viewer_state_jsont : labeler_viewer_state Jsont.t
500+501+502+type labeler_policies = {
503+ label_value_definitions : Com.Atproto.Label.Defs.label_value_definition list option; (** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. *)
504+ label_values : Com.Atproto.Label.Defs.label_value list; (** The label values which this labeler publishes. May include global or custom labels. *)
505+}
506+507+(** Jsont codec for {!type:labeler_policies}. *)
508+val labeler_policies_jsont : labeler_policies Jsont.t
509+510+511+type labeler_view_detailed = {
512+ cid : string;
513+ creator : Jsont.json;
514+ indexed_at : string;
515+ labels : Com.Atproto.Label.Defs.label list option;
516+ like_count : int option;
517+ policies : Jsont.json;
518+ reason_types : Com.Atproto.Moderation.Defs.reason_type list option; (** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. *)
519+ subject_collections : string list option; (** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. *)
520+ subject_types : Com.Atproto.Moderation.Defs.subject_type list option; (** The set of subject types (account, record, etc) this service accepts reports on. *)
521+ uri : string;
522+ viewer : Jsont.json option;
523+}
524+525+(** Jsont codec for {!type:labeler_view_detailed}. *)
526+val labeler_view_detailed_jsont : labeler_view_detailed Jsont.t
527+528+529+type labeler_view = {
530+ cid : string;
531+ creator : Jsont.json;
532+ indexed_at : string;
533+ labels : Com.Atproto.Label.Defs.label list option;
534+ like_count : int option;
535+ uri : string;
536+ viewer : Jsont.json option;
537+}
538+539+(** Jsont codec for {!type:labeler_view}. *)
540+val labeler_view_jsont : labeler_view Jsont.t
541+542+ end
543+ module Service : sig
544+(** A declaration of the existence of labeler service. *)
545+546+type main = {
547+ created_at : string;
548+ labels : Com.Atproto.Label.Defs.self_labels option;
549+ policies : Jsont.json;
550+ reason_types : Com.Atproto.Moderation.Defs.reason_type list option; (** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. *)
551+ subject_collections : string list option; (** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. *)
552+ subject_types : Com.Atproto.Moderation.Defs.subject_type list option; (** The set of subject types (account, record, etc) this service accepts reports on. *)
553+}
554+555+(** Jsont codec for {!type:main}. *)
556+val main_jsont : main Jsont.t
557+558+ end
559+ module GetServices : sig
560+(** Get information about a list of labeler services. *)
561+562+(** Query/procedure parameters. *)
563+type params = {
564+ detailed : bool option;
565+ dids : string list;
566+}
567+568+(** Jsont codec for {!type:params}. *)
569+val params_jsont : params Jsont.t
570+571+572+type output = {
573+ views : Jsont.json list;
574+}
575+576+(** Jsont codec for {!type:output}. *)
577+val output_jsont : output Jsont.t
578+579+ end
580+ end
581 module Actor : sig
582 module Status : sig
583(** Advertises an account as currently offering live content. *)
···592 duration_minutes : int option; (** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. *)
593 embed : Jsont.json option; (** An optional embed associated with the status. *)
594 status : string; (** The status for the account. *)
00000000000000000000595}
596597(** Jsont codec for {!type:main}. *)
···961val known_followers_jsont : known_followers Jsont.t
962963 end
964+ module Profile : sig
965+(** A declaration of a Bluesky account profile. *)
966967+type main = {
968+ avatar : Atp.Blob_ref.t option; (** Small image to be displayed next to posts from account. AKA, 'profile picture' *)
969+ banner : Atp.Blob_ref.t option; (** Larger horizontal image to display behind profile view. *)
970+ created_at : string option;
971+ description : string option; (** Free-form profile description text. *)
972+ display_name : string option;
973+ joined_via_starter_pack : Com.Atproto.Repo.StrongRef.main option;
974+ labels : Com.Atproto.Label.Defs.self_labels option; (** Self-label values, specific to the Bluesky application, on the overall account. *)
975+ pinned_post : Com.Atproto.Repo.StrongRef.main option;
976+ pronouns : string option; (** Free-form pronouns text. *)
977+ website : string option;
978}
979980+(** Jsont codec for {!type:main}. *)
981+val main_jsont : main Jsont.t
982983 end
984+ module GetProfiles : sig
985+(** Get detailed profile views of multiple actors. *)
986987(** Query/procedure parameters. *)
988type params = {
989+ actors : string list;
00990}
991992(** Jsont codec for {!type:params}. *)
···994995996type output = {
997+ profiles : Jsont.json list;
998}
9991000(** Jsont codec for {!type:output}. *)
1001val output_jsont : output Jsont.t
10021003 end
1004+ module GetPreferences : sig
1005+(** Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth. *)
10061007(** Query/procedure parameters. *)
1008+type params = unit
0010091010(** Jsont codec for {!type:params}. *)
1011val params_jsont : params Jsont.t
101210131014+type output = {
1015+ preferences : Jsont.json;
1016+}
10171018(** Jsont codec for {!type:output}. *)
1019val output_jsont : output Jsont.t
10201021 end
1022+ module GetSuggestions : sig
1023+(** Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding. *)
10241025(** Query/procedure parameters. *)
1026type params = {
1027 cursor : string option;
1028 limit : int option;
001029}
10301031(** Jsont codec for {!type:params}. *)
···1035type output = {
1036 actors : Jsont.json list;
1037 cursor : string option;
1038+ rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
1039}
10401041(** Jsont codec for {!type:output}. *)
1042val output_jsont : output Jsont.t
10431044 end
1045+ module GetProfile : sig
1046+(** Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth. *)
10471048(** Query/procedure parameters. *)
1049type params = {
1050+ actor : string; (** Handle or DID of account to fetch profile of. *)
01051}
10521053(** Jsont codec for {!type:params}. *)
1054val params_jsont : params Jsont.t
105510561057+type output = Jsont.json
000010581059(** Jsont codec for {!type:output}. *)
1060val output_jsont : output Jsont.t
10611062 end
1063+ module SearchActors : sig
1064+(** Find actors (profiles) matching search criteria. Does not require auth. *)
10651066(** Query/procedure parameters. *)
1067type params = {
1068+ cursor : string option;
1069+ limit : int option;
1070+ q : string option; (** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. *)
1071+ term : string option; (** DEPRECATED: use 'q' instead. *)
1072}
10731074(** Jsont codec for {!type:params}. *)
···107610771078type output = {
1079+ actors : Jsont.json list;
1080+ cursor : string option;
1081}
10821083(** Jsont codec for {!type:output}. *)
···1096val input_jsont : input Jsont.t
10971098 end
1099+ module SearchActorsTypeahead : sig
1100+(** Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth. *)
011011102+(** Query/procedure parameters. *)
1103+type params = {
1104+ limit : int option;
1105+ q : string option; (** Search query prefix; not a full query string. *)
1106+ term : string option; (** DEPRECATED: use 'q' instead. *)
1107}
11081109+(** Jsont codec for {!type:params}. *)
1110+val params_jsont : params Jsont.t
1111011121113+type output = {
1114+ actors : Jsont.json list;
000000000001115}
11161117+(** Jsont codec for {!type:output}. *)
1118+val output_jsont : output Jsont.t
11191120 end
1121+ end
1122+ module Video : sig
1123+ module GetUploadLimits : sig
1124+(** Get video upload limits for the authenticated user. *)
0000112511261127+type output = {
1128+ can_upload : bool;
1129+ error : string option;
1130+ message : string option;
1131+ remaining_daily_bytes : int option;
1132+ remaining_daily_videos : int option;
1133+}
11341135(** Jsont codec for {!type:output}. *)
1136val output_jsont : output Jsont.t
11371138 end
1139+ module Defs : sig
0011401141+type job_status = {
1142+ blob : Atp.Blob_ref.t option;
1143+ did : string;
1144+ error : string option;
1145+ job_id : string;
1146+ message : string option;
1147+ progress : int option; (** Progress within the current processing state. *)
1148+ state : string; (** The state of the video processing job. All values not listed as a known value indicate that the job is in process. *)
1149}
11501151+(** Jsont codec for {!type:job_status}. *)
1152+val job_status_jsont : job_status Jsont.t
00000011531154 end
1155+ module GetJobStatus : sig
1156+(** Get status details for a video processing job. *)
11571158(** Query/procedure parameters. *)
1159type params = {
1160+ job_id : string;
01161}
11621163(** Jsont codec for {!type:params}. *)
···116511661167type output = {
1168+ job_status : Defs.job_status;
01169}
11701171(** Jsont codec for {!type:output}. *)
1172val output_jsont : output Jsont.t
11731174 end
1175+ module UploadVideo : sig
1176+(** Upload a video to be processed then stored on the PDS. *)
117711781179+type input = unit
000001180val input_jsont : input Jsont.t
118111821183type output = {
1184+ job_status : Defs.job_status;
1185}
11861187(** Jsont codec for {!type:output}. *)
1188val output_jsont : output Jsont.t
11891190 end
1191+ end
1192+ module Richtext : sig
1193+ module Facet : sig
1194+(** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). *)
11951196+type tag = {
1197+ tag : string;
1198+}
11991200+(** Jsont codec for {!type:tag}. *)
1201+val tag_jsont : tag Jsont.t
1202+1203+(** Facet feature for mention of another account. The text is usually a handle, including a '\@' prefix, but the facet reference is a DID. *)
1204+1205+type mention = {
1206+ did : string;
1207}
12081209+(** Jsont codec for {!type:mention}. *)
1210+val mention_jsont : mention Jsont.t
12111212+(** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. *)
12131214+type link = {
1215+ uri : string;
1216+}
12171218+(** Jsont codec for {!type:link}. *)
1219+val link_jsont : link Jsont.t
12201221+(** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. *)
0012221223+type byte_slice = {
1224+ byte_end : int;
1225+ byte_start : int;
01226}
12271228+(** Jsont codec for {!type:byte_slice}. *)
1229+val byte_slice_jsont : byte_slice Jsont.t
12301231+(** Annotation of a sub-string within rich text. *)
12321233+type main = {
1234+ features : Jsont.json list;
1235+ index : byte_slice;
1236+}
12371238+(** Jsont codec for {!type:main}. *)
1239+val main_jsont : main Jsont.t
12401241 end
1242+ end
1243+ module AuthCreatePosts : sig
12441245+type main = unit
1246+val main_jsont : main Jsont.t
12471248+ end
1249+ module Ageassurance : sig
1250+ module Defs : sig
1251+(** The status of the Age Assurance process. *)
12521253+type status = string
1254+val status_jsont : status Jsont.t
12551256+(** Additional metadata needed to compute Age Assurance state client-side. *)
1257+1258+type state_metadata = {
1259+ account_created_at : string option; (** The account creation timestamp. *)
1260}
12611262+(** Jsont codec for {!type:state_metadata}. *)
1263+val state_metadata_jsont : state_metadata Jsont.t
12641265+(** Object used to store Age Assurance data in stash. *)
0012661267+type event = {
1268+ access : string; (** The access level granted based on Age Assurance data we've processed. *)
1269+ attempt_id : string; (** The unique identifier for this instance of the Age Assurance flow, in UUID format. *)
1270+ complete_ip : string option; (** The IP address used when completing the Age Assurance flow. *)
1271+ complete_ua : string option; (** The user agent used when completing the Age Assurance flow. *)
1272+ country_code : string; (** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. *)
1273+ created_at : string; (** The date and time of this write operation. *)
1274+ email : string option; (** The email used for Age Assurance. *)
1275+ init_ip : string option; (** The IP address used when initiating the Age Assurance flow. *)
1276+ init_ua : string option; (** The user agent used when initiating the Age Assurance flow. *)
1277+ region_code : string option; (** The ISO 3166-2 region code provided when beginning the Age Assurance flow. *)
1278+ status : string; (** The status of the Age Assurance process. *)
1279}
12801281+(** Jsont codec for {!type:event}. *)
1282+val event_jsont : event Jsont.t
12831284+(** The Age Assurance configuration for a specific region. *)
12851286+type config_region = {
1287+ country_code : string; (** The ISO 3166-1 alpha-2 country code this configuration applies to. *)
1288+ min_access_age : int; (** The minimum age (as a whole integer) required to use Bluesky in this region. *)
1289+ region_code : string option; (** The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. *)
1290+ rules : Jsont.json list; (** The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item. *)
1291}
12921293+(** Jsont codec for {!type:config_region}. *)
1294+val config_region_jsont : config_region Jsont.t
1295+1296+(** The access level granted based on Age Assurance data we've processed. *)
12971298+type access = string
1299+val access_jsont : access Jsont.t
1300+1301+(** The user's computed Age Assurance state. *)
13021303+type state = {
1304+ access : access;
1305+ last_initiated_at : string option; (** The timestamp when this state was last updated. *)
1306+ status : status;
1307}
13081309+(** Jsont codec for {!type:state}. *)
1310+val state_jsont : state Jsont.t
13111312+(** Age Assurance rule that applies if the user has declared themselves under a certain age. *)
13131314+type config_region_rule_if_declared_under_age = {
1315+ access : access;
1316+ age : int; (** The age threshold as a whole integer. *)
00001317}
13181319+(** Jsont codec for {!type:config_region_rule_if_declared_under_age}. *)
1320+val config_region_rule_if_declared_under_age_jsont : config_region_rule_if_declared_under_age Jsont.t
13211322+(** Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. *)
0013231324+type config_region_rule_if_declared_over_age = {
1325+ access : access;
1326+ age : int; (** The age threshold as a whole integer. *)
001327}
13281329+(** Jsont codec for {!type:config_region_rule_if_declared_over_age}. *)
1330+val config_region_rule_if_declared_over_age_jsont : config_region_rule_if_declared_over_age Jsont.t
13311332+(** Age Assurance rule that applies if the user has been assured to be under a certain age. *)
13331334+type config_region_rule_if_assured_under_age = {
1335+ access : access;
1336+ age : int; (** The age threshold as a whole integer. *)
01337}
13381339+(** Jsont codec for {!type:config_region_rule_if_assured_under_age}. *)
1340+val config_region_rule_if_assured_under_age_jsont : config_region_rule_if_assured_under_age Jsont.t
13411342+(** Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. *)
0013431344+type config_region_rule_if_assured_over_age = {
1345+ access : access;
1346+ age : int; (** The age threshold as a whole integer. *)
1347}
13481349+(** Jsont codec for {!type:config_region_rule_if_assured_over_age}. *)
1350+val config_region_rule_if_assured_over_age_jsont : config_region_rule_if_assured_over_age Jsont.t
13511352+(** Age Assurance rule that applies if the account is older than a certain date. *)
13531354+type config_region_rule_if_account_older_than = {
1355+ access : access;
1356+ date : string; (** The date threshold as a datetime string. *)
01357}
13581359+(** Jsont codec for {!type:config_region_rule_if_account_older_than}. *)
1360+val config_region_rule_if_account_older_than_jsont : config_region_rule_if_account_older_than Jsont.t
13611362+(** Age Assurance rule that applies if the account is equal-to or newer than a certain date. *)
0013631364+type config_region_rule_if_account_newer_than = {
1365+ access : access;
1366+ date : string; (** The date threshold as a datetime string. *)
1367}
13681369+(** Jsont codec for {!type:config_region_rule_if_account_newer_than}. *)
1370+val config_region_rule_if_account_newer_than_jsont : config_region_rule_if_account_newer_than Jsont.t
13711372+(** Age Assurance rule that applies by default. *)
0013731374+type config_region_rule_default = {
1375+ access : access;
01376}
13771378+(** Jsont codec for {!type:config_region_rule_default}. *)
1379+val config_region_rule_default_jsont : config_region_rule_default Jsont.t
138000013811382+type config = {
1383+ regions : config_region list; (** The per-region Age Assurance configuration. *)
01384}
13851386+(** Jsont codec for {!type:config}. *)
1387+val config_jsont : config Jsont.t
13881389 end
1390+ module GetState : sig
1391+(** Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side. *)
13921393(** Query/procedure parameters. *)
1394type params = {
1395+ country_code : string;
1396+ region_code : string option;
01397}
13981399(** Jsont codec for {!type:params}. *)
···140114021403type output = {
1404+ metadata : Defs.state_metadata;
1405+ state : Defs.state;
01406}
14071408(** Jsont codec for {!type:output}. *)
1409val output_jsont : output Jsont.t
14101411 end
1412+ module Begin : sig
1413+(** Initiate Age Assurance for an account. *)
141414151416type input = {
1417+ country_code : string; (** An ISO 3166-1 alpha-2 code of the user's location. *)
1418+ email : string; (** The user's email address to receive Age Assurance instructions. *)
1419+ language : string; (** The user's preferred language for communication during the Age Assurance process. *)
1420+ region_code : string option; (** An optional ISO 3166-2 code of the user's region or state within the country. *)
1421}
14221423(** Jsont codec for {!type:input}. *)
1424val input_jsont : input Jsont.t
142500014261427+type output = Defs.state
000014281429+(** Jsont codec for {!type:output}. *)
1430+val output_jsont : output Jsont.t
14311432 end
1433+ module GetConfig : sig
1434+(** Returns Age Assurance configuration for use on the client. *)
143514361437+type output = Defs.config
0000000000000014381439+(** Jsont codec for {!type:output}. *)
1440+val output_jsont : output Jsont.t
14411442 end
1443+ end
1444+ module Embed : sig
1445+ module Defs : sig
1446+(** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. *)
14471448+type aspect_ratio = {
1449+ height : int;
1450+ width : int;
1451}
14521453+(** Jsont codec for {!type:aspect_ratio}. *)
1454+val aspect_ratio_jsont : aspect_ratio Jsont.t
14551456 end
1457+ module External : sig
014581459+type view_external = {
1460+ description : string;
1461+ thumb : string option;
1462+ title : string;
1463+ uri : string;
1464}
14651466+(** Jsont codec for {!type:view_external}. *)
1467+val view_external_jsont : view_external Jsont.t
146814691470+type external_ = {
1471+ description : string;
1472+ thumb : Atp.Blob_ref.t option;
1473+ title : string;
1474+ uri : string;
1475}
14761477+(** Jsont codec for {!type:external_}. *)
1478+val external__jsont : external_ Jsont.t
0000147914801481+type view = {
1482+ external_ : Jsont.json;
1483}
14841485+(** Jsont codec for {!type:view}. *)
1486+val view_jsont : view Jsont.t
14871488+(** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). *)
0014891490type main = {
1491+ external_ : Jsont.json;
001492}
14931494(** Jsont codec for {!type:main}. *)
1495val main_jsont : main Jsont.t
14961497 end
1498+ module Images : sig
14991500+type view_image = {
1501+ alt : string; (** Alt text description of the image, for accessibility. *)
1502+ aspect_ratio : Jsont.json option;
1503+ fullsize : string; (** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. *)
1504+ thumb : string; (** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. *)
000001505}
15061507+(** Jsont codec for {!type:view_image}. *)
1508+val view_image_jsont : view_image Jsont.t
1509015101511+type image = {
1512+ alt : string; (** Alt text description of the image, for accessibility. *)
1513+ aspect_ratio : Jsont.json option;
1514+ image : Atp.Blob_ref.t;
00001515}
15161517+(** Jsont codec for {!type:image}. *)
1518+val image_jsont : image Jsont.t
1519015201521+type view = {
1522+ images : Jsont.json list;
1523+}
15241525+(** Jsont codec for {!type:view}. *)
1526+val view_jsont : view Jsont.t
15271528+1529+type main = {
1530+ images : Jsont.json list;
1531}
15321533+(** Jsont codec for {!type:main}. *)
1534+val main_jsont : main Jsont.t
1535+1536+ end
1537+ module Video : sig
15381539+type view = {
1540+ alt : string option;
1541+ aspect_ratio : Jsont.json option;
1542+ cid : string;
1543+ playlist : string;
1544+ thumbnail : string option;
1545+}
15461547+(** Jsont codec for {!type:view}. *)
1548+val view_jsont : view Jsont.t
154915501551+type caption = {
1552+ file : Atp.Blob_ref.t;
1553+ lang : string;
1554}
15551556+(** Jsont codec for {!type:caption}. *)
1557+val caption_jsont : caption Jsont.t
155815591560+type main = {
1561+ alt : string option; (** Alt text description of the video, for accessibility. *)
1562+ aspect_ratio : Jsont.json option;
1563+ captions : Jsont.json list option;
1564+ video : Atp.Blob_ref.t; (** The mp4 video file. May be up to 100mb, formerly limited to 50mb. *)
1565+}
15661567+(** Jsont codec for {!type:main}. *)
1568+val main_jsont : main Jsont.t
15691570+ end
1571+ module RecordWithMedia : sig
1572+1573+type view = {
1574+ media : Jsont.json;
1575+ record : Jsont.json;
1576}
15771578+(** Jsont codec for {!type:view}. *)
1579+val view_jsont : view Jsont.t
1580+15811582+type main = {
1583+ media : Jsont.json;
1584+ record : Jsont.json;
1585+}
15861587+(** Jsont codec for {!type:main}. *)
1588+val main_jsont : main Jsont.t
15891590+ end
1591+ module Record : sig
15921593+type view_record = {
1594+ author : Jsont.json;
1595 cid : string;
1596+ embeds : Jsont.json list option;
1597+ indexed_at : string;
1598 labels : Com.Atproto.Label.Defs.label list option;
1599+ like_count : int option;
1600+ quote_count : int option;
1601+ reply_count : int option;
1602+ repost_count : int option;
1603 uri : string;
1604+ value : Jsont.json; (** The record data itself. *)
1605}
16061607+(** Jsont codec for {!type:view_record}. *)
1608+val view_record_jsont : view_record Jsont.t
160916101611+type view_not_found = {
1612+ not_found : bool;
0000000001613 uri : string;
01614}
16151616+(** Jsont codec for {!type:view_not_found}. *)
1617+val view_not_found_jsont : view_not_found Jsont.t
161816191620+type view_detached = {
1621+ detached : bool;
0000000001622 uri : string;
1623}
16241625+(** Jsont codec for {!type:view_detached}. *)
1626+val view_detached_jsont : view_detached Jsont.t
162700016281629+type view_blocked = {
1630+ author : Jsont.json;
1631+ blocked : bool;
1632+ uri : string;
1633}
16341635+(** Jsont codec for {!type:view_blocked}. *)
1636+val view_blocked_jsont : view_blocked Jsont.t
163716381639+type view = {
1640+ record : Jsont.json;
01641}
16421643+(** Jsont codec for {!type:view}. *)
1644+val view_jsont : view Jsont.t
164500016461647type main = {
1648+ record : Com.Atproto.Repo.StrongRef.main;
0001649}
16501651(** Jsont codec for {!type:main}. *)
1652val main_jsont : main Jsont.t
16531654 end
1655+ end
1656+ module Contact : sig
1657+ module SendNotification : sig
1658+(** System endpoint to send notifications related to contact imports. Requires role authentication. *)
16591660+1661+type input = {
1662+ from : string; (** The DID of who this notification comes from. *)
1663+ to_ : string; (** The DID of who this notification should go to. *)
1664}
16651666+(** Jsont codec for {!type:input}. *)
1667+val input_jsont : input Jsont.t
166816691670+type output = unit
00016711672(** Jsont codec for {!type:output}. *)
1673val output_jsont : output Jsont.t
16741675 end
1676+ module StartPhoneVerification : sig
1677+(** Starts a phone verification flow. The phone passed will receive a code via SMS that should be passed to `app.bsky.contact.verifyPhone`. Requires authentication. *)
1678+16791680+type input = {
1681+ phone : string; (** The phone number to receive the code via SMS. *)
001682}
16831684+(** Jsont codec for {!type:input}. *)
1685+val input_jsont : input Jsont.t
168616871688+type output = unit
00016891690(** Jsont codec for {!type:output}. *)
1691val output_jsont : output Jsont.t
16921693 end
1694+ module GetMatches : sig
1695+(** Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication. *)
000000000016961697(** Query/procedure parameters. *)
1698type params = {
01699 cursor : string option;
1700 limit : int option;
1701}
···17061707type output = {
1708 cursor : string option;
1709+ matches : Jsont.json list;
1710}
17111712(** Jsont codec for {!type:output}. *)
1713val output_jsont : output Jsont.t
17141715 end
1716+ module VerifyPhone : sig
1717+(** Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication. *)
1718+17191720+type input = {
1721+ code : string; (** The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`. *)
1722+ phone : string; (** The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`. *)
001723}
17241725+(** Jsont codec for {!type:input}. *)
1726+val input_jsont : input Jsont.t
172717281729type output = {
1730+ token : string; (** JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call. *)
01731}
17321733(** Jsont codec for {!type:output}. *)
1734val output_jsont : output Jsont.t
17351736 end
1737+ module RemoveData : sig
1738+(** Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication. *)
000000000017390017401741+type input = unit
0017421743+(** Jsont codec for {!type:input}. *)
1744+val input_jsont : input Jsont.t
0000000174517461747+type output = unit
00017481749(** Jsont codec for {!type:output}. *)
1750val output_jsont : output Jsont.t
17511752 end
1753+ module Defs : sig
017541755+type sync_status = {
1756+ matches_count : int; (** Number of existing contact matches resulting of the user imports and of their imported contacts having imported the user. Matches stop being counted when the user either follows the matched contact or dismisses the match. *)
1757+ synced_at : string; (** Last date when contacts where imported. *)
1758+}
1759+1760+(** Jsont codec for {!type:sync_status}. *)
1761+val sync_status_jsont : sync_status Jsont.t
1762+1763+(** A stash object to be sent via bsync representing a notification to be created. *)
1764+1765+type notification = {
1766+ from : string; (** The DID of who this notification comes from. *)
1767+ to_ : string; (** The DID of who this notification should go to. *)
1768}
17691770+(** Jsont codec for {!type:notification}. *)
1771+val notification_jsont : notification Jsont.t
17721773+(** Associates a profile with the positional index of the contact import input in the call to `app.bsky.contact.importContacts`, so clients can know which phone caused a particular match. *)
17741775+type match_and_contact_index = {
1776+ contact_index : int; (** The index of this match in the import contact input. *)
1777+ match_ : Jsont.json; (** Profile of the matched user. *)
01778}
17791780+(** Jsont codec for {!type:match_and_contact_index}. *)
1781+val match_and_contact_index_jsont : match_and_contact_index Jsont.t
17821783 end
1784+ module DismissMatch : sig
1785+(** Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication. *)
1786+17871788+type input = {
1789+ subject : string; (** The subject's DID to dismiss the match with. *)
001790}
17911792+(** Jsont codec for {!type:input}. *)
1793+val input_jsont : input Jsont.t
179417951796+type output = unit
00017971798(** Jsont codec for {!type:output}. *)
1799val output_jsont : output Jsont.t
18001801 end
1802+ module GetSyncStatus : sig
1803+(** Gets the user's current contact import status. Requires authentication. *)
18041805(** Query/procedure parameters. *)
1806+type params = unit
0018071808(** Jsont codec for {!type:params}. *)
1809val params_jsont : params Jsont.t
181018111812type output = {
1813+ sync_status : Defs.sync_status option; (** If present, indicates the user has imported their contacts. If not present, indicates the user never used the feature or called `app.bsky.contact.removeData` and didn't import again since. *)
1814}
18151816(** Jsont codec for {!type:output}. *)
1817val output_jsont : output Jsont.t
18181819 end
1820+ module ImportContacts : sig
1821+(** Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication. *)
1822000018231824+type input = {
1825+ contacts : string list; (** List of phone numbers in global E.164 format (e.g., '+12125550123'). Phone numbers that cannot be normalized into a valid phone number will be discarded. Should not repeat the 'phone' input used in `app.bsky.contact.verifyPhone`. *)
1826+ token : string; (** JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`. *)
000000001827}
18281829+(** Jsont codec for {!type:input}. *)
1830+val input_jsont : input Jsont.t
183118321833type output = {
1834+ matches_and_contact_indexes : Defs.match_and_contact_index list; (** The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list. *)
01835}
18361837(** Jsont codec for {!type:output}. *)
1838val output_jsont : output Jsont.t
18391840 end
1841+ end
1842+ module Feed : sig
1843+ module Repost : sig
1844+(** Record representing a 'repost' of an existing Bluesky post. *)
18451846+type main = {
1847+ created_at : string;
1848+ subject : Com.Atproto.Repo.StrongRef.main;
1849+ via : Com.Atproto.Repo.StrongRef.main option;
1850}
18511852+(** Jsont codec for {!type:main}. *)
1853+val main_jsont : main Jsont.t
18541855+ end
1856+ module DescribeFeedGenerator : sig
18571858+type links = {
1859+ privacy_policy : string option;
1860+ terms_of_service : string option;
1861}
18621863+(** Jsont codec for {!type:links}. *)
1864+val links_jsont : links Jsont.t
186500018661867+type feed = {
1868+ uri : string;
01869}
18701871+(** Jsont codec for {!type:feed}. *)
1872+val feed_jsont : feed Jsont.t
1873+1874+(** Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View). *)
187518761877type output = {
1878+ did : string;
1879+ feeds : Jsont.json list;
1880+ links : Jsont.json option;
1881}
18821883(** Jsont codec for {!type:output}. *)
1884val output_jsont : output Jsont.t
18851886 end
1887+ module Threadgate : sig
1888+(** Allow replies from actors mentioned in your post. *)
18891890+type mention_rule = unit
00000018911892+(** Jsont codec for {!type:mention_rule}. *)
1893+val mention_rule_jsont : mention_rule Jsont.t
18941895+(** Allow replies from actors on a list. *)
18961897+type list_rule = {
1898+ list_ : string;
01899}
19001901+(** Jsont codec for {!type:list_rule}. *)
1902+val list_rule_jsont : list_rule Jsont.t
19031904+(** Allow replies from actors you follow. *)
000019051906+type following_rule = unit
00019071908+(** Jsont codec for {!type:following_rule}. *)
1909+val following_rule_jsont : following_rule Jsont.t
19101911+(** Allow replies from actors who follow you. *)
19121913+type follower_rule = unit
00019141915+(** Jsont codec for {!type:follower_rule}. *)
1916+val follower_rule_jsont : follower_rule Jsont.t
19171918+(** Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository. *)
19191920+type main = {
1921+ allow : Jsont.json list option; (** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. *)
1922+ created_at : string;
1923+ hidden_replies : string list option; (** List of hidden reply URIs. *)
1924+ post : string; (** Reference (AT-URI) to the post record. *)
1925}
19261927+(** Jsont codec for {!type:main}. *)
1928+val main_jsont : main Jsont.t
19291930+ end
1931+ module Like : sig
1932+(** Record declaring a 'like' of a piece of subject content. *)
19331934type main = {
1935+ created_at : string;
1936+ subject : Com.Atproto.Repo.StrongRef.main;
1937+ via : Com.Atproto.Repo.StrongRef.main option;
0000001938}
19391940(** Jsont codec for {!type:main}. *)
···1977val output_jsont : output Jsont.t
19781979 end
0000000000000000000001980 module GetRepostedBy : sig
1981(** Get a list of reposts for a given post. *)
1982···2003val output_jsont : output Jsont.t
20042005 end
2006+ module Generator : sig
2007+(** Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository. *)
20082009+type main = {
2010+ accepts_interactions : bool option; (** Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions *)
2011+ avatar : Atp.Blob_ref.t option;
2012+ content_mode : string option;
2013+ created_at : string;
2014+ description : string option;
2015+ description_facets : Richtext.Facet.main list option;
00000000000002016 did : string;
2017+ display_name : string;
2018+ labels : Com.Atproto.Label.Defs.self_labels option; (** Self-label values *)
2019}
20202021+(** Jsont codec for {!type:main}. *)
2022+val main_jsont : main Jsont.t
20232024 end
2025+ module Postgate : sig
2026+(** Disables embedding of this post. *)
00000000000000000000020272028+type disable_rule = unit
20292030+(** Jsont codec for {!type:disable_rule}. *)
2031+val disable_rule_jsont : disable_rule Jsont.t
20322033+(** Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository. *)
00020342035type main = {
02036 created_at : string;
2037+ detached_embedding_uris : string list option; (** List of AT-URIs embedding this post that the author has detached from. *)
2038+ embedding_rules : Jsont.json list option; (** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. *)
2039 post : string; (** Reference (AT-URI) to the post record. *)
00000000000002040}
20412042(** Jsont codec for {!type:main}. *)
···2314val feed_view_post_jsont : feed_view_post Jsont.t
23152316 end
2317+ module Post : sig
2318+(** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. *)
2319+2320+type text_slice = {
2321+ end_ : int;
2322+ start : int;
2323+}
2324+2325+(** Jsont codec for {!type:text_slice}. *)
2326+val text_slice_jsont : text_slice Jsont.t
2327+2328+2329+type reply_ref = {
2330+ parent : Com.Atproto.Repo.StrongRef.main;
2331+ root : Com.Atproto.Repo.StrongRef.main;
2332+}
2333+2334+(** Jsont codec for {!type:reply_ref}. *)
2335+val reply_ref_jsont : reply_ref Jsont.t
2336+2337+(** Deprecated: use facets instead. *)
2338+2339+type entity = {
2340+ index : Jsont.json;
2341+ type_ : string; (** Expected values are 'mention' and 'link'. *)
2342+ value : string;
2343+}
2344+2345+(** Jsont codec for {!type:entity}. *)
2346+val entity_jsont : entity Jsont.t
2347+2348+(** Record containing a Bluesky post. *)
23492350type main = {
2351+ created_at : string; (** Client-declared timestamp when this post was originally created. *)
2352+ embed : Jsont.json option;
2353+ entities : Jsont.json list option; (** DEPRECATED: replaced by app.bsky.richtext.facet. *)
2354+ facets : Richtext.Facet.main list option; (** Annotations of text (mentions, URLs, hashtags, etc) *)
2355+ labels : Com.Atproto.Label.Defs.self_labels option; (** Self-label values for this post. Effectively content warnings. *)
2356+ langs : string list option; (** Indicates human language of post primary text content. *)
2357+ reply : Jsont.json option;
2358+ tags : string list option; (** Additional hashtags, in addition to any included in post text and facets. *)
2359+ text : string; (** The primary post content. May be an empty string, if there are embeds. *)
2360}
23612362(** Jsont codec for {!type:main}. *)
2363val main_jsont : main Jsont.t
23642365 end
2366+ module GetPosts : sig
2367+(** Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'. *)
23682369+(** Query/procedure parameters. *)
2370+type params = {
2371+ uris : string list; (** List of post AT-URIs to return hydrated views for. *)
2372+}
2373+2374+(** Jsont codec for {!type:params}. *)
2375+val params_jsont : params Jsont.t
2376+2377+2378+type output = {
2379+ posts : Jsont.json list;
2380}
23812382+(** Jsont codec for {!type:output}. *)
2383+val output_jsont : output Jsont.t
23842385 end
2386+ module GetQuotes : sig
2387+(** Get a list of quotes for a given post. *)
23882389(** Query/procedure parameters. *)
2390type params = {
2391+ cid : string option; (** If supplied, filters to quotes of specific version (by CID) of the post record. *)
2392+ cursor : string option;
2393+ limit : int option;
2394+ uri : string; (** Reference (AT-URI) of post record *)
2395}
23962397(** Jsont codec for {!type:params}. *)
···239924002401type output = {
2402+ cid : string option;
2403+ cursor : string option;
2404+ posts : Jsont.json list;
2405+ uri : string;
2406}
24072408(** Jsont codec for {!type:output}. *)
2409val output_jsont : output Jsont.t
24102411 end
2412+ module GetAuthorFeed : sig
2413+(** Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth. *)
24142415(** Query/procedure parameters. *)
2416type params = {
2417+ actor : string;
2418 cursor : string option;
2419+ filter : string option; (** Combinations of post/repost types to include in response. *)
2420+ include_pins : bool option;
2421 limit : int option;
2422}
2423···2434val output_jsont : output Jsont.t
24352436 end
2437+ module GetTimeline : sig
2438+(** Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed. *)
24392440(** Query/procedure parameters. *)
2441type params = {
2442+ algorithm : string option; (** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. *)
2443 cursor : string option;
2444 limit : int option;
02445}
24462447(** Jsont codec for {!type:params}. *)
···244924502451type output = {
02452 cursor : string option;
2453+ feed : Jsont.json list;
02454}
24552456(** Jsont codec for {!type:output}. *)
···2480val output_jsont : output Jsont.t
24812482 end
2483+ module GetSuggestedFeeds : sig
2484+(** Get a list of suggested feeds (feed generators) for the requesting account. *)
2485+2486+(** Query/procedure parameters. *)
2487+type params = {
2488+ cursor : string option;
2489+ limit : int option;
2490+}
2491+2492+(** Jsont codec for {!type:params}. *)
2493+val params_jsont : params Jsont.t
2494+2495+2496+type output = {
2497+ cursor : string option;
2498+ feeds : Jsont.json list;
2499+}
2500+2501+(** Jsont codec for {!type:output}. *)
2502+val output_jsont : output Jsont.t
2503+2504+ end
2505 module GetActorLikes : sig
2506(** Get a list of posts liked by an actor. Requires auth, actor must be the requesting account. *)
2507···2525val output_jsont : output Jsont.t
25262527 end
2528+ module GetActorFeeds : sig
2529+(** Get a list of feeds (feed generator records) created by the actor (in the actor's repo). *)
25302531(** Query/procedure parameters. *)
2532type params = {
2533+ actor : string;
2534 cursor : string option;
02535 limit : int option;
2536}
2537···25412542type output = {
2543 cursor : string option;
2544+ feeds : Jsont.json list;
02545}
25462547(** Jsont codec for {!type:output}. *)
···2581val output_jsont : output Jsont.t
25822583 end
2584+ module GetPostThread : sig
2585+(** Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests. *)
2586+2587+(** Query/procedure parameters. *)
2588+type params = {
2589+ depth : int option; (** How many levels of reply depth should be included in response. *)
2590+ parent_height : int option; (** How many levels of parent (and grandparent, etc) post to include. *)
2591+ uri : string; (** Reference (AT-URI) to post record. *)
2592+}
2593+2594+(** Jsont codec for {!type:params}. *)
2595+val params_jsont : params Jsont.t
2596+2597+2598+type output = {
2599+ thread : Jsont.json;
2600+ threadgate : Jsont.json option;
2601+}
2602+2603+(** Jsont codec for {!type:output}. *)
2604+val output_jsont : output Jsont.t
2605+2606+ end
2607+ module GetFeedSkeleton : sig
2608+(** Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service. *)
2609+2610+(** Query/procedure parameters. *)
2611+type params = {
2612+ cursor : string option;
2613+ feed : string; (** Reference to feed generator record describing the specific feed being requested. *)
2614+ limit : int option;
2615+}
2616+2617+(** Jsont codec for {!type:params}. *)
2618+val params_jsont : params Jsont.t
2619+2620+2621+type output = {
2622+ cursor : string option;
2623+ feed : Jsont.json list;
2624+ req_id : string option; (** Unique identifier per request that may be passed back alongside interactions. *)
2625+}
2626+2627+(** Jsont codec for {!type:output}. *)
2628+val output_jsont : output Jsont.t
2629+2630+ end
2631+ module GetFeed : sig
2632+(** Get a hydrated feed from an actor's selected feed generator. Implemented by App View. *)
2633+2634+(** Query/procedure parameters. *)
2635+type params = {
2636+ cursor : string option;
2637+ feed : string;
2638+ limit : int option;
2639+}
2640+2641+(** Jsont codec for {!type:params}. *)
2642+val params_jsont : params Jsont.t
2643+2644+2645+type output = {
2646+ cursor : string option;
2647+ feed : Jsont.json list;
2648+}
2649+2650+(** Jsont codec for {!type:output}. *)
2651+val output_jsont : output Jsont.t
2652+2653+ end
2654 module GetFeedGenerators : sig
2655(** Get information about a list of feed generators. *)
2656···2689val output_jsont : output Jsont.t
26902691 end
2692+ module GetFeedGenerator : sig
2693+(** Get information about a feed generator. Implemented by AppView. *)
2694+2695+(** Query/procedure parameters. *)
2696+type params = {
2697+ feed : string; (** AT-URI of the feed generator record. *)
2698+}
2699+2700+(** Jsont codec for {!type:params}. *)
2701+val params_jsont : params Jsont.t
2702+2703+2704+type output = {
2705+ is_online : bool; (** Indicates whether the feed generator service has been online recently, or else seems to be inactive. *)
2706+ is_valid : bool; (** Indicates whether the feed generator service is compatible with the record declaration. *)
2707+ view : Jsont.json;
2708+}
2709+2710+(** Jsont codec for {!type:output}. *)
2711+val output_jsont : output Jsont.t
2712+2713+ end
2714+ end
2715+ module Graph : sig
2716+ module Defs : sig
2717+2718+type starter_pack_view_basic = {
2719+ cid : string;
2720+ creator : Jsont.json;
2721+ indexed_at : string;
2722+ joined_all_time_count : int option;
2723+ joined_week_count : int option;
2724+ labels : Com.Atproto.Label.Defs.label list option;
2725+ list_item_count : int option;
2726+ record : Jsont.json;
2727+ uri : string;
2728+}
2729+2730+(** Jsont codec for {!type:starter_pack_view_basic}. *)
2731+val starter_pack_view_basic_jsont : starter_pack_view_basic Jsont.t
2732+2733+(** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) *)
2734+2735+type relationship = {
2736+ blocked_by : string option; (** if the actor is blocked by this DID, contains the AT-URI of the block record *)
2737+ blocked_by_list : string option; (** if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record *)
2738+ blocking : string option; (** if the actor blocks this DID, this is the AT-URI of the block record *)
2739+ blocking_by_list : string option; (** if the actor blocks this DID via a block list, this is the AT-URI of the listblock record *)
2740+ did : string;
2741+ followed_by : string option; (** if the actor is followed by this DID, contains the AT-URI of the follow record *)
2742+ following : string option; (** if the actor follows this DID, this is the AT-URI of the follow record *)
2743+}
2744+2745+(** Jsont codec for {!type:relationship}. *)
2746+val relationship_jsont : relationship Jsont.t
2747+2748+(** A list of actors used for only for reference purposes such as within a starter pack. *)
2749+2750+type referencelist = string
2751+val referencelist_jsont : referencelist Jsont.t
2752+2753+(** indicates that a handle or DID could not be resolved *)
2754+2755+type not_found_actor = {
2756+ actor : string;
2757+ not_found : bool;
2758+}
2759+2760+(** Jsont codec for {!type:not_found_actor}. *)
2761+val not_found_actor_jsont : not_found_actor Jsont.t
2762+2763+(** A list of actors to apply an aggregate moderation action (mute/block) on. *)
2764+2765+type modlist = string
2766+val modlist_jsont : modlist Jsont.t
2767+2768+2769+type list_viewer_state = {
2770+ blocked : string option;
2771+ muted : bool option;
2772+}
2773+2774+(** Jsont codec for {!type:list_viewer_state}. *)
2775+val list_viewer_state_jsont : list_viewer_state Jsont.t
2776+2777+2778+type list_purpose = string
2779+val list_purpose_jsont : list_purpose Jsont.t
2780+2781+2782+type list_item_view = {
2783+ subject : Jsont.json;
2784+ uri : string;
2785+}
2786+2787+(** Jsont codec for {!type:list_item_view}. *)
2788+val list_item_view_jsont : list_item_view Jsont.t
2789+2790+(** A list of actors used for curation purposes such as list feeds or interaction gating. *)
2791+2792+type curatelist = string
2793+val curatelist_jsont : curatelist Jsont.t
2794+2795+2796+type list_view_basic = {
2797+ avatar : string option;
2798+ cid : string;
2799+ indexed_at : string option;
2800+ labels : Com.Atproto.Label.Defs.label list option;
2801+ list_item_count : int option;
2802+ name : string;
2803+ purpose : Jsont.json;
2804+ uri : string;
2805+ viewer : Jsont.json option;
2806+}
2807+2808+(** Jsont codec for {!type:list_view_basic}. *)
2809+val list_view_basic_jsont : list_view_basic Jsont.t
2810+2811+2812+type list_view = {
2813+ avatar : string option;
2814+ cid : string;
2815+ creator : Jsont.json;
2816+ description : string option;
2817+ description_facets : Richtext.Facet.main list option;
2818+ indexed_at : string;
2819+ labels : Com.Atproto.Label.Defs.label list option;
2820+ list_item_count : int option;
2821+ name : string;
2822+ purpose : Jsont.json;
2823+ uri : string;
2824+ viewer : Jsont.json option;
2825+}
2826+2827+(** Jsont codec for {!type:list_view}. *)
2828+val list_view_jsont : list_view Jsont.t
2829+2830+2831+type starter_pack_view = {
2832+ cid : string;
2833+ creator : Jsont.json;
2834+ feeds : Jsont.json list option;
2835+ indexed_at : string;
2836+ joined_all_time_count : int option;
2837+ joined_week_count : int option;
2838+ labels : Com.Atproto.Label.Defs.label list option;
2839+ list_ : Jsont.json option;
2840+ list_items_sample : Jsont.json list option;
2841+ record : Jsont.json;
2842+ uri : string;
2843+}
2844+2845+(** Jsont codec for {!type:starter_pack_view}. *)
2846+val starter_pack_view_jsont : starter_pack_view Jsont.t
2847+2848+ end
2849+ module UnmuteActor : sig
2850+(** Unmutes the specified account. Requires auth. *)
2851+2852+2853+type input = {
2854+ actor : string;
2855+}
2856+2857+(** Jsont codec for {!type:input}. *)
2858+val input_jsont : input Jsont.t
2859+2860+ end
2861+ module Listitem : sig
2862+(** Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records. *)
2863+2864+type main = {
2865+ created_at : string;
2866+ list_ : string; (** Reference (AT-URI) to the list record (app.bsky.graph.list). *)
2867+ subject : string; (** The account which is included on the list. *)
2868+}
2869+2870+(** Jsont codec for {!type:main}. *)
2871+val main_jsont : main Jsont.t
2872+2873+ end
2874+ module GetSuggestedFollowsByActor : sig
2875+(** Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account. *)
2876+2877+(** Query/procedure parameters. *)
2878+type params = {
2879+ actor : string;
2880+}
2881+2882+(** Jsont codec for {!type:params}. *)
2883+val params_jsont : params Jsont.t
2884+2885+2886+type output = {
2887+ is_fallback : bool option; (** If true, response has fallen-back to generic results, and is not scoped using relativeToDid *)
2888+ rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
2889+ suggestions : Jsont.json list;
2890+}
2891+2892+(** Jsont codec for {!type:output}. *)
2893+val output_jsont : output Jsont.t
2894+2895+ end
2896+ module MuteActorList : sig
2897+(** Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth. *)
2898+2899+2900+type input = {
2901+ list_ : string;
2902+}
2903+2904+(** Jsont codec for {!type:input}. *)
2905+val input_jsont : input Jsont.t
2906+2907+ end
2908+ module GetFollowers : sig
2909+(** Enumerates accounts which follow a specified account (actor). *)
29102911(** Query/procedure parameters. *)
2912type params = {
2913 actor : string;
2914 cursor : string option;
002915 limit : int option;
2916}
2917···29212922type output = {
2923 cursor : string option;
2924+ followers : Jsont.json list;
2925+ subject : Jsont.json;
2926}
29272928(** Jsont codec for {!type:output}. *)
2929val output_jsont : output Jsont.t
29302931 end
2932+ module MuteActor : sig
2933+(** Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth. *)
2934+2935+2936+type input = {
2937+ actor : string;
2938+}
2939+2940+(** Jsont codec for {!type:input}. *)
2941+val input_jsont : input Jsont.t
2942+2943+ end
2944+ module UnmuteActorList : sig
2945+(** Unmutes the specified list of accounts. Requires auth. *)
2946+2947+2948+type input = {
2949+ list_ : string;
2950+}
2951+2952+(** Jsont codec for {!type:input}. *)
2953+val input_jsont : input Jsont.t
2954+2955+ end
2956+ module GetBlocks : sig
2957+(** Enumerates which accounts the requesting account is currently blocking. Requires auth. *)
29582959(** Query/procedure parameters. *)
2960type params = {
2961+ cursor : string option;
2962+ limit : int option;
2963}
29642965(** Jsont codec for {!type:params}. *)
···296729682969type output = {
2970+ blocks : Jsont.json list;
2971+ cursor : string option;
02972}
29732974(** Jsont codec for {!type:output}. *)
2975val output_jsont : output Jsont.t
29762977 end
2978+ module Listblock : sig
2979+(** Record representing a block relationship against an entire an entire list of accounts (actors). *)
2980+2981+type main = {
2982+ created_at : string;
2983+ subject : string; (** Reference (AT-URI) to the mod list record. *)
2984+}
2985+2986+(** Jsont codec for {!type:main}. *)
2987+val main_jsont : main Jsont.t
2988+2989+ end
2990+ module MuteThread : sig
2991+(** Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth. *)
2992+2993+2994+type input = {
2995+ root : string;
2996+}
2997+2998+(** Jsont codec for {!type:input}. *)
2999+val input_jsont : input Jsont.t
3000+3001+ end
3002+ module Follow : sig
3003+(** Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView. *)
3004+3005+type main = {
3006+ created_at : string;
3007+ subject : string;
3008+ via : Com.Atproto.Repo.StrongRef.main option;
3009+}
3010+3011+(** Jsont codec for {!type:main}. *)
3012+val main_jsont : main Jsont.t
3013+3014+ end
3015+ module Starterpack : sig
3016+3017+type feed_item = {
3018+ uri : string;
3019+}
3020+3021+(** Jsont codec for {!type:feed_item}. *)
3022+val feed_item_jsont : feed_item Jsont.t
3023+3024+(** Record defining a starter pack of actors and feeds for new users. *)
3025+3026+type main = {
3027+ created_at : string;
3028+ description : string option;
3029+ description_facets : Richtext.Facet.main list option;
3030+ feeds : Jsont.json list option;
3031+ list_ : string; (** Reference (AT-URI) to the list record. *)
3032+ name : string; (** Display name for starter pack; can not be empty. *)
3033+}
3034+3035+(** Jsont codec for {!type:main}. *)
3036+val main_jsont : main Jsont.t
3037+3038+ end
3039+ module GetFollows : sig
3040+(** Enumerates accounts which a specified account (actor) follows. *)
30413042(** Query/procedure parameters. *)
3043type params = {
3044+ actor : string;
3045 cursor : string option;
3046 limit : int option;
3047}
···30523053type output = {
3054 cursor : string option;
3055+ follows : Jsont.json list;
3056+ subject : Jsont.json;
3057}
30583059(** Jsont codec for {!type:output}. *)
3060val output_jsont : output Jsont.t
30613062 end
3063+ module Verification : sig
3064+(** Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted. *)
3065+3066+type main = {
3067+ created_at : string; (** Date of when the verification was created. *)
3068+ display_name : string; (** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. *)
3069+ handle : string; (** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. *)
3070+ subject : string; (** DID of the subject the verification applies to. *)
3071+}
3072+3073+(** Jsont codec for {!type:main}. *)
3074+val main_jsont : main Jsont.t
3075+3076+ end
3077+ module UnmuteThread : sig
3078+(** Unmutes the specified thread. Requires auth. *)
3079+3080+3081+type input = {
3082+ root : string;
3083+}
3084+3085+(** Jsont codec for {!type:input}. *)
3086+val input_jsont : input Jsont.t
3087+3088+ end
3089+ module GetMutes : sig
3090+(** Enumerates accounts that the requesting account (actor) currently has muted. Requires auth. *)
3091+3092+(** Query/procedure parameters. *)
3093+type params = {
3094+ cursor : string option;
3095+ limit : int option;
3096+}
3097+3098+(** Jsont codec for {!type:params}. *)
3099+val params_jsont : params Jsont.t
3100+3101+3102+type output = {
3103+ cursor : string option;
3104+ mutes : Jsont.json list;
3105+}
3106+3107+(** Jsont codec for {!type:output}. *)
3108+val output_jsont : output Jsont.t
3109+3110+ end
3111+ module GetKnownFollowers : sig
3112+(** Enumerates accounts which follow a specified account (actor) and are followed by the viewer. *)
31133114(** Query/procedure parameters. *)
3115type params = {
···31243125type output = {
3126 cursor : string option;
3127+ followers : Jsont.json list;
3128+ subject : Jsont.json;
3129}
31303131(** Jsont codec for {!type:output}. *)
3132val output_jsont : output Jsont.t
31333134 end
3135+ module Block : sig
3136+(** Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details. *)
3137+3138+type main = {
3139+ created_at : string;
3140+ subject : string; (** DID of the account to be blocked. *)
3141+}
3142+3143+(** Jsont codec for {!type:main}. *)
3144+val main_jsont : main Jsont.t
3145+3146+ end
3147+ module GetRelationships : sig
3148+(** Enumerates public relationships between one account, and a list of other accounts. Does not require auth. *)
31493150(** Query/procedure parameters. *)
3151type params = {
3152+ actor : string; (** Primary account requesting relationships for. *)
3153+ others : string list option; (** List of 'other' accounts to be related back to the primary. *)
3154}
31553156(** Jsont codec for {!type:params}. *)
···315831593160type output = {
3161+ actor : string option;
3162+ relationships : Jsont.json list;
3163}
31643165(** Jsont codec for {!type:output}. *)
3166val output_jsont : output Jsont.t
31673168 end
3169+ module GetListsWithMembership : sig
3170+(** A list and an optional list item indicating membership of a target user to that list. *)
3171+3172+type list_with_membership = {
3173+ list_ : Jsont.json;
3174+ list_item : Jsont.json option;
3175+}
3176+3177+(** Jsont codec for {!type:list_with_membership}. *)
3178+val list_with_membership_jsont : list_with_membership Jsont.t
3179+3180+(** Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth. *)
31813182(** Query/procedure parameters. *)
3183type params = {
3184+ actor : string; (** The account (actor) to check for membership. *)
3185 cursor : string option;
3186 limit : int option;
3187+ purposes : string list option; (** Optional filter by list purpose. If not specified, all supported types are returned. *)
3188}
31893190(** Jsont codec for {!type:params}. *)
···31933194type output = {
3195 cursor : string option;
3196+ lists_with_membership : Jsont.json list;
3197}
31983199(** Jsont codec for {!type:output}. *)
3200val output_jsont : output Jsont.t
32013202 end
3203+ module GetListMutes : sig
3204+(** Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth. *)
3205+3206+(** Query/procedure parameters. *)
3207+type params = {
3208+ cursor : string option;
3209+ limit : int option;
3210+}
32113212+(** Jsont codec for {!type:params}. *)
3213+val params_jsont : params Jsont.t
32143215+3216+type output = {
3217+ cursor : string option;
3218+ lists : Jsont.json list;
3219}
32203221+(** Jsont codec for {!type:output}. *)
3222+val output_jsont : output Jsont.t
32233224 end
3225+ module GetActorStarterPacks : sig
3226+(** Get a list of starter packs created by the actor. *)
32273228+(** Query/procedure parameters. *)
3229+type params = {
3230+ actor : string;
3231+ cursor : string option;
3232+ limit : int option;
3233+}
32343235+(** Jsont codec for {!type:params}. *)
3236+val params_jsont : params Jsont.t
3237+3238+3239+type output = {
3240+ cursor : string option;
3241+ starter_packs : Jsont.json list;
3242}
32433244+(** Jsont codec for {!type:output}. *)
3245+val output_jsont : output Jsont.t
32463247 end
3248+ module SearchStarterPacks : sig
3249+(** Find starter packs matching search criteria. Does not require auth. *)
32503251+(** Query/procedure parameters. *)
3252+type params = {
3253+ cursor : string option;
3254+ limit : int option;
3255+ q : string; (** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. *)
3256}
32573258+(** Jsont codec for {!type:params}. *)
3259+val params_jsont : params Jsont.t
3260032613262+type output = {
3263+ cursor : string option;
3264+ starter_packs : Jsont.json list;
3265}
32663267+(** Jsont codec for {!type:output}. *)
3268+val output_jsont : output Jsont.t
32693270 end
3271+ module GetStarterPacksWithMembership : sig
3272+(** A starter pack and an optional list item indicating membership of a target user to that starter pack. *)
3273+3274+type starter_pack_with_membership = {
3275+ list_item : Jsont.json option;
3276+ starter_pack : Jsont.json;
3277+}
3278+3279+(** Jsont codec for {!type:starter_pack_with_membership}. *)
3280+val starter_pack_with_membership_jsont : starter_pack_with_membership Jsont.t
3281+3282+(** Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth. *)
32833284(** Query/procedure parameters. *)
3285type params = {
3286+ actor : string; (** The account (actor) to check for membership. *)
3287 cursor : string option;
3288 limit : int option;
3289}
···329332943295type output = {
03296 cursor : string option;
3297+ starter_packs_with_membership : Jsont.json list;
3298}
32993300(** Jsont codec for {!type:output}. *)
3301val output_jsont : output Jsont.t
33023303 end
3304+ module GetLists : sig
3305+(** Enumerates the lists created by a specified account (actor). *)
0033063307(** Query/procedure parameters. *)
3308type params = {
3309+ actor : string; (** The account (actor) to enumerate lists from. *)
3310+ cursor : string option;
3311 limit : int option;
3312+ purposes : string list option; (** Optional filter by list purpose. If not specified, all supported types are returned. *)
3313}
33143315(** Jsont codec for {!type:params}. *)
···331733183319type output = {
3320+ cursor : string option;
3321+ lists : Jsont.json list;
3322}
33233324(** Jsont codec for {!type:output}. *)
3325val output_jsont : output Jsont.t
33263327 end
3328+ module GetStarterPack : sig
3329+(** Gets a view of a starter pack. *)
33303331(** Query/procedure parameters. *)
3332type params = {
3333+ starter_pack : string; (** Reference (AT-URI) of the starter pack record. *)
3334+}
3335+3336+(** Jsont codec for {!type:params}. *)
3337+val params_jsont : params Jsont.t
3338+3339+3340+type output = {
3341+ starter_pack : Jsont.json;
3342+}
3343+3344+(** Jsont codec for {!type:output}. *)
3345+val output_jsont : output Jsont.t
3346+3347+ end
3348+ module GetListBlocks : sig
3349+(** Get mod lists that the requesting account (actor) is blocking. Requires auth. *)
3350+3351+(** Query/procedure parameters. *)
3352+type params = {
3353+ cursor : string option;
3354 limit : int option;
3355}
3356···335933603361type output = {
3362+ cursor : string option;
3363+ lists : Jsont.json list;
3364+}
3365+3366+(** Jsont codec for {!type:output}. *)
3367+val output_jsont : output Jsont.t
3368+3369+ end
3370+ module GetStarterPacks : sig
3371+(** Get views for a list of starter packs. *)
3372+3373+(** Query/procedure parameters. *)
3374+type params = {
3375+ uris : string list;
3376+}
3377+3378+(** Jsont codec for {!type:params}. *)
3379+val params_jsont : params Jsont.t
3380+3381+3382+type output = {
3383 starter_packs : Jsont.json list;
3384}
3385···3387val output_jsont : output Jsont.t
33883389 end
3390+ module GetList : sig
3391+(** Gets a 'view' (with additional context) of a specified list. *)
33923393(** Query/procedure parameters. *)
3394type params = {
3395 cursor : string option;
3396 limit : int option;
3397+ list_ : string; (** Reference (AT-URI) of the list record to hydrate. *)
3398}
33993400(** Jsont codec for {!type:params}. *)
···34033404type output = {
3405 cursor : string option;
3406+ items : Jsont.json list;
3407+ list_ : Jsont.json;
3408}
34093410(** Jsont codec for {!type:output}. *)
3411val output_jsont : output Jsont.t
34123413 end
3414+ module List : sig
3415+(** Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists. *)
3416+3417+type main = {
3418+ avatar : Atp.Blob_ref.t option;
3419+ created_at : string;
3420+ description : string option;
3421+ description_facets : Richtext.Facet.main list option;
3422+ labels : Com.Atproto.Label.Defs.self_labels option;
3423+ name : string; (** Display name for list; can not be empty. *)
3424+ purpose : Jsont.json; (** Defines the purpose of the list (aka, moderation-oriented or curration-oriented) *)
3425+}
3426+3427+(** Jsont codec for {!type:main}. *)
3428+val main_jsont : main Jsont.t
3429+3430+ end
3431+ end
3432+ module Bookmark : sig
3433+ module Defs : sig
3434+3435+type bookmark_view = {
3436+ created_at : string option;
3437+ item : Jsont.json;
3438+ subject : Com.Atproto.Repo.StrongRef.main; (** A strong ref to the bookmarked record. *)
3439+}
3440+3441+(** Jsont codec for {!type:bookmark_view}. *)
3442+val bookmark_view_jsont : bookmark_view Jsont.t
3443+3444+(** Object used to store bookmark data in stash. *)
3445+3446+type bookmark = {
3447+ subject : Com.Atproto.Repo.StrongRef.main; (** A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported. *)
3448+}
3449+3450+(** Jsont codec for {!type:bookmark}. *)
3451+val bookmark_jsont : bookmark Jsont.t
3452+3453+ end
3454+ module CreateBookmark : sig
3455+(** Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication. *)
3456+3457+3458+type input = {
3459+ cid : string;
3460+ uri : string;
3461+}
3462+3463+(** Jsont codec for {!type:input}. *)
3464+val input_jsont : input Jsont.t
3465+3466+ end
3467+ module DeleteBookmark : sig
3468+(** Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication. *)
3469+3470+3471+type input = {
3472+ uri : string;
3473+}
3474+3475+(** Jsont codec for {!type:input}. *)
3476+val input_jsont : input Jsont.t
3477+3478+ end
3479+ module GetBookmarks : sig
3480+(** Gets views of records bookmarked by the authenticated user. Requires authentication. *)
34813482(** Query/procedure parameters. *)
3483type params = {
3484+ cursor : string option;
3485 limit : int option;
03486}
34873488(** Jsont codec for {!type:params}. *)
···349034913492type output = {
3493+ bookmarks : Defs.bookmark_view list;
3494+ cursor : string option;
3495}
34963497(** Jsont codec for {!type:output}. *)
3498val output_jsont : output Jsont.t
34993500 end
3501+ end
3502+ module Unspecced : sig
3503 module GetSuggestedFeeds : sig
3504(** Get a list of suggested feeds *)
3505···3520val output_jsont : output Jsont.t
35213522 end
3523+ module GetSuggestedUsers : sig
3524+(** Get a list of suggested users *)
35253526(** Query/procedure parameters. *)
3527type params = {
3528+ category : string option; (** Category of users to get suggestions for. *)
3529 limit : int option;
03530}
35313532(** Jsont codec for {!type:params}. *)
···353435353536type output = {
3537+ actors : Jsont.json list;
3538+ rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
3539}
35403541(** Jsont codec for {!type:output}. *)
3542val output_jsont : output Jsont.t
35433544 end
3545+ module GetSuggestedUsersSkeleton : sig
3546+(** Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers *)
35473548+(** Query/procedure parameters. *)
3549+type params = {
3550+ category : string option; (** Category of users to get suggestions for. *)
3551+ limit : int option;
3552+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3553}
35543555+(** Jsont codec for {!type:params}. *)
3556+val params_jsont : params Jsont.t
00355735583559type output = {
3560+ dids : string list;
3561+ rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
3562}
35633564(** Jsont codec for {!type:output}. *)
···3585val output_jsont : output Jsont.t
35863587 end
3588+ module GetOnboardingSuggestedStarterPacksSkeleton : sig
3589+(** Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks *)
35903591(** Query/procedure parameters. *)
3592type params = {
03593 limit : int option;
3594+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3595}
35963597(** Jsont codec for {!type:params}. *)
···359936003601type output = {
3602+ starter_packs : string list;
03603}
36043605(** Jsont codec for {!type:output}. *)
3606val output_jsont : output Jsont.t
36073608 end
3609+ module GetSuggestedFeedsSkeleton : sig
3610+(** Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds *)
36113612(** Query/procedure parameters. *)
3613type params = {
···362036213622type output = {
3623+ feeds : string list;
3624+}
3625+3626+(** Jsont codec for {!type:output}. *)
3627+val output_jsont : output Jsont.t
3628+3629+ end
3630+ module GetOnboardingSuggestedStarterPacks : sig
3631+(** Get a list of suggested starterpacks for onboarding *)
3632+3633+(** Query/procedure parameters. *)
3634+type params = {
3635+ limit : int option;
3636+}
3637+3638+(** Jsont codec for {!type:params}. *)
3639+val params_jsont : params Jsont.t
3640+3641+3642+type output = {
3643+ starter_packs : Jsont.json list;
3644}
36453646(** Jsont codec for {!type:output}. *)
···3773val age_assurance_event_jsont : age_assurance_event Jsont.t
37743775 end
3776+ module GetConfig : sig
37773778+type live_now_config = {
3779+ did : string;
3780+ domains : string list;
03781}
37823783+(** Jsont codec for {!type:live_now_config}. *)
3784+val live_now_config_jsont : live_now_config Jsont.t
37853786+(** Get miscellaneous runtime configuration. *)
000000378737883789type output = {
3790+ check_email_confirmed : bool option;
3791+ live_now : live_now_config list option;
3792}
37933794(** Jsont codec for {!type:output}. *)
3795val output_jsont : output Jsont.t
37963797 end
3798+ module GetPopularFeedGenerators : sig
3799+(** An unspecced view of globally popular feed generators. *)
38003801(** Query/procedure parameters. *)
3802type params = {
3803+ cursor : string option;
0003804 limit : int option;
3805+ query : string option;
00000003806}
38073808(** Jsont codec for {!type:params}. *)
···38113812type output = {
3813 cursor : string option;
3814+ feeds : Jsont.json list;
03815}
38163817(** Jsont codec for {!type:output}. *)
3818val output_jsont : output Jsont.t
38193820 end
3821+ module GetTaggedSuggestions : sig
38223823+type suggestion = {
3824+ subject : string;
3825+ subject_type : string;
3826+ tag : string;
3827}
38283829+(** Jsont codec for {!type:suggestion}. *)
3830+val suggestion_jsont : suggestion Jsont.t
38313832+(** Get a list of suggestions (feeds and users) tagged with categories *)
38333834(** Query/procedure parameters. *)
3835+type params = unit
00000038363837(** Jsont codec for {!type:params}. *)
3838val params_jsont : params Jsont.t
383938403841type output = {
3842+ suggestions : suggestion list;
003843}
38443845(** Jsont codec for {!type:output}. *)
3846val output_jsont : output Jsont.t
38473848 end
3849+ module GetSuggestedStarterPacksSkeleton : sig
3850+(** Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks *)
38513852(** Query/procedure parameters. *)
3853type params = {
3854 limit : int option;
3855+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
3856}
38573858(** Jsont codec for {!type:params}. *)
···386038613862type output = {
3863+ starter_packs : string list;
03864}
38653866(** Jsont codec for {!type:output}. *)
3867val output_jsont : output Jsont.t
38683869 end
3870+ module InitAgeAssurance : sig
3871+(** Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age. *)
3872+3873+3874+type input = {
3875+ country_code : string; (** An ISO 3166-1 alpha-2 code of the user's location. *)
3876+ email : string; (** The user's email address to receive assurance instructions. *)
3877+ language : string; (** The user's preferred language for communication during the assurance process. *)
3878+}
3879+3880+(** Jsont codec for {!type:input}. *)
3881+val input_jsont : input Jsont.t
3882+3883+3884+type output = Defs.age_assurance_state
3885+3886+(** Jsont codec for {!type:output}. *)
3887+val output_jsont : output Jsont.t
3888+3889+ end
3890+ module GetTrendingTopics : sig
3891+(** Get a list of trending topics *)
38923893(** Query/procedure parameters. *)
3894type params = {
3895 limit : int option;
3896+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. *)
3897}
38983899(** Jsont codec for {!type:params}. *)
···390139023903type output = {
3904+ suggested : Defs.trending_topic list;
3905+ topics : Defs.trending_topic list;
3906}
39073908(** Jsont codec for {!type:output}. *)
···3939val output_jsont : output Jsont.t
39403941 end
3942+ module GetSuggestionsSkeleton : sig
3943+(** Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions *)
3944+3945+(** Query/procedure parameters. *)
3946+type params = {
3947+ cursor : string option;
3948+ limit : int option;
3949+ relative_to_did : string option; (** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. *)
3950+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. *)
3951+}
39523953+(** Jsont codec for {!type:params}. *)
3954+val params_jsont : params Jsont.t
39553956+3957+type output = {
3958+ actors : Defs.skeleton_search_actor list;
3959+ cursor : string option;
3960+ rec_id : int option; (** Snowflake for this recommendation, use when submitting recommendation events. *)
3961+ relative_to_did : string option; (** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. *)
3962}
39633964+(** Jsont codec for {!type:output}. *)
3965+val output_jsont : output Jsont.t
3966+3967+ end
3968+ module GetAgeAssuranceState : sig
3969+(** Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required. *)
397039713972type output = Defs.age_assurance_state
···4000val output_jsont : output Jsont.t
40014002 end
4003+ module SearchPostsSkeleton : sig
4004+(** Backend Posts search, returns only skeleton *)
4005+4006+(** Query/procedure parameters. *)
4007+type params = {
4008+ author : string option; (** Filter to posts by the given account. Handles are resolved to DID before query-time. *)
4009+ cursor : string option; (** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. *)
4010+ domain : string option; (** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. *)
4011+ lang : string option; (** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. *)
4012+ limit : int option;
4013+ mentions : string option; (** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. *)
4014+ q : string; (** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. *)
4015+ since : string option; (** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). *)
4016+ sort : string option; (** Specifies the ranking order of results. *)
4017+ tag : string list option; (** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. *)
4018+ until : string option; (** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). *)
4019+ url : string option; (** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. *)
4020+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries. *)
4021+}
4022+4023+(** Jsont codec for {!type:params}. *)
4024+val params_jsont : params Jsont.t
4025+4026+4027+type output = {
4028+ cursor : string option;
4029+ hits_total : int option; (** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. *)
4030+ posts : Defs.skeleton_search_post list;
4031+}
4032+4033+(** Jsont codec for {!type:output}. *)
4034+val output_jsont : output Jsont.t
4035+4036+ end
4037 module SearchActorsSkeleton : sig
4038(** Backend Actors (profile) search, returns only skeleton. *)
4039···4060val output_jsont : output Jsont.t
40614062 end
4063+ module GetTrends : sig
4064+(** Get the current trends on the network *)
40654066+(** Query/procedure parameters. *)
4067+type params = {
4068+ limit : int option;
4069+}
40704071+(** Jsont codec for {!type:params}. *)
4072+val params_jsont : params Jsont.t
4073+4074+4075+type output = {
4076+ trends : Defs.trend_view list;
4077+}
40784079(** Jsont codec for {!type:output}. *)
4080val output_jsont : output Jsont.t
40814082 end
4083+ module GetPostThreadV2 : sig
4084+4085+type thread_item = {
4086+ depth : int; (** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. *)
4087+ uri : string;
4088+ value : Jsont.json;
4089+}
4090+4091+(** Jsont codec for {!type:thread_item}. *)
4092+val thread_item_jsont : thread_item Jsont.t
4093+4094+(** (NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests. *)
40954096(** Query/procedure parameters. *)
4097type params = {
4098+ above : bool option; (** Whether to include parents above the anchor. *)
4099+ anchor : string; (** Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post. *)
4100+ below : int option; (** How many levels of replies to include below the anchor. *)
4101+ branching_factor : int option; (** Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated). *)
4102+ sort : string option; (** Sorting for the thread replies. *)
4103}
41044105(** Jsont codec for {!type:params}. *)
···410741084109type output = {
4110+ has_other_replies : bool; (** Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them. *)
4111+ thread : thread_item list; (** A flat list of thread items. The depth of each item is indicated by the depth property inside the item. *)
4112+ threadgate : Jsont.json option;
04113}
41144115(** Jsont codec for {!type:output}. *)
4116val output_jsont : output Jsont.t
41174118 end
4119+ module GetTrendsSkeleton : sig
4120+(** Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends *)
41214122(** Query/procedure parameters. *)
4123type params = {
4124 limit : int option;
4125+ viewer : string option; (** DID of the account making the request (not included for public/unauthenticated queries). *)
4126}
41274128(** Jsont codec for {!type:params}. *)
···413041314132type output = {
4133+ trends : Defs.skeleton_trend list;
4134}
41354136(** Jsont codec for {!type:output}. *)
···30end
31module Site : sig
32 module Standard : sig
33- module Graph : sig
34- module Subscription : sig
35-(** Record declaring a subscription to a publication. *)
36-37-type main = {
38- publication : string; (** AT-URI reference to the publication record being subscribed to (ex: at://did:plc:abc123/site.standard.publication/xyz789). *)
39-}
40-41-(** Jsont codec for {!type:main}. *)
42-val main_jsont : main Jsont.t
43-44- end
45- end
46 module Document : sig
47(** A document record representing a published article, blog post, or other content. Documents can belong to a publication or exist independently. *)
48···96 accent_foreground : Color.rgb; (** Color used for button text. *)
97 background : Color.rgb; (** Color used for content background. *)
98 foreground : Color.rgb; (** Color used for content text. *)
000000000000099}
100101(** Jsont codec for {!type:main}. *)
···30end
31module Site : sig
32 module Standard : sig
000000000000033 module Document : sig
34(** A document record representing a published article, blog post, or other content. Documents can belong to a publication or exist independently. *)
35···83 accent_foreground : Color.rgb; (** Color used for button text. *)
84 background : Color.rgb; (** Color used for content background. *)
85 foreground : Color.rgb; (** Color used for content text. *)
86+}
87+88+(** Jsont codec for {!type:main}. *)
89+val main_jsont : main Jsont.t
90+91+ end
92+ end
93+ module Graph : sig
94+ module Subscription : sig
95+(** Record declaring a subscription to a publication. *)
96+97+type main = {
98+ publication : string; (** AT-URI reference to the publication record being subscribed to (ex: at://did:plc:abc123/site.standard.publication/xyz789). *)
99}
100101(** Jsont codec for {!type:main}. *)
···1+# Comprehensive IMAP Implementation Plan
2+3+This document consolidates all RFC implementation plans from `spec/` and `lib/imap/PLAN.md` into a single, prioritized implementation roadmap. Per the design goals, we favor OCaml variants over strings and do not require backwards compatibility.
4+5+## Executive Summary
6+7+The ocaml-imap library implements IMAP4rev2 (RFC 9051) with several extensions. This plan covers:
8+- **P0**: Critical fixes and core infrastructure
9+- **P1**: Core protocol compliance
10+- **P2**: Extension support (SORT/THREAD, QUOTA, etc.)
11+- **P3**: Advanced features (UTF-8, CONDSTORE/QRESYNC)
12+- **P4**: Polish (documentation, unified flag library)
13+14+---
15+16+## Phase 0: Critical Fixes (P0)
17+18+These are blocking issues that need immediate attention.
19+20+### 0.1 Fix SEARCH Response Parsing (Client Library)
21+22+**Source**: `lib/imap/PLAN.md` - P0 Broken Functionality
23+24+**Problem**: `search` function always returns empty list - response is never parsed.
25+26+**Files**:
27+- `lib/imap/read.ml` - Add SEARCH response parsing
28+- `lib/imap/client.ml:536-544` - Fix to read response
29+30+**Implementation**:
31+```ocaml
32+(* In read.ml - add case for SEARCH response *)
33+| "SEARCH" ->
34+ let rec parse_numbers acc =
35+ match R.peek_char r with
36+ | Some ' ' -> sp r; parse_numbers (number r :: acc)
37+ | Some c when c >= '0' && c <= '9' -> parse_numbers (number r :: acc)
38+ | _ -> List.rev acc
39+ in
40+ let nums = parse_numbers [] in
41+ crlf r;
42+ Response.Search nums
43+```
44+45+**Tests** (`test/test_read.ml`):
46+```ocaml
47+let test_search_response () =
48+ let resp = parse "* SEARCH 2 4 7 11\r\n" in
49+ Alcotest.(check (list int)) "search" [2; 4; 7; 11]
50+ (match resp with Response.Search nums -> nums | _ -> [])
51+52+let test_search_empty () =
53+ let resp = parse "* SEARCH\r\n" in
54+ Alcotest.(check (list int)) "empty search" []
55+ (match resp with Response.Search nums -> nums | _ -> [-1])
56+```
57+58+### 0.2 Parse BODY/BODYSTRUCTURE Responses
59+60+**Source**: `lib/imap/PLAN.md` - P1 Incomplete Core Features
61+62+**Problem**: FETCH responses with BODY/BODYSTRUCTURE fall back to empty flags.
63+64+**Files**:
65+- `lib/imap/read.ml:284-302` - Add BODY/BODYSTRUCTURE parsing
66+- `lib/imap/body.ml` - Body structure types (may need new file)
67+68+**Implementation**: Parse nested multipart MIME structures recursively.
69+70+**Tests**:
71+```ocaml
72+let test_body_structure () =
73+ let resp = parse {|* 1 FETCH (BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "UTF-8") NIL NIL "7BIT" 1234 56))|} in
74+ (* verify body structure parsed correctly *)
75+```
76+77+### 0.3 Parse BODY[section] Literal Responses
78+79+**Source**: `lib/imap/PLAN.md` - P1
80+81+**Problem**: Cannot read actual message content from FETCH.
82+83+**Implementation**: Parse section specifiers and literal data:
84+```ocaml
85+(* Patterns: BODY[HEADER], BODY[TEXT], BODY[1.2.MIME], BODY[section]<origin> *)
86+```
87+88+---
89+90+## Phase 1: Core Protocol Compliance (P1)
91+92+### 1.1 Complete ESEARCH Support (RFC 4731)
93+94+**Source**: `spec/PLAN-rfc4731.md`
95+96+**Current State**: Response type exists, parsing not implemented.
97+98+**Tasks**:
99+1. Add ESEARCH response parsing to `lib/imap/read.ml`
100+2. Add search return options to Command type
101+3. Add serialization for `RETURN (MIN MAX COUNT ALL)`
102+4. Add client API functions
103+104+**Types** (use variants, no strings):
105+```ocaml
106+type search_return_opt =
107+ | Return_min
108+ | Return_max
109+ | Return_all
110+ | Return_count
111+112+type esearch_result =
113+ | Esearch_min of int
114+ | Esearch_max of int
115+ | Esearch_count of int
116+ | Esearch_all of Seq.t
117+```
118+119+**Tests**:
120+```ocaml
121+let test_esearch_parsing () =
122+ let resp = parse "* ESEARCH (TAG \"A282\") MIN 2 COUNT 3\r\n" in
123+ assert (resp = Response.Esearch {
124+ tag = Some "A282";
125+ uid = false;
126+ results = [Esearch_min 2; Esearch_count 3]
127+ })
128+```
129+130+### 1.2 Parse APPENDUID/COPYUID Response Codes
131+132+**Source**: `lib/imap/PLAN.md` - P1
133+134+**Files**: `lib/imap/read.ml:169-228`
135+136+**Implementation**:
137+```ocaml
138+(* Add to response_code parsing *)
139+| "APPENDUID" ->
140+ sp r;
141+ let uidvalidity = number32 r in
142+ sp r;
143+ let uid = number32 r in
144+ Code.Appenduid (uidvalidity, uid)
145+| "COPYUID" ->
146+ sp r;
147+ let uidvalidity = number32 r in
148+ sp r;
149+ let source_uids = parse_uid_set r in
150+ sp r;
151+ let dest_uids = parse_uid_set r in
152+ Code.Copyuid (uidvalidity, source_uids, dest_uids)
153+```
154+155+### 1.3 UNSELECT Capability Advertisement
156+157+**Source**: `spec/PLAN-rfc3691.md`
158+159+**Status**: Fully implemented except capability not advertised.
160+161+**Fix** (`lib/imapd/server.ml`):
162+```ocaml
163+let base_capabilities_pre_tls = [
164+ (* existing *)
165+ "UNSELECT"; (* RFC 3691 - already implemented *)
166+]
167+```
168+169+### 1.4 SPECIAL-USE Support (RFC 6154)
170+171+**Source**: `spec/PLAN-rfc6154.md`
172+173+**Current**: Types exist, capability not advertised, flags not returned.
174+175+**Tasks**:
176+1. Add `SPECIAL-USE` to capabilities
177+2. Return special-use flags in LIST responses
178+3. Map standard mailbox names to attributes
179+180+**Types** (already exist, ensure completeness):
181+```ocaml
182+type special_use =
183+ | All | Archive | Drafts | Flagged | Important
184+ | Junk | Sent | Trash
185+ | Snoozed | Scheduled | Memos (* draft-ietf-mailmaint *)
186+```
187+188+**Tests**:
189+```ocaml
190+let test_list_special_use () =
191+ (* LIST "" "*" should return \Drafts on Drafts mailbox *)
192+```
193+194+---
195+196+## Phase 2: Extension Support (P2)
197+198+### 2.1 SORT/THREAD Extension (RFC 5256)
199+200+**Source**: `spec/PLAN-rfc5256.md`
201+202+**Scope**: Large feature - server-side sorting and threading.
203+204+#### 2.1.1 Thread Module Types
205+206+**New file**: `lib/imap/thread.ml`
207+208+```ocaml
209+type algorithm =
210+ | Orderedsubject (** Group by subject, sort by date *)
211+ | References (** Full JWZ threading algorithm *)
212+ | Extension of string
213+214+type 'a node =
215+ | Message of 'a * 'a node list
216+ | Dummy of 'a node list
217+218+type 'a t = 'a node list
219+```
220+221+#### 2.1.2 Base Subject Extraction
222+223+**New file**: `lib/imap/subject.ml`
224+225+Implements RFC 5256 Section 2.1 algorithm:
226+1. Decode RFC 2047 encoded-words
227+2. Remove `Re:`, `Fw:`, `Fwd:` prefixes
228+3. Remove `[blob]` prefixes
229+4. Remove `(fwd)` trailers
230+5. Unwrap `[fwd: ...]` wrappers
231+232+```ocaml
233+val base_subject : string -> string
234+val is_reply_or_forward : string -> bool
235+```
236+237+#### 2.1.3 Sent Date Handling
238+239+**New file**: `lib/imap/date.ml`
240+241+```ocaml
242+type t
243+244+val of_header : string -> t option
245+val of_internaldate : string -> t
246+val sent_date : date_header:string option -> internaldate:string -> t
247+val compare : t -> t -> int
248+```
249+250+#### 2.1.4 Server-Side SORT Handler
251+252+**File**: `lib/imapd/server.ml`
253+254+1. Implement sort key extraction
255+2. Implement comparison by criteria
256+3. Return SORT response
257+258+#### 2.1.5 Threading Algorithms
259+260+**New file**: `lib/imapd/thread.ml`
261+262+1. `orderedsubject` - simple subject-based grouping
263+2. `references` - full JWZ algorithm (6 steps)
264+265+**Tests**:
266+```ocaml
267+let test_base_subject () =
268+ assert (Subject.base_subject "Re: test" = "test");
269+ assert (Subject.base_subject "Re: Re: test" = "test");
270+ assert (Subject.base_subject "[PATCH] Re: [ocaml] test" = "test");
271+ assert (Subject.base_subject "[fwd: wrapped]" = "wrapped")
272+273+let test_orderedsubject () =
274+ (* Test grouping by subject *)
275+276+let test_references_threading () =
277+ (* Test parent/child relationships *)
278+```
279+280+### 2.2 QUOTA Extension (RFC 9208)
281+282+**Source**: `spec/PLAN-rfc9208.md`
283+284+#### 2.2.1 Protocol Types
285+286+**File**: `lib/imapd/protocol.ml`
287+288+```ocaml
289+type quota_resource =
290+ | Quota_storage (** KB of storage *)
291+ | Quota_message (** Number of messages *)
292+ | Quota_mailbox (** Number of mailboxes *)
293+ | Quota_annotation_storage
294+295+type quota_resource_info = {
296+ resource : quota_resource;
297+ usage : int64;
298+ limit : int64;
299+}
300+301+(* Commands *)
302+| Getquota of string
303+| Getquotaroot of mailbox_name
304+| Setquota of { root : string; limits : (quota_resource * int64) list }
305+306+(* Responses *)
307+| Quota_response of { root : string; resources : quota_resource_info list }
308+| Quotaroot_response of { mailbox : mailbox_name; roots : string list }
309+```
310+311+#### 2.2.2 Storage Backend Interface
312+313+**File**: `lib/imapd/storage.mli`
314+315+```ocaml
316+val get_quota_roots : t -> username:string -> mailbox_name -> string list
317+val get_quota : t -> username:string -> string -> (quota_resource_info list, error) result
318+val set_quota : t -> username:string -> string -> (quota_resource * int64) list -> (quota_resource_info list, error) result
319+val check_quota : t -> username:string -> mailbox_name -> additional_size:int64 -> bool
320+```
321+322+#### 2.2.3 Server Handlers
323+324+Implement `handle_getquota`, `handle_getquotaroot`, `handle_setquota`.
325+326+Add quota checks to APPEND/COPY/MOVE:
327+```ocaml
328+if not (Storage.check_quota ...) then
329+ send_response flow (No { code = Some Code_overquota; ... })
330+```
331+332+**Tests**:
333+```ocaml
334+let test_getquotaroot () =
335+ (* GETQUOTAROOT INBOX returns quota info *)
336+337+let test_quota_exceeded () =
338+ (* APPEND fails with OVERQUOTA when over limit *)
339+```
340+341+### 2.3 LIST-EXTENDED (RFC 5258)
342+343+**Source**: `spec/PLAN-rfc5258.md`
344+345+**Types**:
346+```ocaml
347+type list_select_option =
348+ | List_select_subscribed
349+ | List_select_remote
350+ | List_select_recursivematch
351+ | List_select_special_use (* RFC 6154 *)
352+353+type list_return_option =
354+ | List_return_subscribed
355+ | List_return_children
356+ | List_return_special_use
357+358+type list_extended_item =
359+ | Childinfo of string list
360+361+type list_command =
362+ | List_basic of { reference : string; pattern : string }
363+ | List_extended of {
364+ selection : list_select_option list;
365+ reference : string;
366+ patterns : string list;
367+ return_opts : list_return_option list;
368+ }
369+```
370+371+**Tasks**:
372+1. Update grammar for extended LIST syntax
373+2. Add `\NonExistent` and `\Remote` attributes
374+3. Implement subscription tracking in storage
375+4. Handle RECURSIVEMATCH with CHILDINFO
376+5. Add `LIST-EXTENDED` capability
377+378+---
379+380+## Phase 3: Advanced Features (P3)
381+382+### 3.1 UTF-8 Support (RFC 6855)
383+384+**Source**: `spec/PLAN-rfc6855.md`
385+386+#### 3.1.1 Session State Tracking
387+388+```ocaml
389+type session_state = {
390+ utf8_enabled : bool;
391+ (* ... *)
392+}
393+```
394+395+#### 3.1.2 UTF-8 Validation
396+397+**New file**: `lib/imapd/utf8.ml`
398+399+```ocaml
400+val is_valid_utf8 : string -> bool
401+val has_non_ascii : string -> bool
402+val is_valid_utf8_mailbox_name : string -> bool
403+```
404+405+#### 3.1.3 ENABLE Handler Update
406+407+Track UTF8=ACCEPT state, reject SEARCH with CHARSET after enable.
408+409+#### 3.1.4 UTF8 APPEND Extension
410+411+Parse `UTF8 (literal)` syntax for 8-bit headers.
412+413+**Tests**:
414+```ocaml
415+let test_utf8_validation () =
416+ assert (Utf8.is_valid_utf8 "Hello");
417+ assert (Utf8.is_valid_utf8 "\xe4\xb8\xad\xe6\x96\x87");
418+ assert (not (Utf8.is_valid_utf8 "\xff\xfe"))
419+```
420+421+### 3.2 CONDSTORE/QRESYNC (RFC 7162)
422+423+**Source**: `lib/imap/PLAN.md` - P2, `PLAN.md` - Phase 2.2
424+425+#### 3.2.1 CONDSTORE Types
426+427+```ocaml
428+(* Fetch items *)
429+| Modseq
430+| Item_modseq of int64
431+432+(* Response codes *)
433+| Highestmodseq of int64
434+| Nomodseq
435+| Modified of Seq.t
436+437+(* Command modifiers *)
438+type fetch_modifier = { changedsince : int64 option }
439+type store_modifier = { unchangedsince : int64 option }
440+```
441+442+#### 3.2.2 Storage Backend
443+444+Add `modseq` to message type and mailbox state:
445+```ocaml
446+type message = {
447+ (* existing *)
448+ modseq : int64;
449+}
450+451+type mailbox_state = {
452+ (* existing *)
453+ highestmodseq : int64;
454+}
455+```
456+457+#### 3.2.3 QRESYNC
458+459+```ocaml
460+type qresync_params = {
461+ uidvalidity : int32;
462+ modseq : int64;
463+ known_uids : Seq.t option;
464+ seq_match : (Seq.t * Seq.t) option;
465+}
466+467+(* Response *)
468+| Vanished of { earlier : bool; uids : Seq.t }
469+```
470+471+---
472+473+## Phase 4: Polish and Infrastructure (P4)
474+475+### 4.1 RFC 5530 Response Code Documentation
476+477+**Source**: `spec/PLAN-rfc5530.md`
478+479+All 16 response codes already implemented. Add OCamldoc citations.
480+481+### 4.2 Unified Mail Flag Library
482+483+**Source**: `spec/PLAN-unified-mail-flag.md`
484+485+Create shared `mail-flag` library for IMAP/JMAP:
486+487+```
488+mail-flag/
489+├── keyword.ml # Message keywords (typed variants)
490+├── system_flag.ml # IMAP \Seen, \Deleted, etc.
491+├── mailbox_attr.ml # Mailbox attributes/roles
492+├── flag_color.ml # Apple Mail flag colors
493+├── imap_wire.ml # IMAP serialization
494+└── jmap_wire.ml # JMAP serialization
495+```
496+497+### 4.3 Infrastructure Improvements
498+499+**Source**: `PLAN.md` - Phase 1
500+501+1. **Replace Menhir with Eio.Buf_read** - Pure functional parser
502+2. **Integrate conpool** - Connection pooling for client
503+3. **Add bytesrw streaming** - Large message handling
504+4. **Fuzz testing** - Parser robustness with Crowbar
505+5. **Eio mock testing** - Deterministic tests
506+507+---
508+509+## Testing Strategy
510+511+### Unit Tests
512+513+Each module should have corresponding tests in `test/`:
514+515+| Module | Test File | Coverage |
516+|--------|-----------|----------|
517+| `lib/imap/read.ml` | `test/test_read.ml` | Response parsing |
518+| `lib/imap/write.ml` | `test/test_write.ml` | Command serialization |
519+| `lib/imap/subject.ml` | `test/test_subject.ml` | Base subject extraction |
520+| `lib/imap/thread.ml` | `test/test_thread.ml` | Threading algorithms |
521+| `lib/imapd/server.ml` | `test/test_server.ml` | Command handlers |
522+| `lib/imapd/storage.ml` | `test/test_storage.ml` | Storage backends |
523+524+### Integration Tests
525+526+**File**: `test/integration/`
527+528+- Protocol compliance testing against real servers
529+- ImapTest compatibility suite
530+- Dovecot interoperability
531+532+### Fuzz Tests
533+534+**File**: `test/fuzz_parser.ml`
535+536+```ocaml
537+let fuzz_command_parser =
538+ Crowbar.(map [bytes] (fun input ->
539+ try
540+ ignore (Imap_parser.parse_command input);
541+ true
542+ with _ -> true (* Parser should never crash *)
543+ ))
544+```
545+546+---
547+548+## Implementation Order
549+550+### Sprint 1: P0 Critical Fixes
551+1. [ ] Fix SEARCH response parsing
552+2. [ ] Parse BODY/BODYSTRUCTURE responses
553+3. [ ] Parse BODY[section] literals
554+555+### Sprint 2: P1 Core Compliance
556+4. [ ] Complete ESEARCH support
557+5. [ ] Parse APPENDUID/COPYUID response codes
558+6. [ ] Add UNSELECT to capabilities
559+7. [ ] Complete SPECIAL-USE support
560+561+### Sprint 3: P2 SORT/THREAD
562+8. [ ] Thread module types
563+9. [ ] Base subject extraction
564+10. [ ] Sent date handling
565+11. [ ] ORDEREDSUBJECT algorithm
566+12. [ ] REFERENCES algorithm
567+13. [ ] Server SORT/THREAD handlers
568+569+### Sprint 4: P2 QUOTA
570+14. [ ] Quota protocol types
571+15. [ ] Storage backend interface
572+16. [ ] Memory storage quota
573+17. [ ] Maildir storage quota
574+18. [ ] Server handlers
575+576+### Sprint 5: P2 LIST-EXTENDED
577+19. [ ] Extended LIST grammar
578+20. [ ] New attributes
579+21. [ ] Subscription tracking
580+22. [ ] RECURSIVEMATCH support
581+582+### Sprint 6: P3 UTF-8 & CONDSTORE
583+23. [ ] UTF-8 session state
584+24. [ ] UTF-8 validation
585+25. [ ] UTF8 APPEND extension
586+26. [ ] CONDSTORE types
587+27. [ ] CONDSTORE handlers
588+28. [ ] QRESYNC support
589+590+### Sprint 7: P4 Polish
591+29. [ ] Response code documentation
592+30. [ ] Unified mail flag library
593+31. [ ] Infrastructure improvements
594+32. [ ] Comprehensive test suite
595+596+---
597+598+## File Modification Summary
599+600+### New Files
601+602+| File | Purpose |
603+|------|---------|
604+| `lib/imap/thread.ml` | Thread types and parsing |
605+| `lib/imap/subject.ml` | Base subject extraction |
606+| `lib/imap/date.ml` | Sent date handling |
607+| `lib/imap/collation.ml` | Unicode collation |
608+| `lib/imap/mime.ml` | RFC 2047 decoding |
609+| `lib/imapd/thread.ml` | Threading algorithms |
610+| `lib/imapd/utf8.ml` | UTF-8 validation |
611+| `test/test_subject.ml` | Subject tests |
612+| `test/test_thread.ml` | Threading tests |
613+| `test/test_quota.ml` | Quota tests |
614+| `test/fuzz_parser.ml` | Fuzz tests |
615+616+### Modified Files
617+618+| File | Changes |
619+|------|---------|
620+| `lib/imap/read.ml` | SEARCH, ESEARCH, BODY parsing |
621+| `lib/imap/write.ml` | ESEARCH, THREAD serialization |
622+| `lib/imap/command.ml` | Return options, THREAD command |
623+| `lib/imap/response.ml` | ESEARCH, THREAD responses |
624+| `lib/imap/client.ml` | Fix search, add esearch/thread |
625+| `lib/imap/code.ml` | OCamldoc citations |
626+| `lib/imap/list_attr.ml` | Add NonExistent, Remote |
627+| `lib/imapd/protocol.ml` | Quota types, LIST-EXTENDED |
628+| `lib/imapd/server.ml` | Handlers, capabilities |
629+| `lib/imapd/storage.ml` | Quota ops, subscription tracking |
630+| `lib/imapd/grammar.mly` | Extended LIST, QUOTA, UTF8 |
631+| `lib/imapd/lexer.mll` | New tokens |
632+| `lib/imapd/parser.ml` | Response serialization |
633+634+---
635+636+## Design Principles
637+638+1. **Favor OCaml variants** - Use typed variants over strings where possible
639+2. **No backwards compatibility** - Clean API without legacy shims
640+3. **RFC citations** - OCamldoc links to RFC sections
641+4. **Incremental** - Each task is independently useful
642+5. **Test-driven** - Tests accompany each feature
643+6. **Eio-native** - Use Eio patterns throughout
644+645+---
646+647+## References
648+649+### Implemented RFCs
650+- RFC 9051 - IMAP4rev2 (core)
651+- RFC 8314 - Implicit TLS
652+- RFC 2177 - IDLE
653+- RFC 2342 - NAMESPACE
654+- RFC 2971 - ID
655+- RFC 4315 - UIDPLUS
656+- RFC 5161 - ENABLE
657+- RFC 6851 - MOVE
658+- RFC 7888 - LITERAL+
659+660+### RFCs in This Plan
661+- RFC 3691 - UNSELECT (partially complete)
662+- RFC 4731 - ESEARCH
663+- RFC 5256 - SORT/THREAD
664+- RFC 5258 - LIST-EXTENDED
665+- RFC 5530 - Response Codes (types complete)
666+- RFC 6154 - SPECIAL-USE (partially complete)
667+- RFC 6855 - UTF-8 Support
668+- RFC 7162 - CONDSTORE/QRESYNC
669+- RFC 9208 - QUOTA
+48-2
ocaml-imap/lib/imap/client.ml
···570571let search t ?charset criteria =
572 require_selected t;
573- let tag = send_command t (Command.Search { charset; criteria }) in
574 let untagged, final = receive_responses t tag in
575 check_ok tag untagged final;
576 (* Extract search results from untagged responses *)
···582583let uid_search t ?charset criteria =
584 require_selected t;
585- let tag = send_command t (Command.Uid (Uid_search { charset; criteria })) in
586 let untagged, final = receive_responses t tag in
587 check_ok tag untagged final;
588 (* Extract UID search results from untagged responses - UIDs are returned as int64 *)
···685 (function Response.Enabled exts -> enabled := exts | _ -> ())
686 untagged;
687 !enabled
0000000000000000000000000000000000000000000000
···570571let search t ?charset criteria =
572 require_selected t;
573+ let tag = send_command t (Command.Search { charset; criteria; return_opts = None }) in
574 let untagged, final = receive_responses t tag in
575 check_ok tag untagged final;
576 (* Extract search results from untagged responses *)
···582583let uid_search t ?charset criteria =
584 require_selected t;
585+ let tag = send_command t (Command.Uid (Uid_search { charset; criteria; return_opts = None })) in
586 let untagged, final = receive_responses t tag in
587 check_ok tag untagged final;
588 (* Extract UID search results from untagged responses - UIDs are returned as int64 *)
···685 (function Response.Enabled exts -> enabled := exts | _ -> ())
686 untagged;
687 !enabled
688+689+(** {1 ESEARCH Support (RFC 4731)} *)
690+691+type esearch_result = {
692+ min : int option;
693+ max : int option;
694+ count : int option;
695+ all : Seq.t option;
696+}
697+698+let empty_esearch_result = {
699+ min = None;
700+ max = None;
701+ count = None;
702+ all = None;
703+}
704+705+let parse_esearch_response responses =
706+ List.fold_left (fun acc resp ->
707+ match resp with
708+ | Response.Esearch { results; _ } ->
709+ List.fold_left (fun acc item ->
710+ match item with
711+ | Response.Esearch_min n -> { acc with min = Some n }
712+ | Response.Esearch_max n -> { acc with max = Some n }
713+ | Response.Esearch_count n -> { acc with count = Some n }
714+ | Response.Esearch_all seq -> { acc with all = Some seq }
715+ ) acc results
716+ | _ -> acc
717+ ) empty_esearch_result responses
718+719+let esearch t ?charset ?(return_opts = [Command.Return_all]) criteria =
720+ require_selected t;
721+ require_capability t "ESEARCH";
722+ let tag = send_command t (Command.Search { charset; criteria; return_opts = Some return_opts }) in
723+ let untagged, final = receive_responses t tag in
724+ check_ok tag untagged final;
725+ parse_esearch_response untagged
726+727+let uid_esearch t ?charset ?(return_opts = [Command.Return_all]) criteria =
728+ require_selected t;
729+ require_capability t "ESEARCH";
730+ let tag = send_command t (Command.Uid (Uid_search { charset; criteria; return_opts = Some return_opts })) in
731+ let untagged, final = receive_responses t tag in
732+ check_ok tag untagged final;
733+ parse_esearch_response untagged
+30
ocaml-imap/lib/imap/client.mli
···247248val enable : t -> string list -> string list
249(** [enable client extensions] enables protocol extensions. *)
000000000000000000000000000000
···247248val enable : t -> string list -> string list
249(** [enable client extensions] enables protocol extensions. *)
250+251+(** {1 ESEARCH Support (RFC 4731)} *)
252+253+type esearch_result = {
254+ min : int option;
255+ max : int option;
256+ count : int option;
257+ all : Seq.t option;
258+}
259+(** ESEARCH result containing optional min, max, count, and all values. *)
260+261+val esearch :
262+ t ->
263+ ?charset:string ->
264+ ?return_opts:Command.search_return_opt list ->
265+ Search.t ->
266+ esearch_result
267+(** [esearch client ?charset ?return_opts criteria] performs an extended search.
268+ Returns an {!esearch_result} with the requested information.
269+ Default [return_opts] is [[Return_all]].
270+ Requires ESEARCH extension. *)
271+272+val uid_esearch :
273+ t ->
274+ ?charset:string ->
275+ ?return_opts:Command.search_return_opt list ->
276+ Search.t ->
277+ esearch_result
278+(** [uid_esearch client ?charset ?return_opts criteria] like {!esearch} but for UID searches.
279+ Requires ESEARCH extension. *)
···25 | Binary of string * (int * int) option
26 | Binary_peek of string * (int * int) option
27 | Binary_size of string
02829val pp_request : Format.formatter -> request -> unit
30
···25 | Binary of string * (int * int) option
26 | Binary_peek of string * (int * int) option
27 | Binary_size of string
28+ | Modseq (** Request MODSEQ value - RFC 7162 CONDSTORE *)
2930val pp_request : Format.formatter -> request -> unit
31
+52-1
ocaml-imap/lib/imap/flag.ml
···56(** Message Flags
78- IMAP message flags as specified in RFC 9051 Section 2.3.2. *)
000910type system =
11 | Seen (** Message has been read *)
···20 | Flagged -> Fmt.string ppf "\\Flagged"
21 | Deleted -> Fmt.string ppf "\\Deleted"
22 | Draft -> Fmt.string ppf "\\Draft"
002324type t =
25 | System of system
···47 | "\\DRAFT" -> Some (System Draft)
48 | _ ->
49 if String.length s > 0 && s.[0] <> '\\' then Some (Keyword s) else None
0000000000000000000000000000000000000000000000
···56(** Message Flags
78+ Re-exports from {!Mail_flag} for IMAP-specific use.
9+ See {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}. *)
10+11+(** {1 System Flags} *)
1213type system =
14 | Seen (** Message has been read *)
···23 | Flagged -> Fmt.string ppf "\\Flagged"
24 | Deleted -> Fmt.string ppf "\\Deleted"
25 | Draft -> Fmt.string ppf "\\Draft"
26+27+(** {1 Flags} *)
2829type t =
30 | System of system
···52 | "\\DRAFT" -> Some (System Draft)
53 | _ ->
54 if String.length s > 0 && s.[0] <> '\\' then Some (Keyword s) else None
55+56+(** {1 Conversion to/from mail-flag} *)
57+58+let system_to_keyword : system -> Mail_flag.Keyword.t = function
59+ | Seen -> `Seen
60+ | Answered -> `Answered
61+ | Flagged -> `Flagged
62+ | Deleted -> `Deleted
63+ | Draft -> `Draft
64+65+let system_of_keyword : Mail_flag.Keyword.standard -> system option = function
66+ | `Seen -> Some Seen
67+ | `Answered -> Some Answered
68+ | `Flagged -> Some Flagged
69+ | `Deleted -> Some Deleted
70+ | `Draft -> Some Draft
71+ | `Forwarded -> None
72+73+let to_mail_flag : t -> Mail_flag.Imap_wire.flag = function
74+ | System Seen -> Mail_flag.Imap_wire.System `Seen
75+ | System Answered -> Mail_flag.Imap_wire.System `Answered
76+ | System Flagged -> Mail_flag.Imap_wire.System `Flagged
77+ | System Deleted -> Mail_flag.Imap_wire.System `Deleted
78+ | System Draft -> Mail_flag.Imap_wire.System `Draft
79+ | Keyword k -> Mail_flag.Imap_wire.Keyword (Mail_flag.Keyword.of_string k)
80+81+let of_mail_flag : Mail_flag.Imap_wire.flag -> t = function
82+ | Mail_flag.Imap_wire.System `Seen -> System Seen
83+ | Mail_flag.Imap_wire.System `Answered -> System Answered
84+ | Mail_flag.Imap_wire.System `Flagged -> System Flagged
85+ | Mail_flag.Imap_wire.System `Deleted -> System Deleted
86+ | Mail_flag.Imap_wire.System `Draft -> System Draft
87+ | Mail_flag.Imap_wire.Keyword k -> Keyword (Mail_flag.Keyword.to_string k)
88+89+let to_keyword : t -> Mail_flag.Keyword.t = function
90+ | System s -> system_to_keyword s
91+ | Keyword k -> Mail_flag.Keyword.of_string k
92+93+let of_keyword (k : Mail_flag.Keyword.t) : t =
94+ match k with
95+ | `Seen -> System Seen
96+ | `Answered -> System Answered
97+ | `Flagged -> System Flagged
98+ | `Deleted -> System Deleted
99+ | `Draft -> System Draft
100+ | other -> Keyword (Mail_flag.Keyword.to_string other)
+26-1
ocaml-imap/lib/imap/flag.mli
···56(** Message Flags
78- IMAP message flags as specified in RFC 9051 Section 2.3.2. *)
0910(** {1 System Flags} *)
11···36val pp : Format.formatter -> t -> unit
37val to_string : t -> string
38val of_string : string -> t option
000000000000000000000000
···56(** Message Flags
78+ Re-exports from {!Mail_flag} for IMAP-specific use.
9+ See {{:https://datatracker.ietf.org/doc/html/rfc9051#section-2.3.2}RFC 9051 Section 2.3.2}. *)
1011(** {1 System Flags} *)
12···37val pp : Format.formatter -> t -> unit
38val to_string : t -> string
39val of_string : string -> t option
40+41+(** {1 Conversion to/from mail-flag}
42+43+ These functions allow interoperability with the {!Mail_flag} library
44+ for cross-protocol flag handling. *)
45+46+val system_to_keyword : system -> Mail_flag.Keyword.t
47+(** [system_to_keyword sys] converts an IMAP system flag to a mail-flag keyword. *)
48+49+val system_of_keyword : Mail_flag.Keyword.standard -> system option
50+(** [system_of_keyword kw] converts a standard mail-flag keyword to an IMAP system flag.
51+ Returns [None] for keywords like [`Forwarded] that have no IMAP system flag equivalent. *)
52+53+val to_mail_flag : t -> Mail_flag.Imap_wire.flag
54+(** [to_mail_flag flag] converts an IMAP flag to a mail-flag wire format flag. *)
55+56+val of_mail_flag : Mail_flag.Imap_wire.flag -> t
57+(** [of_mail_flag flag] converts a mail-flag wire format flag to an IMAP flag. *)
58+59+val to_keyword : t -> Mail_flag.Keyword.t
60+(** [to_keyword flag] converts an IMAP flag to a mail-flag keyword. *)
61+62+val of_keyword : Mail_flag.Keyword.t -> t
63+(** [of_keyword kw] converts a mail-flag keyword to an IMAP flag. *)
+3
ocaml-imap/lib/imap/imap.ml
···57 - {!module:Fetch} - FETCH request/response items
58 - {!module:Search} - SEARCH criteria
59 - {!module:Sort} - SORT criteria (RFC 5256)
060 - {!module:Store} - STORE actions
61 - {!module:Status} - STATUS items
62 - {!module:List_attr} - LIST mailbox attributes
···99module Store = Store
100module Status = Status
101module List_attr = List_attr
00102103(** {1 Client} *)
104
···57 - {!module:Fetch} - FETCH request/response items
58 - {!module:Search} - SEARCH criteria
59 - {!module:Sort} - SORT criteria (RFC 5256)
60+ - {!module:Subject} - Base subject extraction (RFC 5256)
61 - {!module:Store} - STORE actions
62 - {!module:Status} - STATUS items
63 - {!module:List_attr} - LIST mailbox attributes
···100module Store = Store
101module Status = Status
102module List_attr = List_attr
103+module Subject = Subject
104+module Thread = Thread
105106(** {1 Client} *)
107
+3
ocaml-imap/lib/imap/imap.mli
···57 - {!module:Fetch} - FETCH request/response items
58 - {!module:Search} - SEARCH criteria
59 - {!module:Sort} - SORT criteria (RFC 5256)
060 - {!module:Store} - STORE actions
61 - {!module:Status} - STATUS items
62 - {!module:List_attr} - LIST mailbox attributes
···99module Store = Store
100module Status = Status
101module List_attr = List_attr
00102103(** {1 Client} *)
104
···57 - {!module:Fetch} - FETCH request/response items
58 - {!module:Search} - SEARCH criteria
59 - {!module:Sort} - SORT criteria (RFC 5256)
60+ - {!module:Subject} - Base subject extraction (RFC 5256)
61 - {!module:Store} - STORE actions
62 - {!module:Status} - STATUS items
63 - {!module:List_attr} - LIST mailbox attributes
···100module Store = Store
101module Status = Status
102module List_attr = List_attr
103+module Subject = Subject
104+module Thread = Thread
105106(** {1 Client} *)
107
+102-1
ocaml-imap/lib/imap/list_attr.ml
···56(** LIST Command Attributes
78- Mailbox attributes returned by LIST command.
9 See RFC 9051 Section 7.2.2. *)
1011type t =
···43 | Extension s -> Fmt.string ppf s
4445let to_string a = Fmt.str "%a" pp a
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···56(** LIST Command Attributes
78+ Re-exports from {!Mail_flag.Mailbox_attr}.
9 See RFC 9051 Section 7.2.2. *)
1011type t =
···43 | Extension s -> Fmt.string ppf s
4445let to_string a = Fmt.str "%a" pp a
46+47+let of_string s =
48+ let s' = String.lowercase_ascii s in
49+ (* Remove leading backslash if present *)
50+ let s' = if String.length s' > 0 && s'.[0] = '\\' then
51+ String.sub s' 1 (String.length s' - 1)
52+ else s'
53+ in
54+ match s' with
55+ | "noinferiors" -> Noinferiors
56+ | "noselect" -> Noselect
57+ | "marked" -> Marked
58+ | "unmarked" -> Unmarked
59+ | "subscribed" -> Subscribed
60+ | "haschildren" -> Haschildren
61+ | "hasnochildren" -> Hasnochildren
62+ | "all" -> All
63+ | "archive" -> Archive
64+ | "drafts" -> Drafts
65+ | "flagged" -> Flagged
66+ | "junk" | "spam" -> Junk
67+ | "sent" -> Sent
68+ | "trash" -> Trash
69+ | _ -> Extension s
70+71+(** {1 Conversion to/from mail-flag} *)
72+73+let to_mailbox_attr : t -> Mail_flag.Mailbox_attr.t = function
74+ | Noinferiors -> `Noinferiors
75+ | Noselect -> `Noselect
76+ | Marked -> `Marked
77+ | Unmarked -> `Unmarked
78+ | Subscribed -> `Subscribed
79+ | Haschildren -> `HasChildren
80+ | Hasnochildren -> `HasNoChildren
81+ | All -> `All
82+ | Archive -> `Archive
83+ | Drafts -> `Drafts
84+ | Flagged -> `Flagged
85+ | Junk -> `Junk
86+ | Sent -> `Sent
87+ | Trash -> `Trash
88+ | Extension s -> `Extension s
89+90+let of_mailbox_attr : Mail_flag.Mailbox_attr.t -> t = function
91+ | `Noinferiors -> Noinferiors
92+ | `Noselect -> Noselect
93+ | `Marked -> Marked
94+ | `Unmarked -> Unmarked
95+ | `Subscribed -> Subscribed
96+ | `HasChildren -> Haschildren
97+ | `HasNoChildren -> Hasnochildren
98+ | `NonExistent -> Noselect (* NonExistent implies Noselect *)
99+ | `Remote -> Extension "\\Remote"
100+ | `All -> All
101+ | `Archive -> Archive
102+ | `Drafts -> Drafts
103+ | `Flagged -> Flagged
104+ | `Important -> Extension "\\Important"
105+ | `Inbox -> Extension "\\Inbox"
106+ | `Junk -> Junk
107+ | `Sent -> Sent
108+ | `Trash -> Trash
109+ | `Snoozed -> Extension "\\Snoozed"
110+ | `Scheduled -> Extension "\\Scheduled"
111+ | `Memos -> Extension "\\Memos"
112+ | `Extension s -> Extension s
113+114+let to_jmap_role : t -> string option = function
115+ | All -> Some "all"
116+ | Archive -> Some "archive"
117+ | Drafts -> Some "drafts"
118+ | Flagged -> Some "flagged"
119+ | Junk -> Some "junk"
120+ | Sent -> Some "sent"
121+ | Trash -> Some "trash"
122+ | Subscribed -> Some "subscribed"
123+ | Noinferiors | Noselect | Marked | Unmarked
124+ | Haschildren | Hasnochildren | Extension _ -> None
125+126+let of_jmap_role s =
127+ match String.lowercase_ascii s with
128+ | "all" -> Some All
129+ | "archive" -> Some Archive
130+ | "drafts" -> Some Drafts
131+ | "flagged" -> Some Flagged
132+ | "junk" -> Some Junk
133+ | "sent" -> Some Sent
134+ | "trash" -> Some Trash
135+ | "subscribed" -> Some Subscribed
136+ | _ -> None
137+138+let is_special_use = function
139+ | All | Archive | Drafts | Flagged | Junk | Sent | Trash -> true
140+ | Subscribed -> true (* Also a JMAP role *)
141+ | Noinferiors | Noselect | Marked | Unmarked
142+ | Haschildren | Hasnochildren | Extension _ -> false
143+144+let is_selectable = function
145+ | Noselect -> false
146+ | _ -> true
+29-1
ocaml-imap/lib/imap/list_attr.mli
···56(** LIST Command Attributes
78- Mailbox attributes returned by LIST command.
9 See RFC 9051 Section 7.2.2. *)
1011type t =
···2728val pp : Format.formatter -> t -> unit
29val to_string : t -> string
0000000000000000000000000000
···56(** LIST Command Attributes
78+ Re-exports from {!Mail_flag.Mailbox_attr}.
9 See RFC 9051 Section 7.2.2. *)
1011type t =
···2728val pp : Format.formatter -> t -> unit
29val to_string : t -> string
30+val of_string : string -> t
31+32+(** {1 Conversion to/from mail-flag}
33+34+ These functions allow interoperability with the {!Mail_flag} library
35+ for cross-protocol attribute handling. *)
36+37+val to_mailbox_attr : t -> Mail_flag.Mailbox_attr.t
38+(** [to_mailbox_attr attr] converts an IMAP list attribute to a mail-flag mailbox attribute. *)
39+40+val of_mailbox_attr : Mail_flag.Mailbox_attr.t -> t
41+(** [of_mailbox_attr attr] converts a mail-flag mailbox attribute to an IMAP list attribute. *)
42+43+val to_jmap_role : t -> string option
44+(** [to_jmap_role attr] converts a special-use attribute to its JMAP role string.
45+ Returns [None] for LIST attributes that don't correspond to JMAP roles. *)
46+47+val of_jmap_role : string -> t option
48+(** [of_jmap_role role] parses a JMAP role string into a special-use attribute.
49+ Returns [None] if the role string is not recognized. *)
50+51+val is_special_use : t -> bool
52+(** [is_special_use attr] returns [true] if the attribute is a special-use
53+ role (as opposed to a LIST attribute or extension). *)
54+55+val is_selectable : t -> bool
56+(** [is_selectable attr] returns [false] if the attribute indicates the
57+ mailbox cannot be selected (i.e., [Noselect]). *)
+530-78
ocaml-imap/lib/imap/read.ml
···106 in
107 loop []
1080000000000000000000000000000000109(** {1 Flags} *)
110111let system_flag r =
···175 in
176 loop []
1770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000178(** {1 Response Codes} *)
179180let response_code r =
···184 match String.uppercase_ascii name with
185 | "ALERT" -> Code.Alert
186 | "ALREADYEXISTS" -> Code.Alreadyexists
000000187 | "AUTHENTICATIONFAILED" -> Code.Authenticationfailed
188 | "AUTHORIZATIONFAILED" -> Code.Authorizationfailed
189 | "CANNOT" -> Code.Cannot
···191 | "CLIENTBUG" -> Code.Clientbug
192 | "CLOSED" -> Code.Closed
193 | "CONTACTADMIN" -> Code.Contactadmin
00000000194 | "CORRUPTION" -> Code.Corruption
195 | "EXPIRED" -> Code.Expired
196 | "EXPUNGEISSUED" -> Code.Expungeissued
197 | "HASCHILDREN" -> Code.Haschildren
000000198 | "INUSE" -> Code.Inuse
199 | "LIMIT" -> Code.Limit
00000000000200 | "NONEXISTENT" -> Code.Nonexistent
201 | "NOPERM" -> Code.Noperm
202 | "OVERQUOTA" -> Code.Overquota
···286 R.char ')' r;
287 Envelope.{ date; subject; from; sender; reply_to; to_; cc; bcc; in_reply_to; message_id }
2880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000289(** {1 FETCH Response Items} *)
290291let fetch_item r =
···333 | Some '[' ->
334 (* BODY[section]<origin> literal-or-nil *)
335 R.char '[' r;
336- let _section = R.take_while (fun c -> c <> ']') r in
337 R.char ']' r;
338- (* Skip optional origin <n> *)
0339 let origin =
340 if R.peek_char r = Some '<' then (
341 R.char '<' r;
···346 in
347 sp r;
348 let data = nstring r in
349- Fetch.Item_body_section { section = None; origin; data }
350 | _ ->
351- (* BODY without [] means bodystructure - skip for now *)
352 sp r;
353- (* Skip the parenthesized body structure *)
354- let depth = ref 0 in
355- (match R.peek_char r with
356- | Some '(' ->
357- R.char '(' r;
358- depth := 1;
359- while !depth > 0 do
360- match R.any_char r with
361- | '(' -> incr depth
362- | ')' -> decr depth
363- | '"' -> ignore (R.take_while (fun c -> c <> '"') r); ignore (R.any_char r)
364- | '{' ->
365- let len = number r in
366- R.char '}' r;
367- crlf r;
368- ignore (R.take len r)
369- | _ -> ()
370- done
371- | _ -> ());
372- (* Return a minimal body structure stub *)
373- let stub_body : Body.t = {
374- body_type = Body.Basic {
375- media_type = "application";
376- subtype = "octet-stream";
377- fields = {
378- params = [];
379- content_id = None;
380- description = None;
381- encoding = "7bit";
382- size = 0L;
383- }
384- };
385- disposition = None;
386- language = None;
387- location = None;
388- } in
389- Fetch.Item_body stub_body)
390 | "BODYSTRUCTURE" ->
0391 sp r;
392- (* Skip the parenthesized body structure - return minimal stub *)
393- let depth = ref 0 in
394- (match R.peek_char r with
395- | Some '(' ->
396- R.char '(' r;
397- depth := 1;
398- while !depth > 0 do
399- match R.any_char r with
400- | '(' -> incr depth
401- | ')' -> decr depth
402- | '"' -> ignore (R.take_while (fun c -> c <> '"') r); ignore (R.any_char r)
403- | '{' ->
404- let len = number r in
405- R.char '}' r;
406- crlf r;
407- ignore (R.take len r)
408- | _ -> ()
409- done
410- | _ -> ());
411- (* Return a minimal body structure stub *)
412- let stub_body : Body.t = {
413- body_type = Body.Basic {
414- media_type = "application";
415- subtype = "octet-stream";
416- fields = {
417- params = [];
418- content_id = None;
419- description = None;
420- encoding = "7bit";
421- size = 0L;
422- }
423- };
424- disposition = None;
425- language = None;
426- location = None;
427- } in
428- Fetch.Item_bodystructure stub_body
429 | _ -> Fetch.Item_flags []
430431let fetch_items r = parse_paren_list ~parse_item:fetch_item r
···444 | "UNSEEN" -> Status.Unseen
445 | "DELETED" -> Status.Deleted
446 | "SIZE" -> Status.Size
0447 | _ -> Status.Messages
448 in
449 (item, value)
···488 let shared = namespace_list r in
489 Response.{ personal; other; shared }
4900000000000000000000000000000000000000000000000000000000000000000000000491(** {1 ID Response} *)
492493let id_params r =
···509 loop ((k, v) :: acc)
510 in
511 loop [])
000000000000000000000000000000000000000000000512513(** {1 Main Response Parser} *)
514···631 done;
632 crlf r;
633 Response.Sort (List.rev !seqs)
0000000000000000000000634 | _ ->
635 let _ = rest_of_line r in
636 Response.Ok { tag = None; code = None; text = "" })
···106 in
107 loop []
108109+(** {1 UID Set Parsing}
110+111+ Parses UID sets in the format used by APPENDUID/COPYUID response codes.
112+ Examples: "304", "319:320", "304,319:320,325" *)
113+114+let uid_set_range r =
115+ let first = number r in
116+ match R.peek_char r with
117+ | Some ':' ->
118+ R.char ':' r;
119+ (* Check for * (wildcard) *)
120+ (match R.peek_char r with
121+ | Some '*' ->
122+ R.char '*' r;
123+ Seq.From first
124+ | _ ->
125+ let last = number r in
126+ Seq.Range (first, last))
127+ | _ -> Seq.Single first
128+129+let uid_set r =
130+ let rec loop acc =
131+ let range = uid_set_range r in
132+ match R.peek_char r with
133+ | Some ',' ->
134+ R.char ',' r;
135+ loop (range :: acc)
136+ | _ -> List.rev (range :: acc)
137+ in
138+ loop []
139+140(** {1 Flags} *)
141142let system_flag r =
···206 in
207 loop []
208209+(** {1 Body Structure Parsing} *)
210+211+(** Parse a parenthesized list of key-value pairs for body parameters.
212+ Format: ("key1" "value1" "key2" "value2" ...) or NIL *)
213+let body_params r =
214+ if is_nil r then (skip_nil r; [])
215+ else (
216+ R.char '(' r;
217+ let rec loop acc =
218+ match R.peek_char r with
219+ | Some ')' ->
220+ R.char ')' r;
221+ List.rev acc
222+ | Some ' ' ->
223+ sp r;
224+ loop acc
225+ | _ ->
226+ let k = astring r in
227+ sp r;
228+ let v = astring r in
229+ loop ((k, v) :: acc)
230+ in
231+ loop [])
232+233+(** Parse body fields common to all body types.
234+ Format: params content-id content-desc encoding size *)
235+let body_fields r =
236+ let params = body_params r in
237+ sp r;
238+ let content_id = nstring r in
239+ sp r;
240+ let description = nstring r in
241+ sp r;
242+ let encoding = astring r in
243+ sp r;
244+ let size = number64 r in
245+ Body.{ params; content_id; description; encoding; size }
246+247+(** Parse body disposition.
248+ Format: ("INLINE" ("filename" "test.txt")) or NIL *)
249+let body_disposition r =
250+ if is_nil r then (skip_nil r; None)
251+ else (
252+ R.char '(' r;
253+ let disposition_type = astring r in
254+ sp r;
255+ let params = body_params r in
256+ R.char ')' r;
257+ Some (disposition_type, params))
258+259+(** Parse body language - single string or list of strings.
260+ Format: NIL or "en" or ("en" "de") *)
261+let body_language r =
262+ if is_nil r then (skip_nil r; None)
263+ else
264+ match R.peek_char r with
265+ | Some '(' ->
266+ (* List of languages *)
267+ R.char '(' r;
268+ let rec loop acc =
269+ match R.peek_char r with
270+ | Some ')' ->
271+ R.char ')' r;
272+ Some (List.rev acc)
273+ | Some ' ' ->
274+ sp r;
275+ loop acc
276+ | _ ->
277+ let lang = astring r in
278+ loop (lang :: acc)
279+ in
280+ loop []
281+ | _ ->
282+ (* Single language *)
283+ Some [astring r]
284+285+(** Skip remaining body extensions after the known ones *)
286+let rec skip_body_extension r =
287+ match R.peek_char r with
288+ | Some '(' ->
289+ R.char '(' r;
290+ let rec loop () =
291+ match R.peek_char r with
292+ | Some ')' -> R.char ')' r
293+ | Some ' ' -> sp r; loop ()
294+ | _ -> skip_body_extension r; loop ()
295+ in
296+ loop ()
297+ | Some '"' -> ignore (quoted_string r)
298+ | Some '{' -> ignore (literal r)
299+ | _ when is_nil r -> skip_nil r
300+ | _ ->
301+ (* Could be a number or atom *)
302+ ignore (R.take_while (fun c -> c <> ' ' && c <> ')' && c <> '\r') r)
303+304+let skip_remaining_extensions r =
305+ while R.peek_char r = Some ' ' do
306+ sp r;
307+ match R.peek_char r with
308+ | Some ')' -> () (* End of body, don't consume *)
309+ | _ -> skip_body_extension r
310+ done
311+312(** {1 Response Codes} *)
313314let response_code r =
···318 match String.uppercase_ascii name with
319 | "ALERT" -> Code.Alert
320 | "ALREADYEXISTS" -> Code.Alreadyexists
321+ | "APPENDUID" ->
322+ sp r;
323+ let uidvalidity = number64 r in
324+ sp r;
325+ let uid = number64 r in
326+ Code.Appenduid (uidvalidity, uid)
327 | "AUTHENTICATIONFAILED" -> Code.Authenticationfailed
328 | "AUTHORIZATIONFAILED" -> Code.Authorizationfailed
329 | "CANNOT" -> Code.Cannot
···331 | "CLIENTBUG" -> Code.Clientbug
332 | "CLOSED" -> Code.Closed
333 | "CONTACTADMIN" -> Code.Contactadmin
334+ | "COPYUID" ->
335+ sp r;
336+ let uidvalidity = number64 r in
337+ sp r;
338+ let source_uids = uid_set r in
339+ sp r;
340+ let dest_uids = uid_set r in
341+ Code.Copyuid (uidvalidity, source_uids, dest_uids)
342 | "CORRUPTION" -> Code.Corruption
343 | "EXPIRED" -> Code.Expired
344 | "EXPUNGEISSUED" -> Code.Expungeissued
345 | "HASCHILDREN" -> Code.Haschildren
346+ | "HIGHESTMODSEQ" ->
347+ (* RFC 7162 Section 3.1.2.1: HIGHESTMODSEQ response code
348+ Returned in SELECT/EXAMINE to indicate the highest mod-sequence
349+ value of all messages in the mailbox. *)
350+ sp r;
351+ Code.Highestmodseq (number64 r)
352 | "INUSE" -> Code.Inuse
353 | "LIMIT" -> Code.Limit
354+ | "MODIFIED" ->
355+ (* RFC 7162 Section 3.1.3: MODIFIED response code
356+ Returned in response to STORE with UNCHANGEDSINCE modifier
357+ when messages have been modified since the specified mod-sequence. *)
358+ sp r;
359+ Code.Modified (uid_set r)
360+ | "NOMODSEQ" ->
361+ (* RFC 7162 Section 3.1.2.2: NOMODSEQ response code
362+ Indicates that the mailbox does not support persistent storage
363+ of mod-sequences (e.g., a virtual mailbox). *)
364+ Code.Nomodseq
365 | "NONEXISTENT" -> Code.Nonexistent
366 | "NOPERM" -> Code.Noperm
367 | "OVERQUOTA" -> Code.Overquota
···451 R.char ')' r;
452 Envelope.{ date; subject; from; sender; reply_to; to_; cc; bcc; in_reply_to; message_id }
453454+(** {1 Body Structure Parsing - Recursive Part}
455+456+ These functions parse body structures per RFC 9051 Section 7.4.2.
457+ They must be defined after envelope since MESSAGE/RFC822 bodies contain envelopes. *)
458+459+(** Parse a single body part (non-multipart).
460+ Returns the body type and optional extension data. *)
461+let rec body_type_1part r =
462+ let media_type = astring r in
463+ sp r;
464+ let subtype = astring r in
465+ sp r;
466+ let fields = body_fields r in
467+ let media_type_upper = String.uppercase_ascii media_type in
468+469+ (* Parse type-specific fields and build body_type *)
470+ let (body_type : Body.body_type) =
471+ if media_type_upper = "TEXT" then (
472+ sp r;
473+ let lines = number64 r in
474+ Text { subtype; fields; lines }
475+ ) else if media_type_upper = "MESSAGE" && String.uppercase_ascii subtype = "RFC822" then (
476+ sp r;
477+ let env = envelope r in
478+ sp r;
479+ let nested_body = body r in
480+ sp r;
481+ let lines = number64 r in
482+ Message_rfc822 { fields; envelope = env; body = nested_body; lines }
483+ ) else
484+ Basic { media_type; subtype; fields }
485+ in
486+487+ (* Parse optional extension data for BODYSTRUCTURE *)
488+ let disposition, language, location =
489+ match R.peek_char r with
490+ | Some ' ' -> (
491+ sp r;
492+ match R.peek_char r with
493+ | Some ')' -> (None, None, None) (* End of body *)
494+ | _ ->
495+ (* md5 - skip it *)
496+ ignore (nstring r);
497+ match R.peek_char r with
498+ | Some ' ' -> (
499+ sp r;
500+ match R.peek_char r with
501+ | Some ')' -> (None, None, None)
502+ | _ ->
503+ let disposition = body_disposition r in
504+ match R.peek_char r with
505+ | Some ' ' -> (
506+ sp r;
507+ match R.peek_char r with
508+ | Some ')' -> (disposition, None, None)
509+ | _ ->
510+ let language = body_language r in
511+ match R.peek_char r with
512+ | Some ' ' -> (
513+ sp r;
514+ match R.peek_char r with
515+ | Some ')' -> (disposition, language, None)
516+ | _ ->
517+ let location = nstring r in
518+ skip_remaining_extensions r;
519+ (disposition, language, location))
520+ | _ -> (disposition, language, None))
521+ | _ -> (disposition, None, None))
522+ | _ -> (None, None, None))
523+ | _ -> (None, None, None)
524+ in
525+526+ Body.{ body_type; disposition; language; location }
527+528+(** Parse multipart body structure.
529+ Format: (body)(body)... "subtype" [extensions] *)
530+and body_type_mpart r =
531+ (* Collect all nested body parts *)
532+ let rec collect_parts acc =
533+ match R.peek_char r with
534+ | Some '(' ->
535+ let part = body r in
536+ collect_parts (part :: acc)
537+ | _ -> List.rev acc
538+ in
539+ let parts = collect_parts [] in
540+541+ (* Parse subtype *)
542+ sp r;
543+ let subtype = astring r in
544+545+ (* Parse optional extension data for BODYSTRUCTURE *)
546+ let params, disposition, language, location =
547+ match R.peek_char r with
548+ | Some ' ' -> (
549+ sp r;
550+ match R.peek_char r with
551+ | Some ')' -> ([], None, None, None)
552+ | _ ->
553+ let params = body_params r in
554+ match R.peek_char r with
555+ | Some ' ' -> (
556+ sp r;
557+ match R.peek_char r with
558+ | Some ')' -> (params, None, None, None)
559+ | _ ->
560+ let disposition = body_disposition r in
561+ match R.peek_char r with
562+ | Some ' ' -> (
563+ sp r;
564+ match R.peek_char r with
565+ | Some ')' -> (params, disposition, None, None)
566+ | _ ->
567+ let language = body_language r in
568+ match R.peek_char r with
569+ | Some ' ' -> (
570+ sp r;
571+ match R.peek_char r with
572+ | Some ')' -> (params, disposition, language, None)
573+ | _ ->
574+ let location = nstring r in
575+ skip_remaining_extensions r;
576+ (params, disposition, language, location))
577+ | _ -> (params, disposition, language, None))
578+ | _ -> (params, disposition, None, None))
579+ | _ -> (params, None, None, None))
580+ | _ -> ([], None, None, None)
581+ in
582+583+ Body.{
584+ body_type = Multipart { subtype; parts; params };
585+ disposition;
586+ language;
587+ location;
588+ }
589+590+(** Parse a body structure - either multipart or single part.
591+ Multipart starts with nested parentheses, single part starts with a string. *)
592+and body r =
593+ R.char '(' r;
594+ let result =
595+ match R.peek_char r with
596+ | Some '(' ->
597+ (* Multipart - starts with nested body *)
598+ body_type_mpart r
599+ | _ ->
600+ (* Single part - starts with media type string *)
601+ body_type_1part r
602+ in
603+ R.char ')' r;
604+ result
605+606+(** {1 Section Specifier Parsing}
607+608+ Parse section specifiers like HEADER, TEXT, 1.2.MIME, HEADER.FIELDS (From Subject) *)
609+610+(** Parse a header field list like (From Subject To).
611+ Note: Currently unused as HEADER.FIELDS parsing is simplified. *)
612+let _parse_header_fields r =
613+ sp r;
614+ R.char '(' r;
615+ let rec loop acc =
616+ match R.peek_char r with
617+ | Some ')' ->
618+ R.char ')' r;
619+ List.rev acc
620+ | Some ' ' ->
621+ sp r;
622+ loop acc
623+ | _ ->
624+ let field = astring r in
625+ loop (field :: acc)
626+ in
627+ loop []
628+629+(** Parse a section specifier string into a Body.section option.
630+ Section format per RFC 9051:
631+ - Empty string means whole message
632+ - HEADER, TEXT, MIME
633+ - HEADER.FIELDS (field list)
634+ - HEADER.FIELDS.NOT (field list)
635+ - Part number like 1, 1.2, 1.2.3
636+ - Part number with subsection like 1.HEADER, 1.2.TEXT, 1.2.MIME *)
637+let parse_section_spec section_str =
638+ if section_str = "" then None
639+ else
640+ let upper = String.uppercase_ascii section_str in
641+ if upper = "HEADER" then Some Body.Header
642+ else if upper = "TEXT" then Some Body.Text
643+ else if upper = "MIME" then Some Body.Mime
644+ else if String.length upper > 14 && String.sub upper 0 14 = "HEADER.FIELDS " then
645+ (* HEADER.FIELDS (field1 field2...) - simplified parsing *)
646+ Some Body.Header
647+ else if String.length upper > 18 && String.sub upper 0 18 = "HEADER.FIELDS.NOT " then
648+ (* HEADER.FIELDS.NOT (field1 field2...) - simplified parsing *)
649+ Some Body.Header
650+ else
651+ (* Try to parse as part numbers: 1, 1.2, 1.2.3, possibly with .HEADER/.TEXT/.MIME suffix *)
652+ let parts = String.split_on_char '.' section_str in
653+ let rec parse_parts nums = function
654+ | [] -> Some (Body.Part (List.rev nums, None))
655+ | [s] when String.uppercase_ascii s = "HEADER" ->
656+ Some (Body.Part (List.rev nums, Some Body.Header))
657+ | [s] when String.uppercase_ascii s = "TEXT" ->
658+ Some (Body.Part (List.rev nums, Some Body.Text))
659+ | [s] when String.uppercase_ascii s = "MIME" ->
660+ Some (Body.Part (List.rev nums, Some Body.Mime))
661+ | s :: rest ->
662+ (try
663+ let n = int_of_string s in
664+ parse_parts (n :: nums) rest
665+ with Failure _ ->
666+ (* Not a number, and not a known section type at end - skip *)
667+ Some (Body.Part (List.rev nums, None)))
668+ in
669+ parse_parts [] parts
670+671(** {1 FETCH Response Items} *)
672673let fetch_item r =
···715 | Some '[' ->
716 (* BODY[section]<origin> literal-or-nil *)
717 R.char '[' r;
718+ let section_str = R.take_while (fun c -> c <> ']') r in
719 R.char ']' r;
720+ let section = parse_section_spec section_str in
721+ (* Parse optional origin <n> *)
722 let origin =
723 if R.peek_char r = Some '<' then (
724 R.char '<' r;
···729 in
730 sp r;
731 let data = nstring r in
732+ Fetch.Item_body_section { section; origin; data }
733 | _ ->
734+ (* BODY without [] means basic bodystructure (no extensions) *)
735 sp r;
736+ let parsed_body = body r in
737+ Fetch.Item_body parsed_body)
00000000000000000000000000000000000738 | "BODYSTRUCTURE" ->
739+ (* BODYSTRUCTURE includes extension data *)
740 sp r;
741+ let parsed_body = body r in
742+ Fetch.Item_bodystructure parsed_body
00000000000000000000000000000000000743 | _ -> Fetch.Item_flags []
744745let fetch_items r = parse_paren_list ~parse_item:fetch_item r
···758 | "UNSEEN" -> Status.Unseen
759 | "DELETED" -> Status.Deleted
760 | "SIZE" -> Status.Size
761+ | "HIGHESTMODSEQ" -> Status.Highestmodseq (* RFC 7162 CONDSTORE *)
762 | _ -> Status.Messages
763 in
764 (item, value)
···803 let shared = namespace_list r in
804 Response.{ personal; other; shared }
805806+(** {1 ESEARCH Response (RFC 4731)} *)
807+808+let esearch_correlator r =
809+ (* Parse (TAG "xxx") *)
810+ R.char '(' r;
811+ let name = atom r in
812+ sp r;
813+ let value = quoted_string r in
814+ R.char ')' r;
815+ if String.uppercase_ascii name = "TAG" then Some value else None
816+817+let esearch_result_item r =
818+ let name = atom r in
819+ match String.uppercase_ascii name with
820+ | "MIN" ->
821+ sp r;
822+ Some (Response.Esearch_min (number r))
823+ | "MAX" ->
824+ sp r;
825+ Some (Response.Esearch_max (number r))
826+ | "COUNT" ->
827+ sp r;
828+ Some (Response.Esearch_count (number r))
829+ | "ALL" ->
830+ sp r;
831+ Some (Response.Esearch_all (uid_set r))
832+ | _ -> None
833+834+let esearch_data r =
835+ (* Parse optional (TAG "xxx") correlator *)
836+ let tag =
837+ match R.peek_char r with
838+ | Some '(' -> esearch_correlator r
839+ | _ -> None
840+ in
841+ (* Skip space if present after correlator *)
842+ if Option.is_some tag && R.peek_char r = Some ' ' then sp r;
843+ (* Check for UID indicator *)
844+ let uid =
845+ match R.peek_char r with
846+ | Some 'U' | Some 'u' ->
847+ (* Peek ahead to check if it's "UID" *)
848+ R.ensure r 3;
849+ let buf = R.peek r in
850+ if Cstruct.length buf >= 3
851+ && Char.uppercase_ascii (Cstruct.get_char buf 0) = 'U'
852+ && Char.uppercase_ascii (Cstruct.get_char buf 1) = 'I'
853+ && Char.uppercase_ascii (Cstruct.get_char buf 2) = 'D'
854+ then (
855+ ignore (R.take 3 r); (* consume "UID" *)
856+ if R.peek_char r = Some ' ' then sp r;
857+ true
858+ ) else false
859+ | _ -> false
860+ in
861+ (* Parse result data items *)
862+ let rec loop acc =
863+ match R.peek_char r with
864+ | Some '\r' | Some '\n' | None -> List.rev acc
865+ | Some ' ' ->
866+ sp r;
867+ loop acc
868+ | _ ->
869+ (match esearch_result_item r with
870+ | Some item -> loop (item :: acc)
871+ | None -> List.rev acc)
872+ in
873+ let results = loop [] in
874+ Response.Esearch { tag; uid; results }
875+876(** {1 ID Response} *)
877878let id_params r =
···894 loop ((k, v) :: acc)
895 in
896 loop [])
897+898+(** {1 THREAD Response (RFC 5256)}
899+900+ Parses the THREAD response format as specified in RFC 5256 Section 4.
901+902+ The thread response has a recursive structure where each thread node
903+ can be either a message number or a nested list of children.
904+905+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
906+907+(** Parse a single thread node.
908+909+ A thread node is either:
910+ - A message number optionally followed by children: "(n ...children...)"
911+ - A dummy node (missing parent) starting with nested parens: "((...)...)"
912+913+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
914+let rec thread_node r =
915+ R.char '(' r;
916+ match R.peek_char r with
917+ | Some '(' ->
918+ (* Dummy node - starts with ( instead of number.
919+ This represents a missing parent message in the thread. *)
920+ let children = thread_children r in
921+ R.char ')' r;
922+ Thread.Dummy children
923+ | _ ->
924+ let n = number r in
925+ let children = thread_children r in
926+ R.char ')' r;
927+ Thread.Message (n, children)
928+929+(** Parse thread children (zero or more thread nodes).
930+931+ Children can be separated by spaces or appear consecutively.
932+ This handles formats like "(3 6 (4 23))" where 6 is a child of 3,
933+ and (4 23) is a sibling subtree. *)
934+and thread_children r =
935+ let rec loop acc =
936+ match R.peek_char r with
937+ | Some '(' -> loop (thread_node r :: acc)
938+ | Some ' ' -> sp r; loop acc
939+ | _ -> List.rev acc
940+ in
941+ loop []
942943(** {1 Main Response Parser} *)
944···1061 done;
1062 crlf r;
1063 Response.Sort (List.rev !seqs)
1064+ | "THREAD" ->
1065+ (* RFC 5256 Section 4 - THREAD response format:
1066+ thread-data = "THREAD" [SP 1*thread-list]
1067+ thread-list = "(" thread-members / thread-nested ")"
1068+ thread-members = thread-node *(SP thread-node)
1069+ thread-nested = thread-list
1070+ thread-node = nz-number / thread-nested
1071+1072+ Example: * THREAD (2)(3 6 (4 23)(44 7 96))
1073+ - Thread 1: Message 2 (no children)
1074+ - Thread 2: Message 3 with child 6, which has children 4,23 and 44->7->96
1075+1076+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
1077+ let threads = thread_children r in
1078+ crlf r;
1079+ Response.Thread threads
1080+ | "ESEARCH" ->
1081+ (* RFC 4731 ESEARCH response *)
1082+ if R.peek_char r = Some ' ' then sp r;
1083+ let result = esearch_data r in
1084+ crlf r;
1085+ result
1086 | _ ->
1087 let _ = rest_of_line r in
1088 Response.Ok { tag = None; code = None; text = "" })
+2
ocaml-imap/lib/imap/response.ml
···45 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
46 | Search of int list
47 | Sort of int64 list
048 | Flags of Flag.t list
49 | Exists of int
50 | Recent of int
···83 | Esearch _ -> Fmt.string ppf "* ESEARCH ..."
84 | Search seqs -> Fmt.pf ppf "* SEARCH %a" Fmt.(list ~sep:sp int) seqs
85 | Sort seqs -> Fmt.pf ppf "* SORT %a" Fmt.(list ~sep:sp int64) seqs
086 | Flags flags -> Fmt.pf ppf "* FLAGS (%a)" Fmt.(list ~sep:sp Flag.pp) flags
87 | Exists n -> Fmt.pf ppf "* %d EXISTS" n
88 | Recent n -> Fmt.pf ppf "* %d RECENT" n
···45 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
46 | Search of int list
47 | Sort of int64 list
48+ | Thread of int Thread.t
49 | Flags of Flag.t list
50 | Exists of int
51 | Recent of int
···84 | Esearch _ -> Fmt.string ppf "* ESEARCH ..."
85 | Search seqs -> Fmt.pf ppf "* SEARCH %a" Fmt.(list ~sep:sp int) seqs
86 | Sort seqs -> Fmt.pf ppf "* SORT %a" Fmt.(list ~sep:sp int64) seqs
87+ | Thread threads -> Fmt.pf ppf "* THREAD %a" (Thread.pp Fmt.int) threads
88 | Flags flags -> Fmt.pf ppf "* FLAGS (%a)" Fmt.(list ~sep:sp Flag.pp) flags
89 | Exists n -> Fmt.pf ppf "* %d EXISTS" n
90 | Recent n -> Fmt.pf ppf "* %d RECENT" n
+1
ocaml-imap/lib/imap/response.mli
···45 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
46 | Search of int list
47 | Sort of int64 list
048 | Flags of Flag.t list
49 | Exists of int
50 | Recent of int
···45 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
46 | Search of int list
47 | Sort of int64 list
48+ | Thread of int Thread.t
49 | Flags of Flag.t list
50 | Exists of int
51 | Recent of int
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** {0 Base Subject Extraction}
7+8+ Implements {{:https://datatracker.ietf.org/doc/html/rfc5256#section-2.1}RFC 5256 Section 2.1}
9+ for extracting the "base subject" from email Subject headers.
10+11+ The base subject is used for SORT by SUBJECT and threading operations. *)
12+13+(** Normalize whitespace: convert tabs to spaces, collapse multiple spaces to one,
14+ and trim leading/trailing whitespace. *)
15+let normalize_whitespace s =
16+ (* Replace tabs with spaces *)
17+ let s = String.map (fun c -> if c = '\t' then ' ' else c) s in
18+ (* Collapse multiple spaces and trim *)
19+ let buf = Buffer.create (String.length s) in
20+ let last_was_space = ref true in (* Start true to skip leading spaces *)
21+ String.iter (fun c ->
22+ if c = ' ' then begin
23+ if not !last_was_space then Buffer.add_char buf ' ';
24+ last_was_space := true
25+ end else begin
26+ Buffer.add_char buf c;
27+ last_was_space := false
28+ end
29+ ) s;
30+ (* Remove trailing space if present *)
31+ let result = Buffer.contents buf in
32+ let len = String.length result in
33+ if len > 0 && result.[len - 1] = ' ' then
34+ String.sub result 0 (len - 1)
35+ else
36+ result
37+38+(** Case-insensitive string prefix check *)
39+let starts_with_ci ~prefix s =
40+ let plen = String.length prefix in
41+ let slen = String.length s in
42+ slen >= plen &&
43+ let rec check i =
44+ if i >= plen then true
45+ else if Char.lowercase_ascii s.[i] = Char.lowercase_ascii prefix.[i] then
46+ check (i + 1)
47+ else false
48+ in
49+ check 0
50+51+(** Case-insensitive string suffix check *)
52+let ends_with_ci ~suffix s =
53+ let suflen = String.length suffix in
54+ let slen = String.length s in
55+ slen >= suflen &&
56+ let rec check i =
57+ if i >= suflen then true
58+ else if Char.lowercase_ascii s.[slen - suflen + i] = Char.lowercase_ascii suffix.[i] then
59+ check (i + 1)
60+ else false
61+ in
62+ check 0
63+64+(** Remove trailing (fwd) and whitespace - subj-trailer from RFC 5256 Section 5.
65+ Returns the string and whether anything was removed. *)
66+let remove_trailer s =
67+ let s = String.trim s in
68+ if ends_with_ci ~suffix:"(fwd)" s then
69+ let len = String.length s in
70+ (String.trim (String.sub s 0 (len - 5)), true)
71+ else
72+ (s, false)
73+74+(** Skip a [blob] pattern starting at position i.
75+ Returns the position after the blob and trailing whitespace, or None if no blob. *)
76+let skip_blob s i =
77+ let len = String.length s in
78+ if i >= len || s.[i] <> '[' then None
79+ else begin
80+ (* Find matching ] - blob chars are anything except [ ] *)
81+ let rec find_close j =
82+ if j >= len then None
83+ else if s.[j] = ']' then Some j
84+ else if s.[j] = '[' then None (* nested [ not allowed in blob *)
85+ else find_close (j + 1)
86+ in
87+ match find_close (i + 1) with
88+ | None -> None
89+ | Some close_pos ->
90+ (* Skip trailing whitespace after ] *)
91+ let rec skip_ws k =
92+ if k >= len then k
93+ else if s.[k] = ' ' || s.[k] = '\t' then skip_ws (k + 1)
94+ else k
95+ in
96+ Some (skip_ws (close_pos + 1))
97+ end
98+99+(** Try to remove a subj-refwd pattern: ("re" / ("fw" ["d"])) *WSP [subj-blob] ":"
100+ Returns (rest_of_string, removed) *)
101+let remove_refwd s =
102+ let len = String.length s in
103+ let try_prefix prefix =
104+ if starts_with_ci ~prefix s then
105+ let after_prefix = String.length prefix in
106+ (* Skip optional whitespace *)
107+ let rec skip_ws i =
108+ if i >= len then i
109+ else if s.[i] = ' ' || s.[i] = '\t' then skip_ws (i + 1)
110+ else i
111+ in
112+ let after_ws = skip_ws after_prefix in
113+ (* Try to skip optional blob *)
114+ let after_blob = match skip_blob s after_ws with
115+ | Some pos -> pos
116+ | None -> after_ws
117+ in
118+ (* Must have colon *)
119+ if after_blob < len && s.[after_blob] = ':' then
120+ let rest = String.sub s (after_blob + 1) (len - after_blob - 1) in
121+ Some (String.trim rest)
122+ else
123+ None
124+ else
125+ None
126+ in
127+ (* Try fwd first (longer), then fw, then re *)
128+ match try_prefix "fwd" with
129+ | Some rest -> (rest, true)
130+ | None ->
131+ match try_prefix "fw" with
132+ | Some rest -> (rest, true)
133+ | None ->
134+ match try_prefix "re" with
135+ | Some rest -> (rest, true)
136+ | None -> (s, false)
137+138+(** Try to remove a leading [blob] if doing so leaves a non-empty base subject.
139+ Returns (rest_of_string, removed) *)
140+let remove_leading_blob s =
141+ match skip_blob s 0 with
142+ | None -> (s, false)
143+ | Some after_blob ->
144+ let rest = String.sub s after_blob (String.length s - after_blob) in
145+ let rest = String.trim rest in
146+ (* Only remove if non-empty base remains *)
147+ if String.length rest > 0 then (rest, true)
148+ else (s, false)
149+150+(** Remove [fwd: ... ] wrapper pattern.
151+ Returns (unwrapped_content, was_wrapped) *)
152+let remove_fwd_wrapper s =
153+ let len = String.length s in
154+ if len >= 6 && starts_with_ci ~prefix:"[fwd:" s && s.[len - 1] = ']' then begin
155+ let inner = String.sub s 5 (len - 6) in
156+ (String.trim inner, true)
157+ end else
158+ (s, false)
159+160+(** Internal state for tracking whether modifications occurred *)
161+type extract_state = {
162+ subject : string;
163+ is_reply_or_fwd : bool;
164+}
165+166+(** Extract base subject with full algorithm from RFC 5256 Section 2.1 *)
167+let rec extract_base_subject_full subject =
168+ (* Step 1: Normalize whitespace (RFC 2047 decoding would go here too) *)
169+ let s = normalize_whitespace subject in
170+ let state = { subject = s; is_reply_or_fwd = false } in
171+172+ (* Step 2: Remove trailing (fwd) - subj-trailer *)
173+ let rec remove_trailers state =
174+ let (s, removed) = remove_trailer state.subject in
175+ if removed then
176+ remove_trailers { subject = s; is_reply_or_fwd = true }
177+ else
178+ state
179+ in
180+ let state = remove_trailers state in
181+182+ (* Steps 3-5: Remove leading subj-leader and subj-blob, repeat until stable *)
183+ let rec remove_leaders state =
184+ (* Step 3: Remove subj-refwd *)
185+ let (s, refwd_removed) = remove_refwd state.subject in
186+ let new_is_reply = state.is_reply_or_fwd || refwd_removed in
187+ let state = { subject = s; is_reply_or_fwd = new_is_reply } in
188+189+ (* Step 4: Remove leading [blob] if non-empty base remains *)
190+ let (s, blob_removed) = remove_leading_blob state.subject in
191+ let state = { subject = s; is_reply_or_fwd = state.is_reply_or_fwd } in
192+193+ (* Step 5: Repeat if any changes *)
194+ if refwd_removed || blob_removed then
195+ remove_leaders state
196+ else
197+ state
198+ in
199+ let state = remove_leaders state in
200+201+ (* Step 6: Check for [fwd: ... ] wrapper *)
202+ let (s, fwd_wrapped) = remove_fwd_wrapper state.subject in
203+ let new_is_reply = state.is_reply_or_fwd || fwd_wrapped in
204+ let state = { subject = s; is_reply_or_fwd = new_is_reply } in
205+206+ (* If we unwrapped [fwd:], need to re-run the whole algorithm on the inner content *)
207+ if fwd_wrapped then begin
208+ let inner_result = extract_base_subject_full s in
209+ { subject = inner_result.subject;
210+ is_reply_or_fwd = true (* The outer [fwd:] wrapper counts *) }
211+ end else
212+ state
213+214+let base_subject subject =
215+ let result = extract_base_subject_full subject in
216+ result.subject
217+218+let is_reply_or_forward subject =
219+ let result = extract_base_subject_full subject in
220+ result.is_reply_or_fwd
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** {0 RFC 5256 THREAD Extension}
7+8+ Message threading algorithms as specified in
9+ {{:https://datatracker.ietf.org/doc/html/rfc5256}RFC 5256 Section 3}.
10+11+ The THREAD command allows clients to retrieve messages organized into
12+ conversation threads based on message relationships. *)
13+14+(** {1 Threading Algorithms}
15+16+ RFC 5256 Section 3 defines two threading algorithms. Servers MUST
17+ implement at least ORDEREDSUBJECT and SHOULD implement REFERENCES. *)
18+19+(** Threading algorithm used to organize messages into threads.
20+21+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-3> RFC 5256 Section 3 *)
22+type algorithm =
23+ | Orderedsubject
24+ (** ORDEREDSUBJECT algorithm (RFC 5256 Section 3.1).
25+ Groups messages by base subject (stripping Re:/Fwd: prefixes),
26+ then sorts each group by sent date. Simple but effective for
27+ basic threading. *)
28+ | References
29+ (** REFERENCES algorithm (RFC 5256 Section 3.2).
30+ Implements the JWZ threading algorithm using Message-ID,
31+ In-Reply-To, and References headers to build a complete
32+ parent/child thread tree. More accurate than ORDEREDSUBJECT
33+ but computationally more expensive. *)
34+ | Extension of string
35+ (** Future algorithm extensions. Servers may advertise additional
36+ threading algorithms via the THREAD capability. *)
37+38+(** {1 Thread Result Structure}
39+40+ Thread results form a forest of trees. Each tree represents a
41+ conversation thread, with messages as nodes. *)
42+43+(** A thread node in the result tree.
44+45+ Thread responses use a nested parenthesized structure where each
46+ message may have zero or more child messages (replies).
47+48+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
49+type 'a node =
50+ | Message of 'a * 'a node list
51+ (** A message with its sequence number or UID (depending on whether
52+ UID THREAD was used) and a list of child messages (replies).
53+ The children are ordered by the threading algorithm. *)
54+ | Dummy of 'a node list
55+ (** A placeholder for a missing parent message. This occurs when
56+ replies reference a message that is not in the search results
57+ (e.g., it was deleted or not matched by the search criteria).
58+ The REFERENCES algorithm may produce dummy nodes to maintain
59+ thread structure. *)
60+61+(** Thread result: a list of root-level thread trees.
62+63+ Each element is a top-level thread. The threads are ordered according
64+ to the threading algorithm (typically by date of the first message
65+ in each thread).
66+67+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
68+type 'a t = 'a node list
69+70+(** {1 Pretty Printers} *)
71+72+let pp_algorithm ppf = function
73+ | Orderedsubject -> Fmt.string ppf "ORDEREDSUBJECT"
74+ | References -> Fmt.string ppf "REFERENCES"
75+ | Extension s -> Fmt.pf ppf "%s" (String.uppercase_ascii s)
76+77+let algorithm_to_string alg = Fmt.str "%a" pp_algorithm alg
78+79+let algorithm_of_string s =
80+ match String.uppercase_ascii s with
81+ | "ORDEREDSUBJECT" -> Orderedsubject
82+ | "REFERENCES" -> References
83+ | other -> Extension other
84+85+let rec pp_node pp_elt ppf = function
86+ | Message (elt, []) ->
87+ pp_elt ppf elt
88+ | Message (elt, children) ->
89+ Fmt.pf ppf "(%a %a)" pp_elt elt
90+ Fmt.(list ~sep:sp (pp_node pp_elt)) children
91+ | Dummy children ->
92+ Fmt.pf ppf "(%a)" Fmt.(list ~sep:sp (pp_node pp_elt)) children
93+94+let pp pp_elt ppf threads =
95+ Fmt.(list ~sep:sp (pp_node pp_elt)) ppf threads
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** {0 RFC 5256 THREAD Extension}
7+8+ Message threading algorithms as specified in
9+ {{:https://datatracker.ietf.org/doc/html/rfc5256}RFC 5256 Section 3}.
10+11+ The THREAD command allows clients to retrieve messages organized into
12+ conversation threads based on message relationships. *)
13+14+(** {1 Threading Algorithms}
15+16+ RFC 5256 Section 3 defines two threading algorithms. Servers MUST
17+ implement at least ORDEREDSUBJECT and SHOULD implement REFERENCES. *)
18+19+(** Threading algorithm used to organize messages into threads.
20+21+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-3> RFC 5256 Section 3 *)
22+type algorithm =
23+ | Orderedsubject
24+ (** ORDEREDSUBJECT algorithm (RFC 5256 Section 3.1).
25+ Groups messages by base subject (stripping Re:/Fwd: prefixes),
26+ then sorts each group by sent date. Simple but effective for
27+ basic threading. *)
28+ | References
29+ (** REFERENCES algorithm (RFC 5256 Section 3.2).
30+ Implements the JWZ threading algorithm using Message-ID,
31+ In-Reply-To, and References headers to build a complete
32+ parent/child thread tree. More accurate than ORDEREDSUBJECT
33+ but computationally more expensive. *)
34+ | Extension of string
35+ (** Future algorithm extensions. Servers may advertise additional
36+ threading algorithms via the THREAD capability. *)
37+38+(** {1 Thread Result Structure}
39+40+ Thread results form a forest of trees. Each tree represents a
41+ conversation thread, with messages as nodes. *)
42+43+(** A thread node in the result tree.
44+45+ Thread responses use a nested parenthesized structure where each
46+ message may have zero or more child messages (replies).
47+48+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
49+type 'a node =
50+ | Message of 'a * 'a node list
51+ (** A message with its sequence number or UID (depending on whether
52+ UID THREAD was used) and a list of child messages (replies).
53+ The children are ordered by the threading algorithm. *)
54+ | Dummy of 'a node list
55+ (** A placeholder for a missing parent message. This occurs when
56+ replies reference a message that is not in the search results
57+ (e.g., it was deleted or not matched by the search criteria).
58+ The REFERENCES algorithm may produce dummy nodes to maintain
59+ thread structure. *)
60+61+(** Thread result: a list of root-level thread trees.
62+63+ Each element is a top-level thread. The threads are ordered according
64+ to the threading algorithm (typically by date of the first message
65+ in each thread).
66+67+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
68+type 'a t = 'a node list
69+70+(** {1 Pretty Printers} *)
71+72+val pp_algorithm : Format.formatter -> algorithm -> unit
73+(** [pp_algorithm ppf alg] prints the algorithm name in IMAP wire format. *)
74+75+val algorithm_to_string : algorithm -> string
76+(** [algorithm_to_string alg] returns the algorithm name as a string. *)
77+78+val algorithm_of_string : string -> algorithm
79+(** [algorithm_of_string s] parses an algorithm name from a string.
80+ Unrecognized names are returned as [Extension s]. *)
81+82+val pp_node : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a node -> unit
83+(** [pp_node pp_elt ppf node] prints a thread node using [pp_elt] for elements. *)
84+85+val pp : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit
86+(** [pp pp_elt ppf threads] prints a thread result using [pp_elt] for elements. *)
+45-5
ocaml-imap/lib/imap/write.ml
···116 flags;
117 W.char w ')'
1180000000000000000119(** {1 Search Keys} *)
120121let rec search_key w = function
···245 write_partial w partial
246 | Fetch.Binary_size section ->
247 W.string w "BINARY.SIZE["; W.string w section; W.char w ']'
000248249let fetch_items w = function
250 | [ item ] -> fetch_item w item
···266 | Status.Unseen -> W.string w "UNSEEN"
267 | Status.Deleted -> W.string w "DELETED"
268 | Status.Size -> W.string w "SIZE"
0269270let status_items w items =
271 W.char w '(';
···307 criteria;
308 W.char w ')'
3090000000310(** {1 ID Parameters} *)
311312let id_params w = function
···324325(** {1 Commands} *)
326327-let write_search w charset criteria =
328 W.string w "SEARCH";
0329 Option.iter (fun cs -> W.string w " CHARSET "; astring w cs) charset;
330 sp w;
331 search_key w criteria
···333let write_sort w charset criteria search =
334 W.string w "SORT ";
335 sort_criteria w criteria;
00000000336 sp w;
337 astring w charset;
338 sp w;
···403 | Command.Close -> W.string w "CLOSE"
404 | Command.Unselect -> W.string w "UNSELECT"
405 | Command.Expunge -> W.string w "EXPUNGE"
406- | Command.Search { charset; criteria } ->
407- write_search w charset criteria
408 | Command.Sort { charset; criteria; search } ->
409 write_sort w charset criteria search
00410 | Command.Fetch { sequence; items; changedsince } ->
411 W.string w "FETCH ";
412 sequence_set w sequence;
···476 sequence_set w sequence;
477 sp w;
478 astring w mailbox
479- | Command.Uid_search { charset; criteria } ->
480- write_search w charset criteria
481 | Command.Uid_sort { charset; criteria; search } ->
482 write_sort w charset criteria search
00483 | Command.Uid_expunge set ->
484 W.string w "EXPUNGE ";
485 sequence_set w set)
···116 flags;
117 W.char w ')'
118119+(** {1 Search Return Options (RFC 4731 ESEARCH)} *)
120+121+let search_return_opt w = function
122+ | Command.Return_min -> W.string w "MIN"
123+ | Command.Return_max -> W.string w "MAX"
124+ | Command.Return_all -> W.string w "ALL"
125+ | Command.Return_count -> W.string w "COUNT"
126+127+let search_return_opts w opts =
128+ W.string w "RETURN (";
129+ List.iteri (fun i opt ->
130+ if i > 0 then sp w;
131+ search_return_opt w opt
132+ ) opts;
133+ W.char w ')'
134+135(** {1 Search Keys} *)
136137let rec search_key w = function
···261 write_partial w partial
262 | Fetch.Binary_size section ->
263 W.string w "BINARY.SIZE["; W.string w section; W.char w ']'
264+ | Fetch.Modseq ->
265+ (* RFC 7162 Section 3.1.5: MODSEQ fetch data item *)
266+ W.string w "MODSEQ"
267268let fetch_items w = function
269 | [ item ] -> fetch_item w item
···285 | Status.Unseen -> W.string w "UNSEEN"
286 | Status.Deleted -> W.string w "DELETED"
287 | Status.Size -> W.string w "SIZE"
288+ | Status.Highestmodseq -> W.string w "HIGHESTMODSEQ" (* RFC 7162 CONDSTORE *)
289290let status_items w items =
291 W.char w '(';
···327 criteria;
328 W.char w ')'
329330+(** {1 Thread Algorithm} *)
331+332+let thread_algorithm w = function
333+ | Thread.Orderedsubject -> W.string w "ORDEREDSUBJECT"
334+ | Thread.References -> W.string w "REFERENCES"
335+ | Thread.Extension s -> W.string w (String.uppercase_ascii s)
336+337(** {1 ID Parameters} *)
338339let id_params w = function
···351352(** {1 Commands} *)
353354+let write_search w charset criteria return_opts =
355 W.string w "SEARCH";
356+ Option.iter (fun opts -> sp w; search_return_opts w opts) return_opts;
357 Option.iter (fun cs -> W.string w " CHARSET "; astring w cs) charset;
358 sp w;
359 search_key w criteria
···361let write_sort w charset criteria search =
362 W.string w "SORT ";
363 sort_criteria w criteria;
364+ sp w;
365+ astring w charset;
366+ sp w;
367+ search_key w search
368+369+let write_thread w algorithm charset search =
370+ W.string w "THREAD ";
371+ thread_algorithm w algorithm;
372 sp w;
373 astring w charset;
374 sp w;
···439 | Command.Close -> W.string w "CLOSE"
440 | Command.Unselect -> W.string w "UNSELECT"
441 | Command.Expunge -> W.string w "EXPUNGE"
442+ | Command.Search { charset; criteria; return_opts } ->
443+ write_search w charset criteria return_opts
444 | Command.Sort { charset; criteria; search } ->
445 write_sort w charset criteria search
446+ | Command.Thread { algorithm; charset; search } ->
447+ write_thread w algorithm charset search
448 | Command.Fetch { sequence; items; changedsince } ->
449 W.string w "FETCH ";
450 sequence_set w sequence;
···514 sequence_set w sequence;
515 sp w;
516 astring w mailbox
517+ | Command.Uid_search { charset; criteria; return_opts } ->
518+ write_search w charset criteria return_opts
519 | Command.Uid_sort { charset; criteria; search } ->
520 write_sort w charset criteria search
521+ | Command.Uid_thread { algorithm; charset; search } ->
522+ write_thread w algorithm charset search
523 | Command.Uid_expunge set ->
524 W.string w "EXPUNGE ";
525 sequence_set w set)
+4
ocaml-imap/lib/imap/write.mli
···43val fetch_item : t -> Fetch.request -> unit
44val fetch_items : t -> Fetch.request list -> unit
45000046(** {1 Commands} *)
4748val command : t -> tag:string -> Command.t -> unit
···43val fetch_item : t -> Fetch.request -> unit
44val fetch_items : t -> Fetch.request list -> unit
4546+(** {1 Thread} *)
47+48+val thread_algorithm : t -> Thread.algorithm -> unit
49+50(** {1 Commands} *)
5152val command : t -> tag:string -> Command.t -> unit
+2-2
ocaml-imap/lib/imapd/client.ml
···132 | Flags_response f -> flags := f
133 | Capability_response c -> caps := c
134 | Enabled e -> enabled := e
135- | List_response { flags = f; delimiter; name } ->
136 list_entries := { flags = f; delimiter; name } :: !list_entries
137 | Status_response { mailbox; items } ->
138 let messages =
···554555let list t ~reference ~pattern =
556 require_authenticated t;
557- let responses = run_command t (List { reference; pattern }) in
558 let _, _, _, _, _, _, _, _, entries, _, _, _, _, _, _, _ =
559 process_untagged responses
560 in
···132 | Flags_response f -> flags := f
133 | Capability_response c -> caps := c
134 | Enabled e -> enabled := e
135+ | List_response { flags = f; delimiter; name; _ } ->
136 list_entries := { flags = f; delimiter; name } :: !list_entries
137 | Status_response { mailbox; items } ->
138 let messages =
···554555let list t ~reference ~pattern =
556 require_authenticated t;
557+ let responses = run_command t (List (List_basic { reference; pattern })) in
558 let _, _, _, _, _, _, _, _, entries, _, _, _, _, _, _, _ =
559 process_untagged responses
560 in
···12open Protocol
1314(* Re-export types from Types for backward compatibility *)
0000000000015type command = Protocol.command =
16 | Capability
17 | Noop
···27 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
28 | Subscribe of mailbox_name
29 | Unsubscribe of mailbox_name
30- | List of { reference : string; pattern : string }
31 | Namespace
32 | Status of { mailbox : mailbox_name; items : status_item list }
33 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···42 | Move of { sequence : sequence_set; mailbox : mailbox_name }
43 | Uid of uid_command
44 | Id of (string * string) list option
0000004546type uid_command = Protocol.uid_command =
47 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···50 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
51 | Uid_search of { charset : string option; criteria : search_key }
52 | Uid_expunge of sequence_set
05354type tagged_command = Protocol.tagged_command = {
55 tag : string;
···64 | Bye of { code : response_code option; text : string }
65 | Capability_response of string list
66 | Enabled of string list
67- | List_response of { flags : list_flag list; delimiter : char option; name : mailbox_name }
68 | Namespace_response of namespace_data
69 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
70 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···74 | Fetch_response of { seq : int; items : fetch_response_item list }
75 | Continuation of string option
76 | Id_response of (string * string) list option
000007778(* ===== Menhir Parser Interface ===== *)
79···113 write_string f "}\r\n";
114 write_string f s
11500000000116let write_flag f flag =
117 write_string f (flag_to_string flag)
118···219 List.iter (fun c -> write_sp f; write_string f c) caps;
220 write_crlf f
221222- | List_response { flags; delimiter; name } ->
0223 write_string f "* LIST (";
224 List.iteri (fun i flag ->
225 if i > 0 then write_sp f;
···231 | List_subscribed -> write_string f "\\Subscribed"
232 | List_haschildren -> write_string f "\\HasChildren"
233 | List_hasnochildren -> write_string f "\\HasNoChildren"
00234 | List_all -> write_string f "\\All"
235 | List_archive -> write_string f "\\Archive"
236 | List_drafts -> write_string f "\\Drafts"
···246 | None -> write_string f "NIL");
247 write_sp f;
248 write_quoted_string f name;
0000000000000249 write_crlf f
250251 | Namespace_response { personal; other; shared } ->
···371 write_quoted_string f value
372 ) pairs;
373 write_char f ')');
000000000000000000000000000000000000000000000000000000000000000000374 write_crlf f
375376let response_to_string resp =
···12open Protocol
1314(* Re-export types from Types for backward compatibility *)
15+type thread_algorithm = Protocol.thread_algorithm =
16+ | Thread_orderedsubject
17+ | Thread_references
18+ | Thread_extension of string
19+20+type thread_node = Protocol.thread_node =
21+ | Thread_message of int * thread_node list
22+ | Thread_dummy of thread_node list
23+24+type thread_result = Protocol.thread_result
25+26type command = Protocol.command =
27 | Capability
28 | Noop
···38 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
39 | Subscribe of mailbox_name
40 | Unsubscribe of mailbox_name
41+ | List of list_command (** LIST command - RFC 9051, RFC 5258 LIST-EXTENDED *)
42 | Namespace
43 | Status of { mailbox : mailbox_name; items : status_item list }
44 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···53 | Move of { sequence : sequence_set; mailbox : mailbox_name }
54 | Uid of uid_command
55 | Id of (string * string) list option
56+ (* QUOTA extension - RFC 9208 *)
57+ | Getquota of string
58+ | Getquotaroot of mailbox_name
59+ | Setquota of { root : string; limits : (quota_resource * int64) list }
60+ (* THREAD extension - RFC 5256 *)
61+ | Thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
6263type uid_command = Protocol.uid_command =
64 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···67 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
68 | Uid_search of { charset : string option; criteria : search_key }
69 | Uid_expunge of sequence_set
70+ | Uid_thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
7172type tagged_command = Protocol.tagged_command = {
73 tag : string;
···82 | Bye of { code : response_code option; text : string }
83 | Capability_response of string list
84 | Enabled of string list
85+ | List_response of list_response_data (** RFC 9051, RFC 5258 LIST-EXTENDED *)
86 | Namespace_response of namespace_data
87 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
88 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···92 | Fetch_response of { seq : int; items : fetch_response_item list }
93 | Continuation of string option
94 | Id_response of (string * string) list option
95+ (* QUOTA extension responses - RFC 9208 *)
96+ | Quota_response of { root : string; resources : quota_resource_info list }
97+ | Quotaroot_response of { mailbox : mailbox_name; roots : string list }
98+ (* THREAD extension response - RFC 5256 *)
99+ | Thread_response of thread_result
100101(* ===== Menhir Parser Interface ===== *)
102···136 write_string f "}\r\n";
137 write_string f s
138139+(** Convert quota resource to IMAP string.
140+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5}RFC 9208 Section 5}. *)
141+let quota_resource_to_string = function
142+ | Quota_storage -> "STORAGE"
143+ | Quota_message -> "MESSAGE"
144+ | Quota_mailbox -> "MAILBOX"
145+ | Quota_annotation_storage -> "ANNOTATION-STORAGE"
146+147let write_flag f flag =
148 write_string f (flag_to_string flag)
149···250 List.iter (fun c -> write_sp f; write_string f c) caps;
251 write_crlf f
252253+ | List_response { flags; delimiter; name; extended } ->
254+ (* LIST response per RFC 9051 Section 7.3.1, RFC 5258 Section 3.4 *)
255 write_string f "* LIST (";
256 List.iteri (fun i flag ->
257 if i > 0 then write_sp f;
···263 | List_subscribed -> write_string f "\\Subscribed"
264 | List_haschildren -> write_string f "\\HasChildren"
265 | List_hasnochildren -> write_string f "\\HasNoChildren"
266+ | List_nonexistent -> write_string f "\\NonExistent" (* RFC 5258 Section 3.4 *)
267+ | List_remote -> write_string f "\\Remote" (* RFC 5258 Section 3.4 *)
268 | List_all -> write_string f "\\All"
269 | List_archive -> write_string f "\\Archive"
270 | List_drafts -> write_string f "\\Drafts"
···280 | None -> write_string f "NIL");
281 write_sp f;
282 write_quoted_string f name;
283+ (* Extended data per RFC 5258 Section 3.5 *)
284+ List.iter (fun ext ->
285+ match ext with
286+ | Childinfo subscriptions ->
287+ (* CHILDINFO extended data item: "CHILDINFO" SP "(" tag-list ")" *)
288+ write_sp f;
289+ write_string f "(\"CHILDINFO\" (";
290+ List.iteri (fun i tag ->
291+ if i > 0 then write_sp f;
292+ write_quoted_string f tag
293+ ) subscriptions;
294+ write_string f "))"
295+ ) extended;
296 write_crlf f
297298 | Namespace_response { personal; other; shared } ->
···418 write_quoted_string f value
419 ) pairs;
420 write_char f ')');
421+ write_crlf f
422+423+ (* QUOTA extension responses - RFC 9208 *)
424+ | Quota_response { root; resources } ->
425+ (* QUOTA response format: * QUOTA root (resource usage limit ...) *)
426+ write_string f "* QUOTA ";
427+ write_quoted_string f root;
428+ write_string f " (";
429+ List.iteri (fun i { resource; usage; limit } ->
430+ if i > 0 then write_sp f;
431+ write_string f (quota_resource_to_string resource);
432+ write_sp f;
433+ write_string f (Int64.to_string usage);
434+ write_sp f;
435+ write_string f (Int64.to_string limit)
436+ ) resources;
437+ write_char f ')';
438+ write_crlf f
439+440+ | Quotaroot_response { mailbox; roots } ->
441+ (* QUOTAROOT response format: * QUOTAROOT mailbox root ... *)
442+ write_string f "* QUOTAROOT ";
443+ write_quoted_string f mailbox;
444+ List.iter (fun root ->
445+ write_sp f;
446+ write_quoted_string f root
447+ ) roots;
448+ write_crlf f
449+450+ (* THREAD extension response - RFC 5256 Section 4 *)
451+ | Thread_response threads ->
452+ (* THREAD response format: * THREAD [SP 1*thread-list]
453+ Each thread node is either:
454+ - (n) for a single message
455+ - (n children...) for a message with children
456+ - ((children...)) for a dummy parent
457+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-4> RFC 5256 Section 4 *)
458+ let rec write_thread_node = function
459+ | Thread_message (n, []) ->
460+ (* Single message with no children: (n) *)
461+ write_char f '(';
462+ write_string f (string_of_int n);
463+ write_char f ')'
464+ | Thread_message (n, children) ->
465+ (* Message with children: (n child1 child2 ...) *)
466+ write_char f '(';
467+ write_string f (string_of_int n);
468+ List.iter (fun child ->
469+ write_sp f;
470+ write_thread_node child
471+ ) children;
472+ write_char f ')'
473+ | Thread_dummy children ->
474+ (* Dummy node (missing parent): ((child1)(child2)...) *)
475+ write_char f '(';
476+ List.iteri (fun i child ->
477+ if i > 0 then write_sp f;
478+ write_thread_node child
479+ ) children;
480+ write_char f ')'
481+ in
482+ write_string f "* THREAD";
483+ List.iter (fun thread ->
484+ write_sp f;
485+ write_thread_node thread
486+ ) threads;
487 write_crlf f
488489let response_to_string resp =
+19-2
ocaml-imap/lib/imapd/parser.mli
···1516 Types are defined in {!Protocol} and re-exported here for convenience. *)
170000018type command = Protocol.command =
19 | Capability
20 | Noop
···30 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
31 | Subscribe of mailbox_name
32 | Unsubscribe of mailbox_name
33- | List of { reference : string; pattern : string }
34 | Namespace
35 | Status of { mailbox : mailbox_name; items : status_item list }
36 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···45 | Move of { sequence : sequence_set; mailbox : mailbox_name }
46 | Uid of uid_command
47 | Id of (string * string) list option
0000004849type uid_command = Protocol.uid_command =
50 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···53 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
54 | Uid_search of { charset : string option; criteria : search_key }
55 | Uid_expunge of sequence_set
05657type tagged_command = Protocol.tagged_command = {
58 tag : string;
···67 | Bye of { code : response_code option; text : string }
68 | Capability_response of string list
69 | Enabled of string list
70- | List_response of { flags : list_flag list; delimiter : char option; name : mailbox_name }
71 | Namespace_response of namespace_data
72 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
73 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···77 | Fetch_response of { seq : int; items : fetch_response_item list }
78 | Continuation of string option
79 | Id_response of (string * string) list option
000008081(** {1 Parsing} *)
82
···1516 Types are defined in {!Protocol} and re-exported here for convenience. *)
1718+type thread_algorithm = Protocol.thread_algorithm =
19+ | Thread_orderedsubject
20+ | Thread_references
21+ | Thread_extension of string
22+23type command = Protocol.command =
24 | Capability
25 | Noop
···35 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
36 | Subscribe of mailbox_name
37 | Unsubscribe of mailbox_name
38+ | List of list_command
39 | Namespace
40 | Status of { mailbox : mailbox_name; items : status_item list }
41 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···50 | Move of { sequence : sequence_set; mailbox : mailbox_name }
51 | Uid of uid_command
52 | Id of (string * string) list option
53+ (* QUOTA extension - RFC 9208 *)
54+ | Getquota of string
55+ | Getquotaroot of mailbox_name
56+ | Setquota of { root : string; limits : (quota_resource * int64) list }
57+ (* THREAD extension - RFC 5256 *)
58+ | Thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
5960type uid_command = Protocol.uid_command =
61 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···64 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
65 | Uid_search of { charset : string option; criteria : search_key }
66 | Uid_expunge of sequence_set
67+ | Uid_thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
6869type tagged_command = Protocol.tagged_command = {
70 tag : string;
···79 | Bye of { code : response_code option; text : string }
80 | Capability_response of string list
81 | Enabled of string list
82+ | List_response of list_response_data
83 | Namespace_response of namespace_data
84 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
85 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···89 | Fetch_response of { seq : int; items : fetch_response_item list }
90 | Continuation of string option
91 | Id_response of (string * string) list option
92+ (* QUOTA extension responses - RFC 9208 *)
93+ | Quota_response of { root : string; resources : quota_resource_info list }
94+ | Quotaroot_response of { mailbox : mailbox_name; roots : string list }
95+ (* THREAD extension response - RFC 5256 *)
96+ | Thread_response of thread_result
9798(** {1 Parsing} *)
99
+127-3
ocaml-imap/lib/imapd/protocol.ml
···183 | Status_deleted
184 | Status_size
185186-(* LIST flags - RFC 9051 Section 7.3.1 *)
187type list_flag =
188 | List_noinferiors
189 | List_noselect
···192 | List_subscribed
193 | List_haschildren
194 | List_hasnochildren
00195 | List_all
196 | List_archive
197 | List_drafts
···200 | List_sent
201 | List_trash
202 | List_extension of string
0000000000000000000000000000000000000000000000000203204(* Connection state - RFC 9051 Section 3 *)
205type connection_state =
···334 else
335 None
3360000000000000000000000000000000000000000000000000337(* === Commands - RFC 9051 Section 6 === *)
338339type command =
···351 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
352 | Subscribe of mailbox_name
353 | Unsubscribe of mailbox_name
354- | List of { reference : string; pattern : string }
355 | Namespace
356 | Status of { mailbox : mailbox_name; items : status_item list }
357 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···366 | Move of { sequence : sequence_set; mailbox : mailbox_name }
367 | Uid of uid_command
368 | Id of (string * string) list option (** RFC 2971 - NIL or list of field/value pairs *)
00000000000369370and uid_command =
371 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···374 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
375 | Uid_search of { charset : string option; criteria : search_key }
376 | Uid_expunge of sequence_set
00377378type tagged_command = {
379 tag : string;
···419 | Bye of { code : response_code option; text : string }
420 | Capability_response of string list
421 | Enabled of string list
422- | List_response of { flags : list_flag list; delimiter : char option; name : mailbox_name }
423 | Namespace_response of namespace_data
424 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
425 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···429 | Fetch_response of { seq : int; items : fetch_response_item list }
430 | Continuation of string option
431 | Id_response of (string * string) list option (** RFC 2971 - NIL or list of field/value pairs *)
00000000000
···183 | Status_deleted
184 | Status_size
185186+(* LIST flags - RFC 9051 Section 7.3.1, RFC 5258 Section 3.4 *)
187type list_flag =
188 | List_noinferiors
189 | List_noselect
···192 | List_subscribed
193 | List_haschildren
194 | List_hasnochildren
195+ | List_nonexistent (** RFC 5258 Section 3.4 - Mailbox name refers to non-existent mailbox *)
196+ | List_remote (** RFC 5258 Section 3.4 - Mailbox is remote, not on this server *)
197 | List_all
198 | List_archive
199 | List_drafts
···202 | List_sent
203 | List_trash
204 | List_extension of string
205+206+(** LIST selection options per RFC 5258 Section 3.1
207+208+ Selection options control which mailboxes are returned by LIST:
209+ - SUBSCRIBED: Return subscribed mailboxes (like LSUB)
210+ - REMOTE: Include remote mailboxes (not on this server)
211+ - RECURSIVEMATCH: Include ancestors of matched mailboxes
212+ - SPECIAL-USE: Return only special-use mailboxes (RFC 6154) *)
213+type list_select_option =
214+ | List_select_subscribed (** RFC 5258 Section 3.1.1 *)
215+ | List_select_remote (** RFC 5258 Section 3.1.2 *)
216+ | List_select_recursivematch (** RFC 5258 Section 3.1.3 *)
217+ | List_select_special_use (** RFC 6154 Section 3 *)
218+219+(** LIST return options per RFC 5258 Section 3.2
220+221+ Return options control what additional data is returned:
222+ - SUBSCRIBED: Include \Subscribed flag
223+ - CHILDREN: Include \HasChildren/\HasNoChildren flags
224+ - SPECIAL-USE: Include special-use flags (RFC 6154) *)
225+type list_return_option =
226+ | List_return_subscribed (** RFC 5258 Section 3.2.1 *)
227+ | List_return_children (** RFC 5258 Section 3.2.2 *)
228+ | List_return_special_use (** RFC 6154 Section 3 *)
229+230+(** Extended data items in LIST response per RFC 5258 Section 3.5 *)
231+type list_extended_item =
232+ | Childinfo of string list (** RFC 5258 Section 3.5 - CHILDINFO extended data *)
233+234+(** LIST command variants per RFC 5258 *)
235+type list_command =
236+ | List_basic of {
237+ reference : string; (** Reference name (context for pattern) *)
238+ pattern : string; (** Mailbox pattern with wildcards *)
239+ }
240+ | List_extended of {
241+ selection : list_select_option list; (** RFC 5258 Section 3.1 *)
242+ reference : string;
243+ patterns : string list; (** Multiple patterns allowed *)
244+ return_opts : list_return_option list; (** RFC 5258 Section 3.2 *)
245+ }
246+247+(** Extended LIST response per RFC 5258 Section 3.4 *)
248+type list_response_data = {
249+ flags : list_flag list;
250+ delimiter : char option;
251+ name : mailbox_name;
252+ extended : list_extended_item list; (** RFC 5258 Section 3.5 *)
253+}
254255(* Connection state - RFC 9051 Section 3 *)
256type connection_state =
···385 else
386 None
387388+(* === THREAD Types - RFC 5256 === *)
389+390+(** Threading algorithm for the THREAD command.
391+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-3}RFC 5256 Section 3}. *)
392+type thread_algorithm =
393+ | Thread_orderedsubject
394+ (** ORDEREDSUBJECT algorithm (RFC 5256 Section 3.1).
395+ Groups messages by base subject, then sorts by sent date. *)
396+ | Thread_references
397+ (** REFERENCES algorithm (RFC 5256 Section 3.2).
398+ Implements the JWZ threading algorithm using Message-ID,
399+ In-Reply-To, and References headers. *)
400+ | Thread_extension of string
401+ (** Future algorithm extensions. *)
402+403+(** A thread node in the THREAD response.
404+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-4}RFC 5256 Section 4}. *)
405+type thread_node =
406+ | Thread_message of int * thread_node list
407+ (** A message with its sequence number/UID and child threads. *)
408+ | Thread_dummy of thread_node list
409+ (** A placeholder for a missing parent message. *)
410+411+(** Thread result: a list of root-level thread trees.
412+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-4}RFC 5256 Section 4}. *)
413+type thread_result = thread_node list
414+415+(* === Quota Types - RFC 9208 === *)
416+417+(** Quota resource types.
418+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5}RFC 9208 Section 5}. *)
419+type quota_resource =
420+ | Quota_storage (** STORAGE - physical space in KB.
421+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.1}RFC 9208 Section 5.1}. *)
422+ | Quota_message (** MESSAGE - number of messages.
423+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.2}RFC 9208 Section 5.2}. *)
424+ | Quota_mailbox (** MAILBOX - number of mailboxes.
425+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.3}RFC 9208 Section 5.3}. *)
426+ | Quota_annotation_storage (** ANNOTATION-STORAGE - annotation size in KB.
427+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.4}RFC 9208 Section 5.4}. *)
428+429+(** A single quota resource with usage and limit.
430+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.2.2}RFC 9208 Section 4.2.2}. *)
431+type quota_resource_info = {
432+ resource : quota_resource;
433+ usage : int64; (** Current usage *)
434+ limit : int64; (** Maximum allowed *)
435+}
436+437(* === Commands - RFC 9051 Section 6 === *)
438439type command =
···451 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
452 | Subscribe of mailbox_name
453 | Unsubscribe of mailbox_name
454+ | List of list_command (** LIST command - RFC 9051, RFC 5258 LIST-EXTENDED *)
455 | Namespace
456 | Status of { mailbox : mailbox_name; items : status_item list }
457 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···466 | Move of { sequence : sequence_set; mailbox : mailbox_name }
467 | Uid of uid_command
468 | Id of (string * string) list option (** RFC 2971 - NIL or list of field/value pairs *)
469+ (* QUOTA extension - RFC 9208 *)
470+ | Getquota of string (** GETQUOTA quota-root.
471+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.2}RFC 9208 Section 4.2}. *)
472+ | Getquotaroot of mailbox_name (** GETQUOTAROOT mailbox.
473+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.3}RFC 9208 Section 4.3}. *)
474+ | Setquota of { root : string; limits : (quota_resource * int64) list } (** SETQUOTA.
475+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.1}RFC 9208 Section 4.1}. *)
476+ (* THREAD extension - RFC 5256 *)
477+ | Thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
478+ (** THREAD command.
479+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-3}RFC 5256 Section 3}. *)
480481and uid_command =
482 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···485 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
486 | Uid_search of { charset : string option; criteria : search_key }
487 | Uid_expunge of sequence_set
488+ | Uid_thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
489+ (** UID THREAD command - RFC 5256. Returns UIDs instead of sequence numbers. *)
490491type tagged_command = {
492 tag : string;
···532 | Bye of { code : response_code option; text : string }
533 | Capability_response of string list
534 | Enabled of string list
535+ | List_response of list_response_data (** RFC 9051, RFC 5258 LIST-EXTENDED *)
536 | Namespace_response of namespace_data
537 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
538 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···542 | Fetch_response of { seq : int; items : fetch_response_item list }
543 | Continuation of string option
544 | Id_response of (string * string) list option (** RFC 2971 - NIL or list of field/value pairs *)
545+ (* QUOTA extension responses - RFC 9208 *)
546+ | Quota_response of { root : string; resources : quota_resource_info list }
547+ (** QUOTA response.
548+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.1}RFC 9208 Section 5.1}. *)
549+ | Quotaroot_response of { mailbox : mailbox_name; roots : string list }
550+ (** QUOTAROOT response.
551+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.2}RFC 9208 Section 5.2}. *)
552+ (* THREAD extension response - RFC 5256 *)
553+ | Thread_response of thread_result
554+ (** THREAD response - a list of thread trees.
555+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-4}RFC 5256 Section 4}. *)
+115-2
ocaml-imap/lib/imapd/protocol.mli
···245 | List_subscribed (** \Subscribed *)
246 | List_haschildren (** \HasChildren *)
247 | List_hasnochildren (** \HasNoChildren *)
00248 | List_all (** \All - special-use *)
249 | List_archive (** \Archive *)
250 | List_drafts (** \Drafts *)
···253 | List_sent (** \Sent *)
254 | List_trash (** \Trash *)
255 | List_extension of string (** Other flags *)
00000000000000000000000000000000000000256257(** {1 Connection State}
258···332 | Code_unknown_cte
333 | Code_other of string * string option
3340000000000000000000000000000000000000000000000000335(** {1 Commands}
336337 See {{:https://datatracker.ietf.org/doc/html/rfc9051#section-6}RFC 9051 Section 6}. *)
···351 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
352 | Subscribe of mailbox_name
353 | Unsubscribe of mailbox_name
354- | List of { reference : string; pattern : string }
355 | Namespace
356 | Status of { mailbox : mailbox_name; items : status_item list }
357 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···366 | Move of { sequence : sequence_set; mailbox : mailbox_name }
367 | Uid of uid_command
368 | Id of (string * string) list option (** RFC 2971 *)
00000000000369370and uid_command =
371 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···374 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
375 | Uid_search of { charset : string option; criteria : search_key }
376 | Uid_expunge of sequence_set
00377378type tagged_command = {
379 tag : string;
···421 | Bye of { code : response_code option; text : string }
422 | Capability_response of string list
423 | Enabled of string list
424- | List_response of { flags : list_flag list; delimiter : char option; name : mailbox_name }
425 | Namespace_response of namespace_data
426 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
427 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···431 | Fetch_response of { seq : int; items : fetch_response_item list }
432 | Continuation of string option
433 | Id_response of (string * string) list option (** RFC 2971 *)
00000000000434435(** {1 Utility Functions} *)
436
···245 | List_subscribed (** \Subscribed *)
246 | List_haschildren (** \HasChildren *)
247 | List_hasnochildren (** \HasNoChildren *)
248+ | List_nonexistent (** \NonExistent - RFC 5258 Section 3.4 *)
249+ | List_remote (** \Remote - RFC 5258 Section 3.4 *)
250 | List_all (** \All - special-use *)
251 | List_archive (** \Archive *)
252 | List_drafts (** \Drafts *)
···255 | List_sent (** \Sent *)
256 | List_trash (** \Trash *)
257 | List_extension of string (** Other flags *)
258+259+(** LIST selection options per RFC 5258 Section 3.1 *)
260+type list_select_option =
261+ | List_select_subscribed (** RFC 5258 Section 3.1.1 *)
262+ | List_select_remote (** RFC 5258 Section 3.1.2 *)
263+ | List_select_recursivematch (** RFC 5258 Section 3.1.3 *)
264+ | List_select_special_use (** RFC 6154 Section 3 *)
265+266+(** LIST return options per RFC 5258 Section 3.2 *)
267+type list_return_option =
268+ | List_return_subscribed (** RFC 5258 Section 3.2.1 *)
269+ | List_return_children (** RFC 5258 Section 3.2.2 *)
270+ | List_return_special_use (** RFC 6154 Section 3 *)
271+272+(** Extended data items in LIST response per RFC 5258 Section 3.5 *)
273+type list_extended_item =
274+ | Childinfo of string list (** RFC 5258 Section 3.5 - CHILDINFO extended data *)
275+276+(** LIST command variants per RFC 5258 *)
277+type list_command =
278+ | List_basic of {
279+ reference : string; (** Reference name *)
280+ pattern : string; (** Mailbox pattern *)
281+ }
282+ | List_extended of {
283+ selection : list_select_option list;
284+ reference : string;
285+ patterns : string list;
286+ return_opts : list_return_option list;
287+ }
288+289+(** Extended LIST response per RFC 5258 Section 3.4 *)
290+type list_response_data = {
291+ flags : list_flag list;
292+ delimiter : char option;
293+ name : mailbox_name;
294+ extended : list_extended_item list;
295+}
296297(** {1 Connection State}
298···372 | Code_unknown_cte
373 | Code_other of string * string option
374375+(** {1 Quota Types}
376+377+ See {{:https://datatracker.ietf.org/doc/html/rfc9208}RFC 9208 - IMAP QUOTA Extension}. *)
378+379+(** Quota resource types.
380+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5}RFC 9208 Section 5}. *)
381+type quota_resource =
382+ | Quota_storage (** STORAGE - physical space in KB.
383+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.1}RFC 9208 Section 5.1}. *)
384+ | Quota_message (** MESSAGE - number of messages.
385+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.2}RFC 9208 Section 5.2}. *)
386+ | Quota_mailbox (** MAILBOX - number of mailboxes.
387+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.3}RFC 9208 Section 5.3}. *)
388+ | Quota_annotation_storage (** ANNOTATION-STORAGE - annotation size in KB.
389+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.4}RFC 9208 Section 5.4}. *)
390+391+(** A single quota resource with usage and limit.
392+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.2.2}RFC 9208 Section 4.2.2}. *)
393+type quota_resource_info = {
394+ resource : quota_resource;
395+ usage : int64; (** Current usage *)
396+ limit : int64; (** Maximum allowed *)
397+}
398+399+(** {1 Thread Types}
400+401+ See {{:https://datatracker.ietf.org/doc/html/rfc5256}RFC 5256 - IMAP SORT and THREAD Extensions}. *)
402+403+(** Threading algorithm for the THREAD command.
404+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-3}RFC 5256 Section 3}. *)
405+type thread_algorithm =
406+ | Thread_orderedsubject
407+ (** ORDEREDSUBJECT algorithm (RFC 5256 Section 3.1). *)
408+ | Thread_references
409+ (** REFERENCES algorithm (RFC 5256 Section 3.2). *)
410+ | Thread_extension of string
411+ (** Future algorithm extensions. *)
412+413+(** A thread node in the THREAD response.
414+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-4}RFC 5256 Section 4}. *)
415+type thread_node =
416+ | Thread_message of int * thread_node list
417+ (** A message with its sequence number/UID and child threads. *)
418+ | Thread_dummy of thread_node list
419+ (** A placeholder for a missing parent message. *)
420+421+(** Thread result: a list of root-level thread trees. *)
422+type thread_result = thread_node list
423+424(** {1 Commands}
425426 See {{:https://datatracker.ietf.org/doc/html/rfc9051#section-6}RFC 9051 Section 6}. *)
···440 | Rename of { old_name : mailbox_name; new_name : mailbox_name }
441 | Subscribe of mailbox_name
442 | Unsubscribe of mailbox_name
443+ | List of list_command (** LIST command - RFC 9051, RFC 5258 LIST-EXTENDED *)
444 | Namespace
445 | Status of { mailbox : mailbox_name; items : status_item list }
446 | Append of { mailbox : mailbox_name; flags : flag list; date : string option; message : string }
···455 | Move of { sequence : sequence_set; mailbox : mailbox_name }
456 | Uid of uid_command
457 | Id of (string * string) list option (** RFC 2971 *)
458+ (* QUOTA extension - RFC 9208 *)
459+ | Getquota of string (** GETQUOTA quota-root.
460+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.2}RFC 9208 Section 4.2}. *)
461+ | Getquotaroot of mailbox_name (** GETQUOTAROOT mailbox.
462+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.3}RFC 9208 Section 4.3}. *)
463+ | Setquota of { root : string; limits : (quota_resource * int64) list } (** SETQUOTA.
464+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-4.1}RFC 9208 Section 4.1}. *)
465+ (* THREAD extension - RFC 5256 *)
466+ | Thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
467+ (** THREAD command.
468+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-3}RFC 5256 Section 3}. *)
469470and uid_command =
471 | Uid_fetch of { sequence : sequence_set; items : fetch_item list }
···474 | Uid_move of { sequence : sequence_set; mailbox : mailbox_name }
475 | Uid_search of { charset : string option; criteria : search_key }
476 | Uid_expunge of sequence_set
477+ | Uid_thread of { algorithm : thread_algorithm; charset : string; criteria : search_key }
478+ (** UID THREAD command - RFC 5256. Returns UIDs instead of sequence numbers. *)
479480type tagged_command = {
481 tag : string;
···523 | Bye of { code : response_code option; text : string }
524 | Capability_response of string list
525 | Enabled of string list
526+ | List_response of list_response_data (** RFC 9051, RFC 5258 LIST-EXTENDED *)
527 | Namespace_response of namespace_data
528 | Status_response of { mailbox : mailbox_name; items : (status_item * int64) list }
529 | Esearch of { tag : string option; uid : bool; results : esearch_result list }
···533 | Fetch_response of { seq : int; items : fetch_response_item list }
534 | Continuation of string option
535 | Id_response of (string * string) list option (** RFC 2971 *)
536+ (* QUOTA extension responses - RFC 9208 *)
537+ | Quota_response of { root : string; resources : quota_resource_info list }
538+ (** QUOTA response.
539+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.1}RFC 9208 Section 5.1}. *)
540+ | Quotaroot_response of { mailbox : mailbox_name; roots : string list }
541+ (** QUOTAROOT response.
542+ See {{:https://datatracker.ietf.org/doc/html/rfc9208#section-5.2}RFC 9208 Section 5.2}. *)
543+ (* THREAD extension response - RFC 5256 *)
544+ | Thread_response of thread_result
545+ (** THREAD response containing thread tree.
546+ See {{:https://datatracker.ietf.org/doc/html/rfc5256#section-4}RFC 5256 Section 4}. *)
547548(** {1 Utility Functions} *)
549
+1-1
ocaml-imap/lib/imapd/read.ml
···870 sp r;
871 let name = astring r in
872 crlf r;
873- List_response { flags; delimiter; name }
874 | "STATUS" ->
875 sp r;
876 let mailbox = astring r in
···870 sp r;
871 let name = astring r in
872 crlf r;
873+ List_response { flags; delimiter; name; extended = [] }
874 | "STATUS" ->
875 sp r;
876 let mailbox = astring r in
+172-49
ocaml-imap/lib/imapd/server.ml
···13(* Module alias to access Storage types without conflicting with functor parameter *)
14module Storage_types = Storage
1516-(* Base capabilities per RFC 9051 *)
0017let base_capabilities_pre_tls = [
18 "IMAP4rev2";
19 "IMAP4rev1"; (* For compatibility *)
···26 "ENABLE";
27 "LITERAL+";
28 "ID";
000000000000029]
3031let base_capabilities_post_tls = [
···39 "ENABLE";
40 "LITERAL+";
41 "ID";
000000000000042]
4344(* Server configuration *)
···62 (Storage : Storage.STORAGE)
63 (Auth : Auth.AUTH) = struct
640065 type connection_state =
66 | Not_authenticated
67- | Authenticated of { username : string }
68- | Selected of { username : string; mailbox : string; readonly : bool }
69 | Logout
7071 (* Action returned by command handlers *)
···151 code = Some (Code_capability caps);
152 text = "LOGIN completed"
153 });
154- Authenticated { username }
155 end else begin
156 send_response flow (No {
157 tag = Some tag;
···170171 (* Process SELECT/EXAMINE command - only valid in Authenticated/Selected state *)
172 let handle_select t flow tag mailbox ~readonly state =
173- let username = match state with
174- | Authenticated { username } -> Some username
175- | Selected { username; _ } -> Some username
176- | _ -> None
177 in
178 match username with
179 | None ->
···191 code = None;
192 text = "Invalid mailbox name"
193 });
194- Authenticated { username }
195 end else
196 match Storage.select_mailbox t.storage ~username mailbox ~readonly with
197 | Error _ ->
···200 code = Some Code_nonexistent;
201 text = "Mailbox does not exist"
202 });
203- Authenticated { username }
204 | Ok mb_state ->
205 (* Send untagged responses *)
206 send_response flow (Flags_response mb_state.flags);
···227 code;
228 text = if readonly then "EXAMINE completed" else "SELECT completed"
229 });
230- Selected { username; mailbox; readonly }
231232- (* Process LIST command *)
233- let handle_list t flow tag ~reference ~pattern state =
234 let username = match state with
235- | Authenticated { username } -> Some username
236 | Selected { username; _ } -> Some username
237 | _ -> None
238 in
···245 });
246 state
247 | Some username ->
248- let mailboxes = Storage.list_mailboxes t.storage ~username ~reference ~pattern in
000000000249 List.iter (fun (mb : Storage_types.mailbox_info) ->
250 send_response flow (List_response {
251 flags = mb.flags;
252 delimiter = mb.delimiter;
253 name = mb.name;
0254 })
255- ) mailboxes;
256 send_response flow (Ok { tag = Some tag; code = None; text = "LIST completed" });
257 state
258···264 state
265 end else
266 let username = match state with
267- | Authenticated { username } -> Some username
268 | Selected { username; _ } -> Some username
269 | _ -> None
270 in
···320 (* Process STORE command *)
321 let handle_store t flow tag ~sequence ~silent ~action ~flags state =
322 match state with
323- | Selected { username; mailbox; readonly } ->
324 if readonly then begin
325 send_response flow (No { tag = Some tag; code = None; text = "Mailbox is read-only" });
326 state
···351 (* Process EXPUNGE command *)
352 let handle_expunge t flow tag state =
353 match state with
354- | Selected { username; mailbox; readonly } ->
355 if readonly then begin
356 send_response flow (No { tag = Some tag; code = None; text = "Mailbox is read-only" });
357 state
···376 (* Process CLOSE command *)
377 let handle_close t flow tag state =
378 match state with
379- | Selected { username; mailbox; readonly } ->
380 (* Silently expunge if not readonly *)
381 if not readonly then
382 ignore (Storage.expunge t.storage ~username ~mailbox);
383 send_response flow (Ok { tag = Some tag; code = None; text = "CLOSE completed" });
384- Authenticated { username }
385 | _ ->
386 send_response flow (Bad {
387 tag = Some tag;
···393 (* Process UNSELECT command *)
394 let handle_unselect flow tag state =
395 match state with
396- | Selected { username; _ } ->
397 send_response flow (Ok { tag = Some tag; code = None; text = "UNSELECT completed" });
398- Authenticated { username }
399 | _ ->
400 send_response flow (Bad {
401 tag = Some tag;
···412 state
413 end else
414 let username = match state with
415- | Authenticated { username } -> Some username
416 | Selected { username; _ } -> Some username
417 | _ -> None
418 in
···448 state
449 end else
450 let username = match state with
451- | Authenticated { username } -> Some username
452 | Selected { username; _ } -> Some username
453 | _ -> None
454 in
···485 state
486 end else
487 let username = match state with
488- | Authenticated { username } -> Some username
489 | Selected { username; _ } -> Some username
490 | _ -> None
491 in
···567 state
568 end else
569 match state with
570- | Selected { username; mailbox = src_mailbox; readonly } ->
571 if readonly then begin
572 send_response flow (No { tag = Some tag; code = None; text = "Mailbox is read-only" });
573 state
···605 });
606 state
607608- (* Process SEARCH command *)
609- let handle_search t flow tag ~charset:_ ~criteria state =
00610 match state with
611- | Selected { username; mailbox; _ } ->
612- (match Storage.search t.storage ~username ~mailbox ~criteria with
613- | Result.Error _ ->
614- send_response flow (No { tag = Some tag; code = None; text = "SEARCH failed" })
615- | Result.Ok uids ->
616- (* Send ESEARCH response per RFC 9051 *)
617- let results = if List.length uids > 0 then
618- [Esearch_count (List.length uids); Esearch_all (List.map (fun uid -> Single (Int32.to_int uid)) uids)]
619- else
620- [Esearch_count 0]
621- in
622- send_response flow (Esearch { tag = Some tag; uid = false; results });
623- send_response flow (Ok { tag = Some tag; code = None; text = "SEARCH completed" }));
624- state
00000000000625 | _ ->
626 send_response flow (Bad {
627 tag = Some tag;
···630 });
631 state
632000000000000000000000000000000000633 (* Process APPEND command *)
634 let handle_append t flow tag ~mailbox ~flags ~date ~message state =
635 (* Security: Validate mailbox name *)
···638 state
639 end else
640 let username = match state with
641- | Authenticated { username } -> Some username
642 | Selected { username; _ } -> Some username
643 | _ -> None
644 in
···694 });
695 state
696697- (* Process ENABLE command - RFC 5161 *)
000698 let handle_enable flow tag ~capabilities state =
699 match state with
700- | Authenticated _ ->
701 (* Filter to capabilities we actually support *)
702 let enabled = List.filter (fun cap ->
703 let cap_upper = String.uppercase_ascii cap in
704 cap_upper = "IMAP4REV2" || cap_upper = "UTF8=ACCEPT"
705 ) capabilities in
0000706 if List.length enabled > 0 then
707 send_response flow (Enabled enabled);
708 send_response flow (Ok { tag = Some tag; code = None; text = "ENABLE completed" });
709- state
710 | _ ->
711 send_response flow (Bad {
712 tag = Some tag;
···779 | Login { username; password } -> (handle_login t flow tag ~username ~password ~tls_active state, Continue)
780 | Select mailbox -> (handle_select t flow tag mailbox ~readonly:false state, Continue)
781 | Examine mailbox -> (handle_select t flow tag mailbox ~readonly:true state, Continue)
782- | List { reference; pattern } -> (handle_list t flow tag ~reference ~pattern state, Continue)
783 | Status { mailbox; items } -> (handle_status t flow tag mailbox ~items state, Continue)
784 | Fetch { sequence; items } -> (handle_fetch t flow tag ~sequence ~items state, Continue)
785 | Store { sequence; silent; action; flags } -> (handle_store t flow tag ~sequence ~silent ~action ~flags state, Continue)
···792 | Copy { sequence; mailbox } -> (handle_copy t flow tag ~sequence ~mailbox state, Continue)
793 | Move { sequence; mailbox } -> (handle_move t flow tag ~sequence ~mailbox state, Continue)
794 | Search { charset; criteria } -> (handle_search t flow tag ~charset ~criteria state, Continue)
0795 | Append { mailbox; flags; date; message } -> (handle_append t flow tag ~mailbox ~flags ~date ~message state, Continue)
796 | Namespace -> (handle_namespace flow tag state, Continue)
797 | Enable caps -> (handle_enable flow tag ~capabilities:caps state, Continue)
···816 | Authenticate _ ->
817 send_response flow (No { tag = Some tag; code = None; text = "Use LOGIN instead" });
818 (state, Continue)
00000000000000000000000000819820 (* Handle UID prefixed commands *)
821 and handle_uid_command t flow tag ~read_line_fn:_ uid_cmd state =
···834 | Uid_expunge _sequence ->
835 (* UID EXPUNGE only expunges messages in the given UID set *)
836 handle_expunge t flow tag state
000837838 (* Maximum line length to prevent DoS attacks via memory exhaustion.
839 RFC 9051 Section 4 recommends supporting lines up to 8192 octets. *)
···990 text = "LOGIN completed"
991 });
992 (* Continue session as authenticated user *)
993- let state = Authenticated { username } in
994 ignore (command_loop t flow state tls_active)
995 end else begin
996 (* Failed to drop privileges *)
···13(* Module alias to access Storage types without conflicting with functor parameter *)
14module Storage_types = Storage
1516+(** Base capabilities per RFC 9051.
17+ @see <https://datatracker.ietf.org/doc/html/rfc9051> RFC 9051: IMAP4rev2
18+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3: UTF8=ACCEPT *)
19let base_capabilities_pre_tls = [
20 "IMAP4rev2";
21 "IMAP4rev1"; (* For compatibility *)
···28 "ENABLE";
29 "LITERAL+";
30 "ID";
31+ "UNSELECT"; (* RFC 3691 *)
32+ "SPECIAL-USE"; (* RFC 6154 *)
33+ "LIST-EXTENDED"; (* RFC 5258 *)
34+ "CONDSTORE"; (* RFC 7162 - modification sequences for flags *)
35+ (* QUOTA extension - RFC 9208 *)
36+ "QUOTA";
37+ "QUOTA=RES-STORAGE"; (* RFC 9208 Section 5.1 *)
38+ "QUOTA=RES-MESSAGE"; (* RFC 9208 Section 5.2 *)
39+ (* UTF-8 support - RFC 6855 *)
40+ "UTF8=ACCEPT"; (* RFC 6855 Section 3 *)
41+ (* THREAD extension - RFC 5256 *)
42+ "THREAD=ORDEREDSUBJECT"; (* RFC 5256 Section 3.1 *)
43+ "THREAD=REFERENCES"; (* RFC 5256 Section 3.2 *)
44]
4546let base_capabilities_post_tls = [
···54 "ENABLE";
55 "LITERAL+";
56 "ID";
57+ "UNSELECT"; (* RFC 3691 *)
58+ "SPECIAL-USE"; (* RFC 6154 *)
59+ "LIST-EXTENDED"; (* RFC 5258 *)
60+ "CONDSTORE"; (* RFC 7162 - modification sequences for flags *)
61+ (* QUOTA extension - RFC 9208 *)
62+ "QUOTA";
63+ "QUOTA=RES-STORAGE"; (* RFC 9208 Section 5.1 *)
64+ "QUOTA=RES-MESSAGE"; (* RFC 9208 Section 5.2 *)
65+ (* UTF-8 support - RFC 6855 *)
66+ "UTF8=ACCEPT"; (* RFC 6855 Section 3 *)
67+ (* THREAD extension - RFC 5256 *)
68+ "THREAD=ORDEREDSUBJECT"; (* RFC 5256 Section 3.1 *)
69+ "THREAD=REFERENCES"; (* RFC 5256 Section 3.2 *)
70]
7172(* Server configuration *)
···90 (Storage : Storage.STORAGE)
91 (Auth : Auth.AUTH) = struct
9293+ (** Connection state with UTF-8 mode tracking.
94+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3 *)
95 type connection_state =
96 | Not_authenticated
97+ | Authenticated of { username : string; utf8_enabled : bool }
98+ | Selected of { username : string; mailbox : string; readonly : bool; utf8_enabled : bool }
99 | Logout
100101 (* Action returned by command handlers *)
···181 code = Some (Code_capability caps);
182 text = "LOGIN completed"
183 });
184+ Authenticated { username; utf8_enabled = false }
185 end else begin
186 send_response flow (No {
187 tag = Some tag;
···200201 (* Process SELECT/EXAMINE command - only valid in Authenticated/Selected state *)
202 let handle_select t flow tag mailbox ~readonly state =
203+ let username, utf8_enabled = match state with
204+ | Authenticated { username; utf8_enabled } -> Some username, utf8_enabled
205+ | Selected { username; utf8_enabled; _ } -> Some username, utf8_enabled
206+ | _ -> None, false
207 in
208 match username with
209 | None ->
···221 code = None;
222 text = "Invalid mailbox name"
223 });
224+ Authenticated { username; utf8_enabled }
225 end else
226 match Storage.select_mailbox t.storage ~username mailbox ~readonly with
227 | Error _ ->
···230 code = Some Code_nonexistent;
231 text = "Mailbox does not exist"
232 });
233+ Authenticated { username; utf8_enabled }
234 | Ok mb_state ->
235 (* Send untagged responses *)
236 send_response flow (Flags_response mb_state.flags);
···257 code;
258 text = if readonly then "EXAMINE completed" else "SELECT completed"
259 });
260+ Selected { username; mailbox; readonly; utf8_enabled }
261262+ (* Process LIST command - RFC 9051, RFC 5258 LIST-EXTENDED *)
263+ let handle_list t flow tag list_cmd state =
264 let username = match state with
265+ | Authenticated { username; _ } -> Some username
266 | Selected { username; _ } -> Some username
267 | _ -> None
268 in
···275 });
276 state
277 | Some username ->
278+ (* Extract reference and patterns from list command *)
279+ let reference, patterns = match list_cmd with
280+ | List_basic { reference; pattern } -> (reference, [pattern])
281+ | List_extended { reference; patterns; _ } -> (reference, patterns)
282+ in
283+ (* Process each pattern and collect mailboxes *)
284+ let all_mailboxes = List.concat_map (fun pattern ->
285+ Storage.list_mailboxes t.storage ~username ~reference ~pattern
286+ ) patterns in
287+ (* Send LIST responses with extended data if needed *)
288 List.iter (fun (mb : Storage_types.mailbox_info) ->
289 send_response flow (List_response {
290 flags = mb.flags;
291 delimiter = mb.delimiter;
292 name = mb.name;
293+ extended = []; (* Extended data can be populated based on return options *)
294 })
295+ ) all_mailboxes;
296 send_response flow (Ok { tag = Some tag; code = None; text = "LIST completed" });
297 state
298···304 state
305 end else
306 let username = match state with
307+ | Authenticated { username; _ } -> Some username
308 | Selected { username; _ } -> Some username
309 | _ -> None
310 in
···360 (* Process STORE command *)
361 let handle_store t flow tag ~sequence ~silent ~action ~flags state =
362 match state with
363+ | Selected { username; mailbox; readonly; _ } ->
364 if readonly then begin
365 send_response flow (No { tag = Some tag; code = None; text = "Mailbox is read-only" });
366 state
···391 (* Process EXPUNGE command *)
392 let handle_expunge t flow tag state =
393 match state with
394+ | Selected { username; mailbox; readonly; _ } ->
395 if readonly then begin
396 send_response flow (No { tag = Some tag; code = None; text = "Mailbox is read-only" });
397 state
···416 (* Process CLOSE command *)
417 let handle_close t flow tag state =
418 match state with
419+ | Selected { username; mailbox; readonly; utf8_enabled } ->
420 (* Silently expunge if not readonly *)
421 if not readonly then
422 ignore (Storage.expunge t.storage ~username ~mailbox);
423 send_response flow (Ok { tag = Some tag; code = None; text = "CLOSE completed" });
424+ Authenticated { username; utf8_enabled }
425 | _ ->
426 send_response flow (Bad {
427 tag = Some tag;
···433 (* Process UNSELECT command *)
434 let handle_unselect flow tag state =
435 match state with
436+ | Selected { username; utf8_enabled; _ } ->
437 send_response flow (Ok { tag = Some tag; code = None; text = "UNSELECT completed" });
438+ Authenticated { username; utf8_enabled }
439 | _ ->
440 send_response flow (Bad {
441 tag = Some tag;
···452 state
453 end else
454 let username = match state with
455+ | Authenticated { username; _ } -> Some username
456 | Selected { username; _ } -> Some username
457 | _ -> None
458 in
···488 state
489 end else
490 let username = match state with
491+ | Authenticated { username; _ } -> Some username
492 | Selected { username; _ } -> Some username
493 | _ -> None
494 in
···525 state
526 end else
527 let username = match state with
528+ | Authenticated { username; _ } -> Some username
529 | Selected { username; _ } -> Some username
530 | _ -> None
531 in
···607 state
608 end else
609 match state with
610+ | Selected { username; mailbox = src_mailbox; readonly; _ } ->
611 if readonly then begin
612 send_response flow (No { tag = Some tag; code = None; text = "Mailbox is read-only" });
613 state
···645 });
646 state
647648+ (** Process SEARCH command.
649+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3
650+ After ENABLE UTF8=ACCEPT, SEARCH with CHARSET is rejected. *)
651+ let handle_search t flow tag ~charset ~criteria state =
652 match state with
653+ | Selected { username; mailbox; utf8_enabled; _ } ->
654+ (* RFC 6855 Section 3: After ENABLE UTF8=ACCEPT, reject SEARCH with CHARSET *)
655+ if utf8_enabled && Option.is_some charset then begin
656+ send_response flow (Bad {
657+ tag = Some tag;
658+ code = None;
659+ text = "CHARSET not allowed after ENABLE UTF8=ACCEPT"
660+ });
661+ state
662+ end else begin
663+ match Storage.search t.storage ~username ~mailbox ~criteria with
664+ | Result.Error _ ->
665+ send_response flow (No { tag = Some tag; code = None; text = "SEARCH failed" });
666+ state
667+ | Result.Ok uids ->
668+ (* Send ESEARCH response per RFC 9051 *)
669+ let results = if List.length uids > 0 then
670+ [Esearch_count (List.length uids); Esearch_all (List.map (fun uid -> Single (Int32.to_int uid)) uids)]
671+ else
672+ [Esearch_count 0]
673+ in
674+ send_response flow (Esearch { tag = Some tag; uid = false; results });
675+ send_response flow (Ok { tag = Some tag; code = None; text = "SEARCH completed" });
676+ state
677+ end
678 | _ ->
679 send_response flow (Bad {
680 tag = Some tag;
···683 });
684 state
685686+ (** Process THREAD command - RFC 5256.
687+688+ The THREAD command is used to retrieve message threads from a mailbox.
689+ It takes an algorithm, charset, and search criteria, returning threads
690+ of messages matching the criteria.
691+692+ Note: This is a basic stub implementation that returns empty threads.
693+ A full implementation would require:
694+ - ORDEREDSUBJECT: subject.ml for base subject extraction (RFC 5256 Section 2.1)
695+ - REFERENCES: Message-ID/In-Reply-To/References header parsing
696+697+ @see <https://datatracker.ietf.org/doc/html/rfc5256#section-3> RFC 5256 Section 3 *)
698+ let handle_thread _t flow tag ~algorithm ~charset:_ ~criteria:_ state =
699+ match state with
700+ | Selected { username = _; mailbox = _; _ } ->
701+ (* TODO: Implement actual threading algorithms.
702+ For now, return empty thread result.
703+ Full implementation would:
704+ 1. Search for messages matching criteria
705+ 2. Apply ORDEREDSUBJECT or REFERENCES algorithm
706+ 3. Build thread tree structure *)
707+ let _ = algorithm in (* Acknowledge the algorithm parameter *)
708+ send_response flow (Thread_response []);
709+ send_response flow (Ok { tag = Some tag; code = None; text = "THREAD completed" });
710+ state
711+ | _ ->
712+ send_response flow (Bad {
713+ tag = Some tag;
714+ code = None;
715+ text = "THREAD requires selected state"
716+ });
717+ state
718+719 (* Process APPEND command *)
720 let handle_append t flow tag ~mailbox ~flags ~date ~message state =
721 (* Security: Validate mailbox name *)
···724 state
725 end else
726 let username = match state with
727+ | Authenticated { username; _ } -> Some username
728 | Selected { username; _ } -> Some username
729 | _ -> None
730 in
···780 });
781 state
782783+ (** Process ENABLE command - RFC 5161, RFC 6855.
784+ After ENABLE UTF8=ACCEPT, the session accepts UTF-8 in quoted-strings.
785+ @see <https://datatracker.ietf.org/doc/html/rfc5161> RFC 5161: ENABLE Extension
786+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3 *)
787 let handle_enable flow tag ~capabilities state =
788 match state with
789+ | Authenticated { username; utf8_enabled } ->
790 (* Filter to capabilities we actually support *)
791 let enabled = List.filter (fun cap ->
792 let cap_upper = String.uppercase_ascii cap in
793 cap_upper = "IMAP4REV2" || cap_upper = "UTF8=ACCEPT"
794 ) capabilities in
795+ (* Check if UTF8=ACCEPT was requested and enabled *)
796+ let new_utf8_enabled = utf8_enabled || List.exists (fun cap ->
797+ String.uppercase_ascii cap = "UTF8=ACCEPT"
798+ ) enabled in
799 if List.length enabled > 0 then
800 send_response flow (Enabled enabled);
801 send_response flow (Ok { tag = Some tag; code = None; text = "ENABLE completed" });
802+ Authenticated { username; utf8_enabled = new_utf8_enabled }
803 | _ ->
804 send_response flow (Bad {
805 tag = Some tag;
···872 | Login { username; password } -> (handle_login t flow tag ~username ~password ~tls_active state, Continue)
873 | Select mailbox -> (handle_select t flow tag mailbox ~readonly:false state, Continue)
874 | Examine mailbox -> (handle_select t flow tag mailbox ~readonly:true state, Continue)
875+ | List list_cmd -> (handle_list t flow tag list_cmd state, Continue)
876 | Status { mailbox; items } -> (handle_status t flow tag mailbox ~items state, Continue)
877 | Fetch { sequence; items } -> (handle_fetch t flow tag ~sequence ~items state, Continue)
878 | Store { sequence; silent; action; flags } -> (handle_store t flow tag ~sequence ~silent ~action ~flags state, Continue)
···885 | Copy { sequence; mailbox } -> (handle_copy t flow tag ~sequence ~mailbox state, Continue)
886 | Move { sequence; mailbox } -> (handle_move t flow tag ~sequence ~mailbox state, Continue)
887 | Search { charset; criteria } -> (handle_search t flow tag ~charset ~criteria state, Continue)
888+ | Thread { algorithm; charset; criteria } -> (handle_thread t flow tag ~algorithm ~charset ~criteria state, Continue)
889 | Append { mailbox; flags; date; message } -> (handle_append t flow tag ~mailbox ~flags ~date ~message state, Continue)
890 | Namespace -> (handle_namespace flow tag state, Continue)
891 | Enable caps -> (handle_enable flow tag ~capabilities:caps state, Continue)
···910 | Authenticate _ ->
911 send_response flow (No { tag = Some tag; code = None; text = "Use LOGIN instead" });
912 (state, Continue)
913+ (* QUOTA extension - RFC 9208 *)
914+ | Getquota root ->
915+ (* GETQUOTA returns quota information for a quota root *)
916+ (* For now, return empty quota - storage backend would provide real data *)
917+ send_response flow (Quota_response { root; resources = [] });
918+ send_response flow (Ok { tag = Some tag; code = None; text = "GETQUOTA completed" });
919+ (state, Continue)
920+ | Getquotaroot mailbox ->
921+ (* GETQUOTAROOT returns the quota roots for a mailbox *)
922+ (* Typically the user's root is the quota root *)
923+ let roots = [mailbox] in (* Simplified: use mailbox as its own quota root *)
924+ send_response flow (Quotaroot_response { mailbox; roots });
925+ (* Also send QUOTA responses for each root *)
926+ List.iter (fun root ->
927+ send_response flow (Quota_response { root; resources = [] })
928+ ) roots;
929+ send_response flow (Ok { tag = Some tag; code = None; text = "GETQUOTAROOT completed" });
930+ (state, Continue)
931+ | Setquota { root; limits = _ } ->
932+ (* SETQUOTA is admin-only in most implementations *)
933+ send_response flow (No {
934+ tag = Some tag;
935+ code = Some Code_noperm;
936+ text = Printf.sprintf "Cannot set quota for %s" root
937+ });
938+ (state, Continue)
939940 (* Handle UID prefixed commands *)
941 and handle_uid_command t flow tag ~read_line_fn:_ uid_cmd state =
···954 | Uid_expunge _sequence ->
955 (* UID EXPUNGE only expunges messages in the given UID set *)
956 handle_expunge t flow tag state
957+ | Uid_thread { algorithm; charset; criteria } ->
958+ (* UID THREAD returns UIDs instead of sequence numbers *)
959+ handle_thread t flow tag ~algorithm ~charset ~criteria state
960961 (* Maximum line length to prevent DoS attacks via memory exhaustion.
962 RFC 9051 Section 4 recommends supporting lines up to 8192 octets. *)
···1113 text = "LOGIN completed"
1114 });
1115 (* Continue session as authenticated user *)
1116+ let state = Authenticated { username; utf8_enabled = false } in
1117 ignore (command_loop t flow state tls_active)
1118 end else begin
1119 (* Failed to drop privileges *)
+14-2
ocaml-imap/lib/imapd/storage.ml
···60 flags : list_flag list;
61}
62000000000063(* Storage backend signature *)
64module type STORAGE = sig
65 type t
···168 ensure_inbox user;
169 Hashtbl.fold (fun name _mb acc ->
170 if matches_pattern ~pattern name then
171- { name; delimiter = Some '/'; flags = [] } :: acc
0172 else acc
173 ) user.mailboxes []
174···700 let name = String.sub entry 1 (String.length entry - 1) in
701 let name = String.map (fun c -> if c = '.' then '/' else c) name in
702 if Memory_storage.matches_pattern ~pattern name then
703- { name; delimiter = Some '/'; flags = [] } :: acc
0704 else acc
705 else acc
706 else acc
···60 flags : list_flag list;
61}
6263+(** Get SPECIAL-USE flags for a mailbox based on its name (RFC 6154) *)
64+let get_special_use_for_mailbox name =
65+ match String.lowercase_ascii name with
66+ | "drafts" -> [List_drafts]
67+ | "sent" | "sent messages" | "sent items" -> [List_sent]
68+ | "trash" | "deleted messages" | "deleted items" -> [List_trash]
69+ | "junk" | "spam" -> [List_junk]
70+ | "archive" -> [List_archive]
71+ | _ -> []
72+73(* Storage backend signature *)
74module type STORAGE = sig
75 type t
···178 ensure_inbox user;
179 Hashtbl.fold (fun name _mb acc ->
180 if matches_pattern ~pattern name then
181+ let flags = get_special_use_for_mailbox name in
182+ { name; delimiter = Some '/'; flags } :: acc
183 else acc
184 ) user.mailboxes []
185···711 let name = String.sub entry 1 (String.length entry - 1) in
712 let name = String.map (fun c -> if c = '.' then '/' else c) name in
713 if Memory_storage.matches_pattern ~pattern name then
714+ let flags = get_special_use_for_mailbox name in
715+ { name; delimiter = Some '/'; flags } :: acc
716 else acc
717 else acc
718 else acc
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** UTF-8 validation per RFC 3629 for RFC 6855 IMAP UTF-8 support.
7+ @see <https://datatracker.ietf.org/doc/html/rfc6855> RFC 6855: IMAP Support for UTF-8
8+ @see <https://datatracker.ietf.org/doc/html/rfc3629> RFC 3629: UTF-8 encoding *)
9+10+(** Check if a string contains any non-ASCII characters (bytes >= 128). *)
11+let has_non_ascii s =
12+ let len = String.length s in
13+ let rec loop i =
14+ if i >= len then false
15+ else if Char.code s.[i] >= 128 then true
16+ else loop (i + 1)
17+ in
18+ loop 0
19+20+(** Validate UTF-8 encoding per RFC 3629 Section 4.
21+22+ UTF-8 encoding (RFC 3629):
23+ - 1-byte: 0xxxxxxx (U+0000..U+007F)
24+ - 2-byte: 110xxxxx 10xxxxxx (U+0080..U+07FF)
25+ - 3-byte: 1110xxxx 10xxxxxx 10xxxxxx (U+0800..U+FFFF)
26+ - 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (U+10000..U+10FFFF)
27+28+ Continuation bytes always have form 10xxxxxx.
29+30+ @see <https://datatracker.ietf.org/doc/html/rfc3629#section-4> RFC 3629 Section 4 *)
31+let is_valid_utf8 s =
32+ let len = String.length s in
33+ let rec loop i =
34+ if i >= len then true
35+ else
36+ let b0 = Char.code s.[i] in
37+ if b0 <= 0x7F then
38+ (* 1-byte sequence: ASCII *)
39+ loop (i + 1)
40+ else if b0 land 0xE0 = 0xC0 then begin
41+ (* 2-byte sequence: 110xxxxx 10xxxxxx *)
42+ if i + 1 >= len then false
43+ else
44+ let b1 = Char.code s.[i + 1] in
45+ (* Check continuation byte *)
46+ if b1 land 0xC0 <> 0x80 then false
47+ else
48+ (* Check for overlong encoding: must encode U+0080 or higher *)
49+ let codepoint = ((b0 land 0x1F) lsl 6) lor (b1 land 0x3F) in
50+ if codepoint < 0x80 then false
51+ else loop (i + 2)
52+ end
53+ else if b0 land 0xF0 = 0xE0 then begin
54+ (* 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx *)
55+ if i + 2 >= len then false
56+ else
57+ let b1 = Char.code s.[i + 1] in
58+ let b2 = Char.code s.[i + 2] in
59+ (* Check continuation bytes *)
60+ if b1 land 0xC0 <> 0x80 || b2 land 0xC0 <> 0x80 then false
61+ else
62+ let codepoint =
63+ ((b0 land 0x0F) lsl 12) lor
64+ ((b1 land 0x3F) lsl 6) lor
65+ (b2 land 0x3F)
66+ in
67+ (* Check for overlong encoding: must encode U+0800 or higher *)
68+ if codepoint < 0x800 then false
69+ (* Check for surrogate pairs (U+D800..U+DFFF are invalid) *)
70+ else if codepoint >= 0xD800 && codepoint <= 0xDFFF then false
71+ else loop (i + 3)
72+ end
73+ else if b0 land 0xF8 = 0xF0 then begin
74+ (* 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx *)
75+ if i + 3 >= len then false
76+ else
77+ let b1 = Char.code s.[i + 1] in
78+ let b2 = Char.code s.[i + 2] in
79+ let b3 = Char.code s.[i + 3] in
80+ (* Check continuation bytes *)
81+ if b1 land 0xC0 <> 0x80 ||
82+ b2 land 0xC0 <> 0x80 ||
83+ b3 land 0xC0 <> 0x80 then false
84+ else
85+ let codepoint =
86+ ((b0 land 0x07) lsl 18) lor
87+ ((b1 land 0x3F) lsl 12) lor
88+ ((b2 land 0x3F) lsl 6) lor
89+ (b3 land 0x3F)
90+ in
91+ (* Check for overlong encoding: must encode U+10000 or higher *)
92+ if codepoint < 0x10000 then false
93+ (* Check valid Unicode range: max is U+10FFFF *)
94+ else if codepoint > 0x10FFFF then false
95+ else loop (i + 4)
96+ end
97+ else
98+ (* Invalid start byte *)
99+ false
100+ in
101+ loop 0
102+103+(** Decode a single UTF-8 codepoint at position [i] in string [s].
104+ Returns the codepoint and the number of bytes consumed, or None if invalid.
105+ Assumes is_valid_utf8 has already passed. *)
106+let decode_codepoint s i =
107+ let len = String.length s in
108+ if i >= len then None
109+ else
110+ let b0 = Char.code s.[i] in
111+ if b0 <= 0x7F then
112+ Some (b0, 1)
113+ else if b0 land 0xE0 = 0xC0 && i + 1 < len then
114+ let b1 = Char.code s.[i + 1] in
115+ let cp = ((b0 land 0x1F) lsl 6) lor (b1 land 0x3F) in
116+ Some (cp, 2)
117+ else if b0 land 0xF0 = 0xE0 && i + 2 < len then
118+ let b1 = Char.code s.[i + 1] in
119+ let b2 = Char.code s.[i + 2] in
120+ let cp =
121+ ((b0 land 0x0F) lsl 12) lor
122+ ((b1 land 0x3F) lsl 6) lor
123+ (b2 land 0x3F)
124+ in
125+ Some (cp, 3)
126+ else if b0 land 0xF8 = 0xF0 && i + 3 < len then
127+ let b1 = Char.code s.[i + 1] in
128+ let b2 = Char.code s.[i + 2] in
129+ let b3 = Char.code s.[i + 3] in
130+ let cp =
131+ ((b0 land 0x07) lsl 18) lor
132+ ((b1 land 0x3F) lsl 12) lor
133+ ((b2 land 0x3F) lsl 6) lor
134+ (b3 land 0x3F)
135+ in
136+ Some (cp, 4)
137+ else
138+ None
139+140+(** Check if a codepoint is disallowed in mailbox names per RFC 6855 Section 3.
141+142+ Disallowed characters:
143+ - U+0000..U+001F: C0 control characters
144+ - U+007F: DELETE
145+ - U+0080..U+009F: C1 control characters
146+ - U+2028: LINE SEPARATOR
147+ - U+2029: PARAGRAPH SEPARATOR
148+149+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3
150+ @see <https://datatracker.ietf.org/doc/html/rfc5198#section-2> RFC 5198 Section 2 *)
151+let is_disallowed_mailbox_codepoint cp =
152+ (* C0 control characters U+0000..U+001F *)
153+ (cp >= 0x0000 && cp <= 0x001F) ||
154+ (* DELETE U+007F *)
155+ cp = 0x007F ||
156+ (* C1 control characters U+0080..U+009F *)
157+ (cp >= 0x0080 && cp <= 0x009F) ||
158+ (* LINE SEPARATOR U+2028 *)
159+ cp = 0x2028 ||
160+ (* PARAGRAPH SEPARATOR U+2029 *)
161+ cp = 0x2029
162+163+(** Validate a mailbox name for UTF-8 compliance per RFC 6855 Section 3.
164+165+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3 *)
166+let is_valid_utf8_mailbox_name s =
167+ (* First check basic UTF-8 validity *)
168+ if not (is_valid_utf8 s) then false
169+ else
170+ (* Then check for disallowed codepoints *)
171+ let len = String.length s in
172+ let rec loop i =
173+ if i >= len then true
174+ else
175+ match decode_codepoint s i with
176+ | None -> false
177+ | Some (cp, bytes) ->
178+ if is_disallowed_mailbox_codepoint cp then false
179+ else loop (i + bytes)
180+ in
181+ loop 0
+33
ocaml-imap/lib/imapd/utf8.mli
···000000000000000000000000000000000
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** UTF-8 validation per RFC 3629 for RFC 6855 IMAP UTF-8 support.
7+ @see <https://datatracker.ietf.org/doc/html/rfc6855> RFC 6855: IMAP Support for UTF-8
8+ @see <https://datatracker.ietf.org/doc/html/rfc3629> RFC 3629: UTF-8 encoding *)
9+10+(** {1 UTF-8 Validation} *)
11+12+val is_valid_utf8 : string -> bool
13+(** [is_valid_utf8 s] returns [true] if [s] contains only valid UTF-8 sequences
14+ per RFC 3629. Returns [true] for empty strings and pure ASCII strings.
15+ @see <https://datatracker.ietf.org/doc/html/rfc3629#section-4> RFC 3629 Section 4 *)
16+17+val has_non_ascii : string -> bool
18+(** [has_non_ascii s] returns [true] if [s] contains any bytes with value >= 128.
19+ This is useful for detecting when UTF-8 validation is needed. *)
20+21+(** {1 Mailbox Name Validation} *)
22+23+val is_valid_utf8_mailbox_name : string -> bool
24+(** [is_valid_utf8_mailbox_name s] validates a mailbox name for UTF-8 compliance
25+ per RFC 6855 Section 3. Mailbox names must:
26+ - Contain only valid UTF-8 sequences
27+ - Comply with Net-Unicode (RFC 5198 Section 2)
28+ - Not contain control characters U+0000-U+001F, U+0080-U+009F
29+ - Not contain delete U+007F
30+ - Not contain line separator U+2028 or paragraph separator U+2029
31+32+ @see <https://datatracker.ietf.org/doc/html/rfc6855#section-3> RFC 6855 Section 3
33+ @see <https://datatracker.ietf.org/doc/html/rfc5198#section-2> RFC 5198 Section 2 *)
+90-5
ocaml-imap/lib/imapd/write.ml
···350 | Unsubscribe mailbox ->
351 W.string w "UNSUBSCRIBE ";
352 astring w mailbox
353- | List { reference; pattern } ->
354 W.string w "LIST ";
355- astring w reference;
356- sp w;
357- astring w pattern
00000000000000000000000000000000000000000358 | Namespace -> W.string w "NAMESPACE"
359 | Status { mailbox; items } ->
360 W.string w "STATUS ";
···449 search_key w criteria
450 | Uid_expunge set ->
451 W.string w "EXPUNGE ";
452- sequence_set w set)
0000000000453 | Id params ->
454 W.string w "ID ";
455 id_params w params
0000000000000000000000000000000000456457let command w ~tag cmd =
458 atom w tag;
···350 | Unsubscribe mailbox ->
351 W.string w "UNSUBSCRIBE ";
352 astring w mailbox
353+ | List list_cmd ->
354 W.string w "LIST ";
355+ (match list_cmd with
356+ | List_basic { reference; pattern } ->
357+ astring w reference;
358+ sp w;
359+ astring w pattern
360+ | List_extended { selection; reference; patterns; return_opts } ->
361+ (* Selection options - RFC 5258 Section 3.1 *)
362+ W.char w '(';
363+ List.iteri (fun i opt ->
364+ if i > 0 then sp w;
365+ match opt with
366+ | List_select_subscribed -> W.string w "SUBSCRIBED"
367+ | List_select_remote -> W.string w "REMOTE"
368+ | List_select_recursivematch -> W.string w "RECURSIVEMATCH"
369+ | List_select_special_use -> W.string w "SPECIAL-USE"
370+ ) selection;
371+ W.char w ')';
372+ sp w;
373+ astring w reference;
374+ sp w;
375+ (* Patterns - multiple patterns in parentheses *)
376+ (match patterns with
377+ | [p] -> astring w p
378+ | ps ->
379+ W.char w '(';
380+ List.iteri (fun i p ->
381+ if i > 0 then sp w;
382+ astring w p
383+ ) ps;
384+ W.char w ')');
385+ (* Return options - RFC 5258 Section 3.2 *)
386+ (match return_opts with
387+ | [] -> ()
388+ | opts ->
389+ sp w;
390+ W.string w "RETURN (";
391+ List.iteri (fun i opt ->
392+ if i > 0 then sp w;
393+ match opt with
394+ | List_return_subscribed -> W.string w "SUBSCRIBED"
395+ | List_return_children -> W.string w "CHILDREN"
396+ | List_return_special_use -> W.string w "SPECIAL-USE"
397+ ) opts;
398+ W.char w ')'))
399 | Namespace -> W.string w "NAMESPACE"
400 | Status { mailbox; items } ->
401 W.string w "STATUS ";
···490 search_key w criteria
491 | Uid_expunge set ->
492 W.string w "EXPUNGE ";
493+ sequence_set w set
494+ | Uid_thread { algorithm; charset; criteria } ->
495+ W.string w "THREAD ";
496+ (match algorithm with
497+ | Thread_orderedsubject -> W.string w "ORDEREDSUBJECT"
498+ | Thread_references -> W.string w "REFERENCES"
499+ | Thread_extension ext -> astring w ext);
500+ sp w;
501+ astring w charset;
502+ sp w;
503+ search_key w criteria)
504 | Id params ->
505 W.string w "ID ";
506 id_params w params
507+ (* QUOTA extension - RFC 9208 *)
508+ | Getquota root ->
509+ W.string w "GETQUOTA ";
510+ astring w root
511+ | Getquotaroot mailbox ->
512+ W.string w "GETQUOTAROOT ";
513+ astring w mailbox
514+ | Setquota { root; limits } ->
515+ W.string w "SETQUOTA ";
516+ astring w root;
517+ sp w;
518+ W.char w '(';
519+ List.iteri (fun i (res, limit) ->
520+ if i > 0 then sp w;
521+ (match res with
522+ | Quota_storage -> W.string w "STORAGE"
523+ | Quota_message -> W.string w "MESSAGE"
524+ | Quota_mailbox -> W.string w "MAILBOX"
525+ | Quota_annotation_storage -> W.string w "ANNOTATION-STORAGE");
526+ sp w;
527+ W.string w (Int64.to_string limit)
528+ ) limits;
529+ W.char w ')'
530+ (* THREAD extension - RFC 5256 *)
531+ | Thread { algorithm; charset; criteria } ->
532+ W.string w "THREAD ";
533+ (match algorithm with
534+ | Thread_orderedsubject -> W.string w "ORDEREDSUBJECT"
535+ | Thread_references -> W.string w "REFERENCES"
536+ | Thread_extension ext -> astring w ext);
537+ sp w;
538+ astring w charset;
539+ sp w;
540+ search_key w criteria
541542let command w ~tag cmd =
543 atom w tag;
···82(** {1 Keyword Type} *)
8384module Keyword = struct
85+ (** Re-export core types from mail-flag.
86+ Note: mail-flag's [standard] type includes [`Deleted] (IMAP only)
87+ which is not part of JMAP's standard keywords. The JMAP standard
88+ keyword type below excludes [`Deleted] for JMAP compliance. *)
89+90+ (** RFC 8621 standard keywords (JMAP subset of mail-flag standard).
91+ This excludes [`Deleted] which is IMAP-only. *)
92 type standard = [
93 | `Seen
94 | `Flagged
···101 ]
102103 (** draft-ietf-mailmaint extended keywords *)
104+ type extended = Mail_flag.Keyword.extended
000000000000000105106 (** Apple Mail flag color keywords *)
107+ type flag_bits = Mail_flag.Keyword.flag_bit
0000108109+ (** Unified keyword type for JMAP.
110+ This is compatible with mail-flag's keyword type but excludes [`Deleted]. *)
111 type t = [
112 | standard
113 | extended
···115 | `Custom of string
116 ]
117118+ (** Convert from mail-flag keyword to JMAP keyword.
119+ [`Deleted] is converted to a custom keyword since JMAP doesn't support it. *)
120+ let of_mail_flag : Mail_flag.Keyword.t -> t = function
121+ | `Deleted -> `Custom "$deleted"
122+ | #t as k -> k
0000000000000000000000000123124+ (** Convert JMAP keyword to mail-flag keyword. *)
125+ let to_mail_flag (k : t) : Mail_flag.Keyword.t =
126+ (k :> Mail_flag.Keyword.t)
127+128+ let of_string s = of_mail_flag (Mail_flag.Keyword.of_string s)
129+130+ let to_string (k : t) = Mail_flag.Keyword.to_string (to_mail_flag k)
00000000000000000000000131132 let pp ppf k = Format.pp_print_string ppf (to_string k)
133134 (** Apple Mail flag colors *)
135+ type flag_color = Mail_flag.Keyword.flag_color
00000000136137 let flag_color_of_keywords (keywords : t list) : flag_color option =
138+ let mail_flag_keywords = List.map to_mail_flag keywords in
139+ Mail_flag.Keyword.flag_color_of_keywords mail_flag_keywords
00000000000140141+ let flag_color_to_keywords (color : flag_color) : t list =
142+ Mail_flag.Keyword.flag_color_to_keywords color
143+ |> List.map of_mail_flag
00000144end
145146(** {1 Mailbox Role Type} *)
147148module Role = struct
149+ (** Re-export special-use mailbox attributes from mail-flag as JMAP roles.
150+ JMAP roles correspond to the special_use subset of mailbox attributes. *)
151+152 (** RFC 8621 standard roles *)
153 type standard = [
154 | `Inbox
···170 | `Memos
171 ]
172173+ (** JMAP role type - corresponds to mail-flag's special_use type *)
174 type t = [
175 | standard
176 | extended
177 | `Custom of string
178 ]
179180+ (** Convert from mail-flag special_use to JMAP role *)
181+ let of_special_use : Mail_flag.Mailbox_attr.special_use -> t = function
182+ | `All -> `All
183+ | `Archive -> `Archive
184+ | `Drafts -> `Drafts
185+ | `Flagged -> `Flagged
186+ | `Important -> `Important
187+ | `Inbox -> `Inbox
188+ | `Junk -> `Junk
189+ | `Sent -> `Sent
190+ | `Subscribed -> `Subscribed
191+ | `Trash -> `Trash
192+ | `Snoozed -> `Snoozed
193+ | `Scheduled -> `Scheduled
194+ | `Memos -> `Memos
00195196+ (** Convert JMAP role to mail-flag special_use.
197+ Returns None for custom roles that don't map to special_use. *)
198+ let to_special_use : t -> Mail_flag.Mailbox_attr.special_use option = function
199+ | `All -> Some `All
200+ | `Archive -> Some `Archive
201+ | `Drafts -> Some `Drafts
202+ | `Flagged -> Some `Flagged
203+ | `Important -> Some `Important
204+ | `Inbox -> Some `Inbox
205+ | `Junk -> Some `Junk
206+ | `Sent -> Some `Sent
207+ | `Subscribed -> Some `Subscribed
208+ | `Trash -> Some `Trash
209+ | `Snoozed -> Some `Snoozed
210+ | `Scheduled -> Some `Scheduled
211+ | `Memos -> Some `Memos
212+ | `Custom _ -> None
213+214+ let of_string s =
215+ match Mail_flag.Mailbox_attr.of_jmap_role s with
216+ | Some special_use -> of_special_use special_use
217+ | None -> `Custom s
218+219+ let to_string : t -> string = function
220 | `Custom s -> s
221+ | #Mail_flag.Mailbox_attr.special_use as role ->
222+ (* safe because to_jmap_role returns Some for all special_use *)
223+ Option.get (Mail_flag.Mailbox_attr.to_jmap_role role)
224225 let pp ppf r = Format.pp_print_string ppf (to_string r)
226end