package tiny_libs
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
From-scratch libraries for teaching: graphics, audio, compression, crypto, networking and more
Install
dune-project
Dependency
Authors
Maintainers
Sources
0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0
doc/src/tiny_libs.networking_mail/Mail_thread.ml.html
Source file Mail_thread.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193(* Claude Code * * Copyright (C) 2026 Yoann Padioleau * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * (LGPL) as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. *) (* See Mail_thread.mli *) type 'a tree = Node of 'a option * 'a tree list (* a container: the essay's, mutable as its are -- the links are made and broken as the messages come *) type 'a container = { mutable message : 'a option; mutable parent : 'a container option; mutable children : 'a container list } (*****************************************************************************) (* Subjects *) (*****************************************************************************) (* "Re:", "RE:", "Re[2]:", "Fwd:", "Fw:", "Aw:" (German), then spaces *) let strip_prefix (s : string) : string option = let s = String.trim s in let lower = String.lowercase_ascii s in let prefixes = [ "re"; "fwd"; "fw"; "aw" ] in List.find_map (fun p -> let n = String.length p in if String.length lower > n && String.sub lower 0 n = p then let rest = String.sub s n (String.length s - n) in (* "Re[2]:" *) let rest = if rest <> "" && rest.[0] = '[' then match String.index_opt rest ']' with Some i -> String.sub rest (i + 1) (String.length rest - i - 1) | None -> rest else rest in if rest <> "" && rest.[0] = ':' then Some (String.sub rest 1 (String.length rest - 1)) else None else None) prefixes let rec base_subject (s : string) : string = match strip_prefix s with Some rest -> base_subject rest | None -> String.lowercase_ascii (String.trim s) let is_reply (s : string) : bool = strip_prefix s <> None (*****************************************************************************) (* The algorithm *) (*****************************************************************************) (* is [a] [b] or one of its descendants? (a link from [b] down to [a] would make a loop) *) let rec below (a : 'a container) (b : 'a container) : bool = a == b || List.exists (fun c -> below a c) b.children let unlink (c : 'a container) : unit = match c.parent with | Some p -> p.children <- List.filter (fun x -> x != c) p.children; c.parent <- None | None -> () let link ~(parent : 'a container) (child : 'a container) : unit = if not (below parent child) then begin unlink child; child.parent <- Some parent; parent.children <- parent.children @ [ child ] end let threads ~(id : 'a -> string option) ~(references : 'a -> string list) ~(subject : 'a -> string) ~(date : 'a -> float) (messages : 'a list) : 'a tree list = let table : (string, 'a container) Hashtbl.t = Hashtbl.create 64 in let all = ref [] in let fresh () = let c = { message = None; parent = None; children = [] } in all := c :: !all; c in let container_of (i : string) : 'a container = match Hashtbl.find_opt table i with | Some c -> c | None -> let c = fresh () in Hashtbl.replace table i c; c in (* 1. the containers, and the links *) List.iter (fun m -> let c = match id m with | Some i -> ( match Hashtbl.find_opt table i with | Some c when c.message = None -> c | Some _ -> fresh () (* the same id twice: kept apart *) | None -> container_of i) | None -> fresh () in c.message <- Some m; let refs = List.filter (fun r -> Some r <> id m) (references m) in let chain = List.map container_of refs in (* each reference the child of the one before, unless it has a parent already *) let rec pairs = function | a :: (b :: _ as rest) -> if b.parent = None && not (below a b) then link ~parent:a b; pairs rest | _ -> () in pairs chain; (* the message under the last of them: its own word wins *) match List.rev chain with | last :: _ when not (below last c) -> link ~parent:last c | _ -> unlink c) messages; (* 2. the roots *) let roots = List.filter (fun c -> c.parent = None) (List.rev !all) in (* 3. the empty containers pruned *) let rec prune ~(top : bool) (cs : 'a container list) : 'a container list = List.concat_map (fun c -> c.children <- prune ~top:false c.children; List.iter (fun k -> k.parent <- Some c) c.children; match (c.message, c.children) with | None, [] -> [] | None, [ only ] when top -> [ only ] | None, kids when not top -> kids | _ -> [ c ]) cs in let roots = prune ~top:true roots in List.iter (fun c -> c.parent <- None) roots; (* 4. the roots grouped by subject *) let subject_of (c : 'a container) : string = match c.message with Some m -> subject m | None -> ( match c.children with k :: _ -> Option.fold ~none:"" ~some:subject k.message | [] -> "") in let by_subject : (string, 'a container) Hashtbl.t = Hashtbl.create 64 in List.iter (fun c -> let s = base_subject (subject_of c) in if s <> "" then match Hashtbl.find_opt by_subject s with | None -> Hashtbl.replace by_subject s c | Some old -> (* the better representative: an empty one, or one that is not a reply *) if (c.message = None && old.message <> None) || (is_reply (subject_of old) && not (is_reply (subject_of c))) then Hashtbl.replace by_subject s c) roots; let merged = List.filter (fun c -> let s = base_subject (subject_of c) in match Hashtbl.find_opt by_subject s with | Some rep when rep != c && s <> "" -> ( match (rep.message, c.message) with | None, None -> rep.children <- rep.children @ c.children; false | None, Some _ -> link ~parent:rep c; false | Some _, Some m when is_reply (subject m) && not (is_reply (subject_of rep)) -> link ~parent:rep c; false | Some _, Some _ -> (* two of a kind: both under a new empty container, which takes [rep]'s place among the roots -- made by moving [rep]'s message and replies down into a new one *) let moved = { message = rep.message; parent = Some rep; children = rep.children } in List.iter (fun k -> k.parent <- Some moved) moved.children; rep.message <- None; rep.children <- [ moved ]; link ~parent:rep c; false | Some _, None -> link ~parent:rep c; false) | _ -> true) roots in (* 5. brothers by date; an empty container dated by its first child *) let rec to_tree (c : 'a container) : 'a tree = Node (c.message, sort c.children) and when_ (c : 'a container) : float = match c.message with Some m -> date m | None -> List.fold_left (fun t k -> min t (when_ k)) infinity c.children and sort (cs : 'a container list) : 'a tree list = List.map to_tree (List.stable_sort (fun a b -> compare (when_ a) (when_ b)) cs) in sort merged let rec flatten_at (depth : int) (ts : 'a tree list) : ('a * int) list = List.concat_map (fun (Node (m, kids)) -> (match m with Some m -> [ (m, depth) ] | None -> []) @ flatten_at (depth + 1) kids) ts let flatten (ts : 'a tree list) : ('a * int) list = flatten_at 0 ts let of_mail (mail : 'a -> Mail.t) (messages : 'a list) : 'a tree list = let get m name = Option.value (Mail.get (mail m) name) ~default:"" in let references m = let refs = Mail.message_ids (get m "references") in let irt = Mail.message_ids (get m "in-reply-to") in refs @ List.filter (fun i -> not (List.mem i refs)) irt in threads ~id:(fun m -> List.nth_opt (Mail.message_ids (get m "message-id")) 0) ~references ~subject:(fun m -> Mime.decode_words (get m "subject")) ~date:(fun m -> match Mail.date (get m "date") with Some d -> Mail.seconds d | None -> 0.) messages
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>