IMAP in OCaml

imap: use int64 for UIDs and UIDVALIDITY to handle large values

IMAP UIDs and UIDVALIDITY are unsigned 32-bit integers (RFC 9051),
which can exceed OCaml's signed int32 max value (2147483647).
Changed to int64 throughout to properly handle values up to 4294967295.

Fixes Int32.of_string failure when UIDVALIDITY value exceeds 2^31-1.

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

+62 -40
+22
CHANGES.json
··· 1 + { 2 + "repository": "ocaml-imap", 3 + "entries": [ 4 + { 5 + "week_start": "2026-01-12", 6 + "week_end": "2026-01-18", 7 + "summary": "Major library reorganization consolidates module structure and renames core modules for clarity.", 8 + "changes": [ 9 + "Reorganized lib/ from 11 subdirectories to 2: lib/imap/ and lib/imapd/", 10 + "Renamed Types module to Protocol for IMAP protocol definitions", 11 + "Renamed modules: imap_auth→Auth, imap_parser→Parser, imap_server→Server", 12 + "Renamed modules: imap_storage→Storage, imap_client→Client, imap_read→Read", 13 + "Access modules via new paths: Imapd.Protocol, Imapd.Server, etc." 14 + ], 15 + "commit_range": { 16 + "from": "ad8d14b", 17 + "to": "5eb9e22", 18 + "count": 3 19 + } 20 + } 21 + ] 22 + }
+3 -3
bin/imap_client.ml
··· 22 22 23 23 (** Email display record *) 24 24 type email_info = { 25 - uid : int32; 25 + uid : int64; 26 26 date : string; 27 27 subject : string; 28 28 from : string; ··· 95 95 String.sub email.subject 0 47 ^ "..." 96 96 else email.subject 97 97 in 98 - Printf.printf "%s %5ld | %s | %s | %s\n" 98 + Printf.printf "%s %5Ld | %s | %s | %s\n" 99 99 status email.uid padded_date padded_from subject_display 100 100 101 101 (** Main IMAP client function *) ··· 189 189 in 190 190 191 191 (* Sort by UID descending (most recent first) *) 192 - let sorted_emails = List.sort (fun a b -> Int32.compare b.uid a.uid) filtered_emails in 192 + let sorted_emails = List.sort (fun a b -> Int64.compare b.uid a.uid) filtered_emails in 193 193 194 194 Printf.printf "\n"; 195 195 Printf.printf " %-5s | %-20s | %-30s | %s\n" "UID" "Date" "From" "Subject";
+6 -6
lib/imap/client.ml
··· 22 22 name : string; 23 23 exists : int; 24 24 recent : int; 25 - uidvalidity : int32; 26 - uidnext : int32; 25 + uidvalidity : int64; 26 + uidnext : int64; 27 27 flags : Flag.t list; 28 28 permanent_flags : Flag.t list; 29 29 readonly : bool; ··· 31 31 32 32 type message_info = { 33 33 seq : int; 34 - uid : int32 option; 34 + uid : int64 option; 35 35 flags : Flag.t list option; 36 36 envelope : Envelope.t option; 37 37 body_structure : Body.t option; ··· 308 308 name = mailbox; 309 309 exists = 0; 310 310 recent = 0; 311 - uidvalidity = 0l; 312 - uidnext = 0l; 311 + uidvalidity = 0L; 312 + uidnext = 0L; 313 313 flags = []; 314 314 permanent_flags = []; 315 315 readonly; ··· 336 336 | _ -> "" 337 337 in 338 338 t.state <- Selected { username; mailbox; readonly = !info.readonly }; 339 - Log.info (fun f -> f "Mailbox %s: %d messages, uidnext=%ld, uidvalidity=%ld" 339 + Log.info (fun f -> f "Mailbox %s: %d messages, uidnext=%Ld, uidvalidity=%Ld" 340 340 mailbox !info.exists !info.uidnext !info.uidvalidity); 341 341 !info 342 342
+5 -5
lib/imap/client.mli
··· 36 36 name : string; 37 37 exists : int; 38 38 recent : int; 39 - uidvalidity : int32; 40 - uidnext : int32; 39 + uidvalidity : int64; 40 + uidnext : int64; 41 41 flags : Flag.t list; 42 42 permanent_flags : Flag.t list; 43 43 readonly : bool; ··· 46 46 47 47 type message_info = { 48 48 seq : int; 49 - uid : int32 option; 49 + uid : int64 option; 50 50 flags : Flag.t list option; 51 51 envelope : Envelope.t option; 52 52 body_structure : Body.t option; ··· 200 200 val expunge : t -> int list 201 201 val uid_expunge : t -> Seq.t -> int list 202 202 val search : t -> ?charset:string -> Search.t -> int list 203 - val uid_search : t -> ?charset:string -> Search.t -> int32 list 203 + val uid_search : t -> ?charset:string -> Search.t -> int64 list 204 204 205 205 val append : 206 206 t -> ··· 209 209 ?flags:Flag.t list -> 210 210 ?date:string -> 211 211 unit -> 212 - int32 option 212 + int64 option 213 213 214 214 (** {1 IDLE Support} *) 215 215
+8 -8
lib/imap/code.ml
··· 10 10 type t = 11 11 | Alert 12 12 | Alreadyexists 13 - | Appenduid of int32 * int32 (** uidvalidity, uid *) 13 + | Appenduid of int64 * int64 (** uidvalidity, uid *) 14 14 | Authenticationfailed 15 15 | Authorizationfailed 16 16 | Badcharset of string list ··· 19 19 | Clientbug 20 20 | Closed 21 21 | Contactadmin 22 - | Copyuid of int32 * Seq.t * Seq.t (** uidvalidity, source, dest *) 22 + | Copyuid of int64 * Seq.t * Seq.t (** uidvalidity, source, dest *) 23 23 | Corruption 24 24 | Expired 25 25 | Expungeissued ··· 37 37 | Serverbug 38 38 | Trycreate 39 39 | Uidnotsticky 40 - | Uidvalidity of int32 41 - | Uidnext of int32 40 + | Uidvalidity of int64 41 + | Uidnext of int64 42 42 | Unavailable 43 43 | Unknown_cte 44 44 | Other of string * string option ··· 46 46 let pp ppf = function 47 47 | Alert -> Fmt.string ppf "ALERT" 48 48 | Alreadyexists -> Fmt.string ppf "ALREADYEXISTS" 49 - | Appenduid (v, u) -> Fmt.pf ppf "APPENDUID %ld %ld" v u 49 + | Appenduid (v, u) -> Fmt.pf ppf "APPENDUID %Ld %Ld" v u 50 50 | Authenticationfailed -> Fmt.string ppf "AUTHENTICATIONFAILED" 51 51 | Authorizationfailed -> Fmt.string ppf "AUTHORIZATIONFAILED" 52 52 | Badcharset cs -> Fmt.pf ppf "BADCHARSET (%a)" Fmt.(list ~sep:sp string) cs ··· 56 56 | Closed -> Fmt.string ppf "CLOSED" 57 57 | Contactadmin -> Fmt.string ppf "CONTACTADMIN" 58 58 | Copyuid (v, s, d) -> 59 - Fmt.pf ppf "COPYUID %ld %a %a" v Seq.pp s Seq.pp d 59 + Fmt.pf ppf "COPYUID %Ld %a %a" v Seq.pp s Seq.pp d 60 60 | Corruption -> Fmt.string ppf "CORRUPTION" 61 61 | Expired -> Fmt.string ppf "EXPIRED" 62 62 | Expungeissued -> Fmt.string ppf "EXPUNGEISSUED" ··· 74 74 | Serverbug -> Fmt.string ppf "SERVERBUG" 75 75 | Trycreate -> Fmt.string ppf "TRYCREATE" 76 76 | Uidnotsticky -> Fmt.string ppf "UIDNOTSTICKY" 77 - | Uidvalidity v -> Fmt.pf ppf "UIDVALIDITY %ld" v 78 - | Uidnext u -> Fmt.pf ppf "UIDNEXT %ld" u 77 + | Uidvalidity v -> Fmt.pf ppf "UIDVALIDITY %Ld" v 78 + | Uidnext u -> Fmt.pf ppf "UIDNEXT %Ld" u 79 79 | Unavailable -> Fmt.string ppf "UNAVAILABLE" 80 80 | Unknown_cte -> Fmt.string ppf "UNKNOWN-CTE" 81 81 | Other (name, arg) ->
+4 -4
lib/imap/code.mli
··· 10 10 type t = 11 11 | Alert 12 12 | Alreadyexists 13 - | Appenduid of int32 * int32 (** uidvalidity, uid *) 13 + | Appenduid of int64 * int64 (** uidvalidity, uid *) 14 14 | Authenticationfailed 15 15 | Authorizationfailed 16 16 | Badcharset of string list ··· 19 19 | Clientbug 20 20 | Closed 21 21 | Contactadmin 22 - | Copyuid of int32 * Seq.t * Seq.t (** uidvalidity, source, dest *) 22 + | Copyuid of int64 * Seq.t * Seq.t (** uidvalidity, source, dest *) 23 23 | Corruption 24 24 | Expired 25 25 | Expungeissued ··· 37 37 | Serverbug 38 38 | Trycreate 39 39 | Uidnotsticky 40 - | Uidvalidity of int32 41 - | Uidnext of int32 40 + | Uidvalidity of int64 41 + | Uidnext of int64 42 42 | Unavailable 43 43 | Unknown_cte 44 44 | Other of string * string option
+2 -2
lib/imap/fetch.ml
··· 50 50 | Item_flags of Flag.t list 51 51 | Item_internaldate of string 52 52 | Item_rfc822_size of int64 53 - | Item_uid of int32 53 + | Item_uid of int64 54 54 | Item_body of Body.t 55 55 | Item_bodystructure of Body.t 56 56 | Item_body_section of { ··· 65 65 66 66 type message = { 67 67 seq : int; 68 - uid : int32 option; 68 + uid : int64 option; 69 69 flags : Flag.t list option; 70 70 envelope : Envelope.t option; 71 71 body_structure : Body.t option;
+2 -2
lib/imap/fetch.mli
··· 35 35 | Item_flags of Flag.t list 36 36 | Item_internaldate of string 37 37 | Item_rfc822_size of int64 38 - | Item_uid of int32 38 + | Item_uid of int64 39 39 | Item_body of Body.t 40 40 | Item_bodystructure of Body.t 41 41 | Item_body_section of { ··· 50 50 51 51 type message = { 52 52 seq : int; 53 - uid : int32 option; 53 + uid : int64 option; 54 54 flags : Flag.t list option; 55 55 envelope : Envelope.t option; 56 56 body_structure : Body.t option;
+3 -3
lib/imap/read.ml
··· 212 212 | "UIDNOTSTICKY" -> Code.Uidnotsticky 213 213 | "UIDVALIDITY" -> 214 214 sp r; 215 - Code.Uidvalidity (number32 r) 215 + Code.Uidvalidity (number64 r) 216 216 | "UIDNEXT" -> 217 217 sp r; 218 - Code.Uidnext (number32 r) 218 + Code.Uidnext (number64 r) 219 219 | "UNAVAILABLE" -> Code.Unavailable 220 220 | "UNKNOWN-CTE" -> Code.Unknown_cte 221 221 | other -> ··· 295 295 Fetch.Item_flags (flag_list r) 296 296 | "UID" -> 297 297 sp r; 298 - Fetch.Item_uid (number32 r) 298 + Fetch.Item_uid (number64 r) 299 299 | "RFC822.SIZE" -> 300 300 sp r; 301 301 Fetch.Item_rfc822_size (number64 r)
+4 -4
test/test_client.ml
··· 48 48 name = "INBOX"; 49 49 exists = 42; 50 50 recent = 3; 51 - uidvalidity = 1234567890l; 52 - uidnext = 43l; 51 + uidvalidity = 1234567890L; 52 + uidnext = 43L; 53 53 flags = [ Imap.Flag.System Imap.Flag.Seen; Imap.Flag.System Imap.Flag.Answered; Imap.Flag.System Imap.Flag.Flagged ]; 54 54 permanent_flags = [ Imap.Flag.System Imap.Flag.Seen; Imap.Flag.System Imap.Flag.Answered ]; 55 55 readonly = false; ··· 64 64 let info : Imap.Client.message_info = 65 65 { 66 66 seq = 1; 67 - uid = Some 12345l; 67 + uid = Some 12345L; 68 68 flags = Some [ Imap.Flag.System Imap.Flag.Seen ]; 69 69 envelope = 70 70 Some ··· 87 87 } 88 88 in 89 89 Alcotest.(check int) "seq" 1 info.seq; 90 - Alcotest.(check (option int32)) "uid" (Some 12345l) info.uid 90 + Alcotest.(check (option int64)) "uid" (Some 12345L) info.uid 91 91 92 92 let test_list_entry () = 93 93 let entry : Imap.Client.list_entry =
+3 -3
test/test_read.ml
··· 195 195 in 196 196 match result with 197 197 | Imap.Response.Ok { code = Some (Imap.Code.Uidvalidity v); _ } -> 198 - Alcotest.(check int32) "uidvalidity" 1234567890l v 198 + Alcotest.(check int64) "uidvalidity" 1234567890L v 199 199 | _ -> Alcotest.fail "expected UIDVALIDITY" 200 200 201 201 let test_response_uidnext () = ··· 203 203 with_reader "* OK [UIDNEXT 42] Next UID\r\n" (fun r -> Imap.Read.response r) 204 204 in 205 205 match result with 206 - | Imap.Response.Ok { code = Some (Imap.Code.Uidnext n); _ } -> Alcotest.(check int32) "uidnext" 42l n 206 + | Imap.Response.Ok { code = Some (Imap.Code.Uidnext n); _ } -> Alcotest.(check int64) "uidnext" 42L n 207 207 | _ -> Alcotest.fail "expected UIDNEXT" 208 208 209 209 let test_response_fetch_flags () = ··· 228 228 (function Imap.Fetch.Item_uid u -> Some u | _ -> None) 229 229 items 230 230 in 231 - Alcotest.(check (option int32)) "uid" (Some 12345l) uid 231 + Alcotest.(check (option int64)) "uid" (Some 12345L) uid 232 232 | _ -> Alcotest.fail "expected FETCH" 233 233 234 234 let test_response_fetch_multiple () =