About half way through switching cocanwiki to using the new PG interface.
[cocanwiki.git] / scripts / lib / cocanwiki_mail.ml
1 (* COCANWIKI - a wiki written in Objective CAML.
2  * Written by Richard W.M. Jones <rich@merjis.com>.
3  * Copyright (C) 2004 Merjis Ltd.
4  * $Id: cocanwiki_mail.ml,v 1.2 2006/03/27 16:43:44 rich Exp $
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; see the file COPYING.  If not, write to
18  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  *)
21
22 open Printf
23
24 open ExtString
25
26 open Cocanwiki_pages
27 open Cocanwiki_template
28 open Cocanwiki_date
29
30 (* Given a subject line, return the "base" subject.
31  * eg. "Re: my message" -> "my message"
32  *)
33 let re_re = Pcre.regexp "^Re\\[\\d+\\]:\\s*"
34
35 let base_subject subject =
36   let rec loop subject =
37     let n = String.length subject in
38     if String.starts_with subject "Re: " then
39       loop (String.sub subject 4 (n-4))
40     else if String.starts_with subject "Re:" then
41       loop (String.sub subject 3 (n-3))
42     else if String.starts_with subject "RE: " then
43       loop (String.sub subject 4 (n-4))
44     else if String.starts_with subject "RE:" then
45       loop (String.sub subject 3 (n-3))
46     else if Pcre.pmatch ~rex:re_re subject then (
47       let subs = Pcre.exec ~rex:re_re subject in
48       let i = String.length (Pcre.get_substring subs 0) in
49       loop (String.sub subject i (n-i))
50     ) else
51       subject
52   in
53   let base_subject = loop subject in
54   let is_reply = base_subject <> subject in
55   base_subject, is_reply
56
57 (* This abstract data type represents a 'forest' and is used for
58  * the implementation of threading below.
59  *)
60 module type FOREST =
61 sig
62   type 'a t
63   exception Already_linked
64   exception Cycle_found
65   val create : int -> 'a t
66   val add : 'a t -> 'a -> unit
67   val link : 'a t -> 'a -> 'a -> unit
68   val unlink : 'a t -> 'a -> unit
69   val get_roots : 'a t -> 'a list
70   val get_children : 'a t -> 'a -> 'a list
71 end
72
73 module Forest : FOREST =
74 struct
75   type 'a node_data = {
76     level : int;
77     parent : 'a option;
78     children : 'a list;
79     root : 'a;
80   }
81
82   type 'a t = ('a, 'a node_data) Hashtbl.t
83
84   exception Already_linked
85   exception Cycle_found
86
87   let create n = Hashtbl.create n
88
89   (* Add node [n] to forest [f]. *)
90   let add f n =
91     Hashtbl.replace f n { level = 0;
92                           parent = None;
93                           children = [];
94                           root = n }
95
96   (* Set the level field of [n]'s children to increasing
97    * values, starting with [lvl].  Point all the root
98    * fields of the children to [rt].
99    *)
100   let rec update f lvl rt n =
101     let n_data = Hashtbl.find f n in
102     Hashtbl.replace f n { n_data with level = lvl; root = rt };
103     List.iter (update f (lvl+1) rt) n_data.children
104
105   (* Link child [na] to parent [nb].  Raises [Already_linked]
106    * if either [na] has a parent already.  Raises [Cycle_found]
107    * if the link would introduce a cycle.
108    *)
109   let link f na nb =
110     let na_data = Hashtbl.find f na in
111     let nb_data = Hashtbl.find f nb in
112     match na_data.parent with
113       | Some _ -> raise Already_linked
114       | None when nb_data.root = na -> raise Cycle_found
115       | _ ->
116           Hashtbl.replace f na { na_data with parent = Some nb };
117           let nb_data = { nb_data with children = na :: nb_data.children } in
118           Hashtbl.replace f nb nb_data;
119           update f (nb_data.level+1) nb_data.root na
120
121   (* Remove the parent link of [n].  If there is no such
122    * link, does nothing.
123    *)
124   let unlink f n =
125     let n_data = Hashtbl.find f n in
126     match n_data.parent with
127       | None -> ()
128       | Some p ->
129           Hashtbl.replace f n { n_data with parent = None };
130           let p_data = Hashtbl.find f p in
131           Hashtbl.replace f p
132             { p_data with
133                 children = List.filter ((!=) n) p_data.children};
134           update f 0 n n
135
136   (* Return the roots in forest [f]. *)
137   let get_roots f =
138     let save_if_root n n_data roots =
139       match n_data.parent with
140         | Some _ -> roots
141         | None -> n :: roots in
142     Hashtbl.fold save_if_root f []
143
144   (* Return [n]'s children. *)
145   let get_children f n =
146     let n_data = Hashtbl.find f n in
147     n_data.children
148 end
149
150 type message =
151     { id : int32;
152       inet_message_id : string;
153       references : string list;
154       subject : string;
155       base_subject : string;
156       is_reply : bool;
157       message_date : PGOCaml.timestamptz }
158
159 type tree = Tree of message option * tree list
160
161 (* Rebuild mail threads for (year, month).
162  * The algorithm was originally by JWZ, http://www.jwz.org/doc/threading.html,
163  * simplified and implemented by Radu Grigore <radugrigore@yahoo.com>.
164  *)
165 let thread_mail dbh hostid ?user ?r year month =
166   (* Pull out all the emails relevant to this month. *)
167   let rows =
168     let year = Int32.of_int year in
169     let month = Int32.of_int month in
170     PGSQL(dbh)
171       "select id, subject, inet_message_id, message_date
172          from messages
173         where hostid = $hostid
174           and extract (year from message_date) :: int = $year
175           and extract (month from message_date) :: int = $month" in
176
177   let msgs =
178     List.map (
179       fun (id, subject, inet_message_id, message_date) ->
180         id, (inet_message_id, subject, message_date)
181     ) rows in
182
183   let references =
184     if msgs <> [] then (
185       let ids = List.map fst msgs in
186       let rows =
187         PGSQL(dbh)
188           "select message_id, inet_message_id, ordering
189              from msg_references
190             where message_id in $@ids
191             order by message_id, ordering" in
192       List.map (
193         fun (id, inet_message_id, _) ->
194           id, inet_message_id
195       ) rows
196     ) else [] in
197
198   (* Aggregate the msgs and references structures together.
199    * Note that references will be in the correct order (because of the
200    * 'order by' clause in the select statement above), with the parent
201    * message appearing first in the list.
202    *)
203   let msgs =
204     List.map (fun (id, (inet_message_id, subject, message_date)) ->
205                 let references =
206                   List.filter (fun (i, _) -> i = id) references in
207                 let references = List.map snd references in
208                 id, inet_message_id, references, subject, message_date)
209       msgs in
210
211   (* Get the base subject lines (removing Re:, etc.), and convert to
212    * list of message structs.
213    *)
214   let msgs =
215     List.map (fun (id, inet_message_id, references, subject, message_date) ->
216                 let base_subject, is_reply = base_subject subject in
217                 { id = id; inet_message_id = inet_message_id;
218                   references = references; subject = subject;
219                   base_subject = base_subject; is_reply = is_reply;
220                   message_date = message_date }) msgs in
221
222   (* Create a hash of inet_message_id -> message structure, which we'll
223    * need later.
224    *)
225   let msgmap =
226     let h = Hashtbl.create (List.length msgs) in
227     let add ({ inet_message_id = inet_message_id } as message) =
228       Hashtbl.replace h inet_message_id message in
229     List.iter add msgs;
230     h in
231
232   (* Step 1: Build the forest. *)
233   let forest = Forest.create 1024 in
234   List.iter
235     (fun { inet_message_id = inet_message_id; references = references } ->
236        Forest.add forest inet_message_id;
237        List.iter (Forest.add forest) references) msgs;
238
239   let add_msg_data f { inet_message_id = inet_message_id;
240                        references = references } =
241     let rec add_one f n lst =
242       match lst with
243         | [] -> ()
244         | h :: t ->
245             (try Forest.link f n h
246              with Forest.Already_linked | Forest.Cycle_found -> ());
247             add_one f h t
248     in
249     match references with
250       | [] -> ()
251       | h :: t ->
252           Forest.unlink f inet_message_id;
253           Forest.link f inet_message_id h;
254           add_one f h t
255   in
256   List.iter (add_msg_data forest) msgs;
257
258   (* Step 2: Find the root set.  Convert the forest into an ordinary tree
259    * structure now (actually, a list of tree structures) since the FOREST
260    * type is no longer needed.
261    *)
262   let threads = Forest.get_roots forest in
263
264   let threads =
265     let rec make_tree root =
266       (* Is there a message associated with this inet_message_id? *)
267       let message =
268         try Some (Hashtbl.find msgmap root) with Not_found -> None in
269       (* Get the children. *)
270       let children = Forest.get_children forest root in
271       let children = List.map make_tree children in
272       Tree (message, children)
273     in
274     List.map make_tree threads in
275
276   (* Step 4A: Prune empty containers. *)
277   let threads =
278     let prune = List.filter (function Tree (None, []) -> false | _ -> true) in
279     let rec loop (Tree (message, children)) =
280       let children = prune children in
281       let children = List.map loop children in
282       Tree (message, children)
283     in
284     List.map loop threads in
285
286   (* Step 4B: Promote children of (some) empty containers. *)
287   let threads =
288     (* Below the top level there should be no empty containers after
289      * this.  Any empty container with children has those children
290      * promoted up.
291      *)
292     let rec promote = function
293         [] -> []
294       | Tree (None, children) :: xs ->
295           let children = promote children in
296           children @ promote xs
297       | Tree (message, children) :: xs ->
298           let children = promote children in
299           Tree (message, children) :: promote xs
300     in
301     let threads = List.map (fun (Tree (message, children)) ->
302                               let children = promote children in
303                               Tree (message, children)) threads in
304
305     (* At the top level we're allowed to have empty containers.  However
306      * if we have an empty container with just a single child, then
307      * promote that child.
308      *)
309     let threads = List.map (function
310                                 Tree (None, [child]) -> child
311                               | message -> message) threads in
312
313     threads in
314
315   (* Step 5: Group root set by subject. *)
316   (* Couldn't be arsed to implement this.  If someone really cares about
317    * mailers which don't set References headers (probably some made by
318    * our friends at Microsoft, I wouldn't mind betting), then send me
319    * a patch ... XXX
320    *)
321
322   (* Step 7: Sort the siblings into date order. *)
323   let threads =
324     let compare (Tree (m1, _)) (Tree (m2, _)) =
325       let md1 = match m1 with
326           Some { message_date = message_date } -> Some message_date
327         | None -> None in
328       let md2 = match m2 with
329           Some { message_date = message_date } -> Some message_date
330         | None -> None in
331       compare md1 md2
332     in
333     let rec sort ms =
334       let ms = List.sort compare ms in
335       List.map (fun (Tree (message, children)) ->
336                   Tree (message, sort children)) ms
337     in
338     sort threads in
339
340   (*----- End of threading algorithm. -----*)
341
342   let title = sprintf "Mail/%04d/%02d/Thread Index" year month in
343   let url =
344     match Wikilib.generate_url_of_title dbh hostid title with
345         Wikilib.GenURL_OK url -> url
346       | Wikilib.GenURL_Duplicate url -> url
347       | Wikilib.GenURL_TooShort | Wikilib.GenURL_BadURL ->
348           failwith ("error generating URL for title: " ^ title) in
349
350   let template = _get_template "mail_thread.txt" in
351
352   (* Rebuild the thread index page. *)
353   let model =
354     try load_page dbh hostid ~url ()
355     with Not_found -> new_page (Title title) in
356
357   let first_section =
358     let sectionname =
359       sprintf "Thread index for %s %04d"
360         (!Printer.month_name (Date.month_of_int month)) year in
361     let content =
362       template#set "year" (string_of_int year);
363       template#set "month" (sprintf "%02d" month);
364       template#set "long_month"
365         (!Printer.month_name (Date.month_of_int month));
366       let prev_year, prev_month =
367         if month = 1 then year - 1, 12
368         else year, month - 1 in
369       template#set "prev_year" (string_of_int prev_year);
370       template#set "prev_month" (sprintf "%02d" prev_month);
371       let next_year, next_month =
372         if month = 12 then year + 1, 1
373         else year, month + 1 in
374       template#set "next_year" (string_of_int next_year);
375       template#set "next_month" (sprintf "%02d" next_month);
376
377       let rec markup threads =
378         let f = function
379           | Tree (None, children) ->
380               let html = markup children in
381               "<li> -\n" :: html @ ["</li>\n"]
382           | Tree (Some message, children) ->
383               let {id = id; subject = subject} = message in
384
385               let url =
386                 let title = sprintf "Mail/%s (%ld)" subject id in
387                 match Wikilib.generate_url_of_title dbh hostid title with
388                     Wikilib.GenURL_OK url | Wikilib.GenURL_Duplicate url -> url
389                   | Wikilib.GenURL_TooShort | Wikilib.GenURL_BadURL ->
390                       failwith ("error finding URL for message: " ^ title) in
391
392               let html = markup children in
393               sprintf "<li> <a href=\"/%s\" class=\"internal\">%s</a>\n"
394                 url (Cgi_escape.escape_html subject)
395               :: html @ [ "</li>\n" ]
396         in
397         "<ul>\n" :: List.concat (List.map f threads) @ ["</ul>\n"]
398       in
399       let html = markup threads in
400       let html = String.concat "" html in
401       template#set "threads" html;
402
403       template#to_string
404     in
405     (Some sectionname, None, content)
406   in
407
408   let contents =
409     match model.contents_ with
410       | [] | [_] -> [ first_section ]
411       | x :: xs -> first_section :: xs in
412
413   let model = { model with contents_ = contents } in
414
415   (* Save the page. *)
416   try
417     ignore (save_page dbh hostid ?user ?r model)
418   with
419     | SaveURLError ->
420         failwith "cocanwiki_mail: thread_mail: unexpected SaveURLError"
421
422       (* The following error should be noted, but is not too bad.  We
423        * expect to rebuild the thread indexes frequently, so hopefully
424        * the next time it is rebuilt it will succeed.
425        *)
426     | SaveConflict _ ->
427         prerr_endline "cocanwiki_mail: thread_mail: SaveConflict (ignored)"