Changes done on the Mac.
[cocanwiki.git] / scripts / mail_import.ml
index dce9f5a..f609062 100644 (file)
@@ -1,7 +1,7 @@
 (* COCANWIKI - a wiki written in Objective CAML.
  * Written by Richard W.M. Jones <rich@merjis.com>.
  * Copyright (C) 2004 Merjis Ltd.
- * $Id: mail_import.ml,v 1.1 2004/10/11 14:13:04 rich Exp $
+ * $Id: mail_import.ml,v 1.11 2006/03/28 13:20:00 rich Exp $
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -28,16 +28,22 @@ open Netmime
 open Netchannels
 open Netstream
 
+open ExtString
+
 open Cocanwiki
 open Cocanwiki_ok
 open Cocanwiki_template
 open Cocanwiki_date
+open Cocanwiki_strings
 open Cocanwiki_pages
+open Cocanwiki_mail
 
 let irt_re = Pcre.regexp "<.*?>"
-let ws_re = Pcre.regexp "\\S+"
+let ws_re = Pcre.regexp "\\s+"
+let comma_re = Pcre.regexp "\\s*,\\s*"
+let lines_re = Pcre.regexp "\\r?\\n"
 
-let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
+let run r (q : cgi) dbh hostid _ user =
   let hdr_template = get_template dbh hostid "mail_import_header.txt" in
 
   (* Overwrite old messages? *)
@@ -52,10 +58,8 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
       upload.upload_value
     with
        Not_found ->
-         (* Force an error status which a script can detect. *)
-         Request.set_status r cHTTP_BAD_REQUEST;
          error ~back_button:true ~title:"No message"
-           q "No message was uploaded.";
+           dbh hostid q "No message was uploaded.";
          return () in
 
   (* Parse the message. *)
@@ -81,29 +85,32 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
    * cannot thread it, so give up.
    *)
   if date = "" || inet_message_id = "" then (
-    Request.set_status r cHTTP_BAD_REQUEST;
     error ~back_button:true ~title:"Headers missing"
-      q "Date or Message-ID header missing.  Cannot handle this message. ";
+      dbh hostid q
+      "Date or Message-ID header missing.  Cannot handle this message. ";
     return ()
   );
 
+  (* Can't handle funny characters in subject lines - remove them. *)
+  let subject = String.map (fun c ->
+                             if Char.code c < 32 then ' ' else c) subject in
+
   (* Parse the date field. *)
-  let date, time =
+  let message_date =
     try
       let date = Netdate.parse date in
-      let date, time =
-       { Dbi.year = date.Netdate.year;
-         Dbi.month = date.Netdate.month;
-         Dbi.day = date.Netdate.day; },
-       { Dbi.hour = date.Netdate.hour;
-         Dbi.min = date.Netdate.minute;
-         Dbi.sec = date.Netdate.second;
-         Dbi.microsec = 0;
-         Dbi.timezone = Some (date.Netdate.zone / 60); } in
-      date, time
+      let cal = Calendar.make
+       date.Netdate.year
+       date.Netdate.month
+       date.Netdate.day
+       date.Netdate.hour
+       date.Netdate.minute
+       date.Netdate.second in
+      let tz = Time_Zone.UTC_Plus (date.Netdate.zone / 60) in
+      cal, tz
     with
-       Invalid_argument _ ->
-         failwith ("cannot parse date: " ^ date) in
+      Invalid_argument _ ->
+       failwith ("cannot parse date: " ^ date) in
 
   (* Find the first thing in the In-Reply-To field which looks like a
    * message ID.
@@ -141,41 +148,38 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
    * else 'None' if this is a never-seen-before message.
    *)
   let overwrite =
-    let sth = dbh#prepare_cached "select id from messages
-                                   where hostid = ? and inet_message_id = ?" in
-    sth#execute [`Int hostid; `String inet_message_id];
-    try
-      let id = sth#fetch1int () in
-      if not overwrite then (
-       ok ~title:"Message exists"
-          q "Message already imported";
-       return ()
-      );
-      Some id
-    with
-       Not_found -> None in
+    let rows = PGSQL(dbh)
+      "select id from messages
+        where hostid = $hostid and inet_message_id = $inet_message_id" in
+    match rows with
+    | [id] ->
+       if not overwrite then (
+         ok ~title:"Message exists"
+            dbh hostid q "Message already imported";
+         return ()
+       );
+       Some id
+    | [] -> None
+    | _ -> assert false in
 
   (* Save all of this in the database. *)
   let msgid =
     match overwrite with
        None ->                         (* Never-seen-before message. *)
-         let sth =
-           dbh#prepare_cached
-             "insert into messages (hostid, subject, inet_message_id,
-                 message_date) values (?, ?, ?, ?)" in
-         sth#execute [`Int hostid; `String subject; `String inet_message_id;
-                      `Timestamp (date, time)];
-         let msgid = sth#serial "messages_id_seq" in
-
-         let sth =
-           dbh#prepare_cached
-             "insert into msg_references (message_id, inet_message_id,
-                 ordering) values (?, ?, ?)" in
+         PGSQL(dbh)
+           "insert into messages (hostid, subject, inet_message_id,
+               message_date)
+             values ($hostid, $subject, $inet_message_id, $message_date)";
+         let msgid = PGOCaml.serial4 dbh "messages_id_seq" in
+
          let ordering = ref 0 in
          List.iter (fun inet_message_id ->
-                      incr ordering; let ordering = !ordering in
-                      sth#execute [`Int msgid; `String inet_message_id;
-                                   `Int ordering]) references;
+                      incr ordering; let ordering = Int32.of_int !ordering in
+                      PGSQL(dbh)
+                        "insert into msg_references (message_id,
+                            inet_message_id, ordering)
+                          values ($msgid, $inet_message_id, $ordering)"
+                   ) references;
 
          msgid
 
@@ -185,7 +189,10 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
           *)
          msgid in
 
-  (* The message is referred to by a unique title: *)
+  (* The message is referred to by a unique title.
+   * NB. Do not change this unique title - it is also used during thread
+   * indexing.
+   *)
   let title = sprintf "Mail/%s (%d)" subject msgid in
 
   (* Choose a suitable URL. *)
@@ -215,20 +222,113 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
     let content =
       (* XXX Escaping! *)
       hdr_template#set "subject" subject;
+      hdr_template#set "from" from;
+      hdr_template#set "inet_message_id" inet_message_id;
+
       let yyyy, mm, dd = date.Dbi.year, date.Dbi.month, date.Dbi.day in
       hdr_template#set "yyyy" (sprintf "%04d" yyyy);
       hdr_template#set "mm" (sprintf "%02d" mm);
       hdr_template#set "dd" (sprintf "%02d" dd);
       hdr_template#set "short_month" (short_month mm);
-      hdr_template#set "from" from;
-      hdr_template#set "inet_message_id" inet_message_id;
+
+      let get_table hdr =
+       List.map (fun addr -> [ "addr", Template.VarString addr ])
+         (Pcre.split ~rex:comma_re hdr)
+      in
+      let table = get_table to_hdr in
+      hdr_template#table "to" table;
+      hdr_template#conditional "has_to" (table <> []);
+      let table = get_table cc in
+      hdr_template#table "cc" table;
+      hdr_template#conditional "has_cc" (table <> []);
+
       hdr_template#to_string
     in
     "", "mail_header", content in
 
-  (* Create the second section (mail body). *)
+  (* Create the second section (mail body).
+   * XXX Very simple.  Should be extended to understand attachments and
+   * convert them into file or image uploads.
+   *)
   let section1 =
-    let content = "(mail body should go here XXX)" in
+    let is_text_plain hdr =
+      try
+       let ct = hdr#field "content-type" in
+       String.starts_with ct "text/plain"
+      with
+         Not_found -> true in
+
+    (* Find the first text/plain body. *)
+    let rec find_body = function
+       (header, `Body mime_body) when is_text_plain header ->
+         mime_body#value
+      | (_, `Body _) -> raise Not_found
+      | (_, `Parts []) -> raise Not_found (* should never happen *)
+      | (_, `Parts (m :: _)) ->
+         find_body m in
+
+    (* Markup a plain text body. *)
+    let markup_body text =
+      (* Get all the titles from the database! We're going to exclude
+       * mail messages from this.
+       *)
+      let links =
+       PGSQL(dbh)
+         "select lower (title) from pages where hostid = $hostid
+              and url is not null and title not like 'Mail/%'" in
+      let links = List.map Option.get links in
+
+      (* This code cannot find titles which are split across multiple lines.
+       * XXX
+       *)
+      let lines = Pcre.split ~rex:lines_re text in
+
+      (* We don't want to mark up the same link twice, so keep track of the
+       * titles we've already used.
+       *)
+      let used = ref [] in
+      let lines =
+       List.map
+         (fun line ->
+            let find str sub_lc =
+              try
+                String.find (String.lowercase str) sub_lc
+              with
+                  Invalid_string -> -1
+            in
+            let rec loop line = function
+                [] -> line
+              | link :: links ->
+                  let i = find line link in
+                  if i >= 0 && not (List.mem link !used) then (
+                    used := link :: !used;
+                    let n = String.length link in
+                    let n' = String.length line in
+                    let line =
+                      String.sub line 0 i ^
+                      "[[" ^ String.sub line i n ^
+                      "]]" ^ loop (String.sub line (i+n) (n' - (i+n))) links
+                    in
+                    line
+                  ) else
+                    loop line links
+            in
+            loop line links) lines in
+
+      let lines = List.map trim lines in
+      let lines = List.map (fun line -> line ^ "<br>") lines in
+
+      String.concat "\n" lines
+    in
+
+    let content =
+      try
+       let text = find_body msg in
+       let content = markup_body text in
+       content
+      with
+         Not_found ->
+           "No plain text message body found" in
     "Message", "mail_body", content in
 
   (* Overwrite the first two sections of the current page, regardless of
@@ -250,12 +350,16 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid _ user =
    *)
   save_page dbh hostid ~user ~r model;
 
+  (* Rebuild threads? *)
+  if rebuild then
+    thread_mail dbh hostid ~user ~r date.Dbi.year date.Dbi.month;
+
   (* Commit to the database. *)
-  dbh#commit ();
+  PGOCaml.commit dbh;
 
   (* Finish off. *)
   ok ~title:"Imported"
-    q ("Message " ^ inet_message_id ^ " was imported.")
+    dbh hostid q ("Message " ^ inet_message_id ^ " was imported.")
 
 let () =
   register_script ~restrict:[CanImportMail] run