bugfix to basename
[org.ibex.mail.git] / src / org / ibex / mail / target / FileBasedMailbox.java
index a934fac..c700ae0 100644 (file)
+// Copyright 2000-2005 the Contributors, as shown in the revision logs.
+// Licensed under the Apache Public Source License 2.0 ("the License").
+// You may not use this file except in compliance with the License.
+
 package org.ibex.mail.target;
+import org.prevayler.*;
 import org.ibex.mail.*;
 import org.ibex.util.*;
 import org.ibex.io.*;
 import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
 import java.net.*;
 import java.util.*;
 import java.text.*;
-
-// FIXME: we can omit UIDNEXT!
-// FIXME use directory date/time as UIDNEXT and file date/time as UID; need to 'correct' file date/time after changes
+import javax.servlet.*;
+import javax.servlet.http.*;
 
 /** An exceptionally crude implementation of Mailbox relying on POSIXy filesystem semantics */
 public class FileBasedMailbox extends Mailbox.Default {
 
-    public String toString() { return "[FileBasedMailbox " + path + "]"; }
-
+    public static final long MAGIC_DATE = 0;
     private static final char slash = File.separatorChar;
-    private static final Hashtable instances = new Hashtable();
-    public static FileBasedMailbox getFileBasedMailbox(String path, boolean create) {
-        FileBasedMailbox ret = (FileBasedMailbox)instances.get(path);
-        if (ret != null) return ret;
-        File f = new File(path);
-        if (!create && !f.exists()) return null;
-        instances.put(path, ret = new FileBasedMailbox(path));
-        return ret;
+    private static final WeakHashMap<String,FileBasedMailbox> instances = new WeakHashMap<String,FileBasedMailbox>();
+    public String toString() { return "[FileBasedMailbox " + path.getAbsolutePath() + "]"; }
+    public Mailbox slash(String name, boolean create) { return getFileBasedMailbox(path.getAbsolutePath()+slash+name, create); }
+
+    // FIXME: should be a File()
+    public static synchronized FileBasedMailbox getFileBasedMailbox(String path, boolean create) {
+        try {
+            FileBasedMailbox ret = instances.get(path);
+            if (ret == null) {
+                if (!create && !(new File(path).exists())) return null;
+                instances.put(path, ret = new FileBasedMailbox(new File(path)));
+            }
+            return ret;
+        } catch (Exception e) {
+            Log.error(FileBasedMailbox.class, e);
+            return null;
+        }
     }
 
-    public static final FilenameFilter filter = new FilenameFilter() {
-            public boolean accept(File f, String s) {
-                return s.indexOf('.') != -1;
-            } };
+    // Instance //////////////////////////////////////////////////////////////////////////////
+
+    private File path;
+    private FileLock lock;
+    private int uidNext;
+    private int uidValidity;
 
+    // Helpers //////////////////////////////////////////////////////////////////////////////
 
-    // Instance //////////////////////////////////////////////////////////////////////////////
+    private static void rmDashRf(File f) throws IOException {
+        if (!f.isDirectory()) { f.delete(); return; }
+        String[] children = f.list();
+        for(int i=0; i<children.length; i++) rmDashRf(new File(f.getAbsolutePath() + slash + children[i]));
+        f.delete();
+    }
 
-    private String path;
-    private File uidNext;
-    private FileBasedMailbox(String path) throws MailException {
-        new File(this.path = path).mkdirs();
-        uidNext(false);
+    private FileBasedMailbox(File path) throws MailException, IOException, ClassNotFoundException {
+        this.path = path;
+        path.mkdirs();
+
+        // acquire lock
+        File lockfile = new File(this.path.getAbsolutePath() + slash + ".lock");
+        lock = new RandomAccessFile(lockfile, "rw").getChannel().tryLock();
+        if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
+        uidValidity = (int)(lockfile.lastModified() & 0xffffffff);
+        uidNext = 0;
+        String[] files = path.list();
+        for(int i=0; i<files.length; i++) {
+            try {
+                if (files[i].indexOf('.') != -1) files[i] = files[i].substring(0, files[i].indexOf('.'));
+                int n = Integer.parseInt(files[i]);
+                if (n>=uidNext) uidNext = n;
+            } catch(Exception e) { /* DELIBERATE */ }
+        }
     }
 
-    public Mailbox          slash(String name, boolean create) {
-        return FileBasedMailbox.getFileBasedMailbox(path + slash + name, create); }
 
+    public Mailbox.Iterator  iterator()           { return new Iterator(); }
+    public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
     public String[] children() {
         Vec vec = new Vec();
-        String[] list = new File(path).list();
+        String[] list = path.list();
         for(int i=0; i<list.length; i++) {
-            if (!(new File(path + slash + list[i]).isDirectory())) continue;
-            vec.addElement(list[i]);
+            File f = new File(path.getAbsolutePath() + slash + list[i]);
+            if (f.isDirectory() && f.getName().charAt(0) != '.') vec.addElement(list[i]);
         }
-        String[] ret = new String[vec.size()];
-        vec.copyInto(ret);
-        return ret;
+        return (String[])vec.copyInto(new String[vec.size()]);
     }
 
-    public Mailbox.Iterator iterator() { return new FileBasedMailbox.Iterator(); }
-    public int              uidValidity() { return (int)(new File(path).lastModified() & 0xffffffL); }
-
-    public int uidNext() { return uidNext(false); }
-    public int uidNext(boolean inc) {
-        try {
-            uidNext = new File(path + slash + "UIDNEXT");
-            if (!uidNext.exists()) {
-                File tmp = new File(uidNext + "-");
-                PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tmp)));
-                pw.println("1");
-                pw.flush();
-                pw.close();
-                tmp.renameTo(uidNext);
-                return 1;
-            }
-            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(uidNext)));
-            int ret = Integer.parseInt(br.readLine());
-            br.close();
-            if (inc) {
-                File tmp = new File(uidNext + "-");
-                PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tmp)));
-                pw.println(ret+1);
-                pw.flush();
-                pw.close();
-                tmp.renameTo(uidNext);
-            }
-            return ret;
-        } catch (IOException e) { throw new MailException.IOException(e); }
-    }        
-
-    public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
+    public int uidValidity() { return uidValidity; }
+    public int uidNext() {  return uidNext; }
     public synchronized void add(Message message, int flags) {
-        Log.info(this, "adding message to ["+toString()+"]:\n" + message.summary());
         try {
-            int num = new File(path).list(filter).length;
-           String name = path + slash + uidNext(true) + "." +
-                ((flags & Mailbox.Flag.DELETED) == Mailbox.Flag.DELETED ? "x" : "") +
-                ((flags & Mailbox.Flag.DRAFT) == Mailbox.Flag.DRAFT ? "d" : "") +
-                ((flags & Mailbox.Flag.RECENT) == Mailbox.Flag.RECENT ? "r" : "") +
-                ((flags & Mailbox.Flag.ANSWERED) == Mailbox.Flag.ANSWERED ? "a" : "") +
-                ((flags & Mailbox.Flag.FLAGGED) == Mailbox.Flag.FLAGGED ? "f" : "") +
-                ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN ? "s" : "");
-            Log.info(this, "    with chosen filename " + name);
-            File target = new File(name);
-            File f = new File(target.getCanonicalPath() + "-");
-            FileOutputStream fo = new FileOutputStream(f);
-            Stream stream = new Stream(fo);
-            stream.println("X-org.ibex.mail.headers.envelope.From: " + message.envelope.from);
-            stream.println("X-org.ibex.mail.headers.envelope.To: " + message.envelope.to);
-            message.dump(stream);
-            fo.close();
-            f.renameTo(target);
-            Log.info(this, "    done writing.");
+            String name, fullname; File target, f;
+            for(int i = uidNext; ; i++) {
+                name = i + ".";
+                fullname = path.getAbsolutePath() + slash + name;
+                target = new File(fullname);
+                f = new File(target.getCanonicalPath() + "-");
+                if (!f.exists() && !target.exists()) break;
+                Log.error(this, "aieeee!!!! target of add() already exists: " + target.getAbsolutePath());
+            }
+            Stream stream = new Stream(new FileOutputStream(f));
+            message.getStream().transcribe(stream);
+            stream.close();
+            f.renameTo(new File(fullname));
+            uidNext++;
+            f = new File(fullname);
+            if ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN) f.setLastModified(MAGIC_DATE);
         } catch (IOException e) { throw new MailException.IOException(e); }
+        Log.info(this, path + " <= " + message.summary());
     }
 
     private class Iterator extends Mailbox.Default.Iterator {
         int cur = -1;
-        private String[] names;
-        private boolean seen = false, deleted = false, draft = false, flagged = false, answered = false, recent = false;
-        public Iterator() { names = new File(path).list(filter); }
-
-        public Message head() { return cur(); }
+        String[] files = path.list(new FilenameFilter() { public boolean accept(File dir, String name) {
+            return name.endsWith(".");
+        } });
+        private File file() { return new File(path.getAbsolutePath() + slash + files[cur]); }
+        public boolean done() { return cur >= files.length; }
+        public boolean next() { cur++; return !done(); }
+        public boolean seen() { return false; }
+        public boolean recent() { return false; }
+        public int num() { return cur+1; }  // EUDORA insists that message numbers start at 1, not 0
+        public int uid() { return done() ? -1 : Integer.parseInt(files[cur].substring(0, files[cur].length()-1)); }
+        public void delete() { File f = file(); if (f != null && f.exists()) f.delete(); }
+        public void seen(boolean seen) { }
+        public Headers head() {
+            if (done()) return null;
+            FileInputStream fis = null;
+            try {
+                return new Headers.Original(new Stream(new FileInputStream(file())));
+            } catch (IOException e) { throw new MailException.IOException(e);
+            } finally { if (fis != null) try { fis.close(); } catch (Exception e) { /* DELIBERATE */ } }
+        }
         public Message cur() {
-           if (cur >= names.length) return null;
+            FileInputStream fis = null;
             try {
-                File file = new File(path + File.separatorChar + names[cur]);
-                FileInputStream fis = new FileInputStream(file);
-                Stream stream = new Stream(fis);
-                Address envelopeFrom = null;
-                Address envelopeTo = null;
-                /*
-                for(String s = stream.readln(); s != null; s = stream.readln()) {
-                    if (s.startsWith("X-org.ibex.mail.headers.envelope.From: "))
-                        envelopeFrom = Address.parse(s.substring(38).trim());
-                    else if (s.startsWith("X-org.ibex.mail.headers.envelope.To: "))
-                        envelopeTo = Address.parse(s.substring(36).trim());
-                    else {
-                        stream.unread(s + "\r\n");
+                return Message.newMessage(new Fountain.File(file()));
+                //} catch (IOException e) { throw new MailException.IOException(e);
+            } catch (Message.Malformed e) { throw new MailException(e.getMessage());
+            } finally { if (fis != null) try { fis.close(); } catch (Exception e) { /* DELIBERATE */ } }
+        }
+    }
+
+    public static class Servlet extends HttpServlet {
+        public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { doGet(request, response); }
+        private void frames(HttpServletRequest request, HttpServletResponse response, boolean top) throws IOException {
+            String basename = request.getRequestURI();
+            PrintWriter pw = new PrintWriter(response.getWriter());
+            pw.println("<html>");
+            if (top) {
+                pw.println("  <frameset rows='10%,30%,*'>");
+                pw.println("    <frame src='"+basename+"?frame=banner' marginwidth=0 marginheight=0 name=banner/>");
+                pw.println("    <frame src='"+basename+"?frame=top' marginwidth=0 marginheight=0 name=top/>");
+                pw.println("    <frame src='"+basename+"?frame=bottom' marginwidth=0 marginheight=0 name='bottom'/>");
+            } else {
+                pw.println("  <frameset cols='150,*'>");
+                pw.println("    <frame src='"+basename+"?frame=topleft' marginwidth=0 marginheight=0 name=topleft/>");
+                pw.println("    <frame src='"+basename+"?frame=topright' marginwidth=0 marginheight=0 name=topright/>");
+            }
+            pw.println("  </frameset>");
+            pw.println("</html>");
+            pw.flush();
+        }
+
+        public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+            String frame = request.getParameter("frame");
+            String basename = request.getRequestURI();
+
+            if (frame == null) { frames(request, response, true); return; }
+            if (frame.equals("top")) { frames(request, response, false); return; }
+            if (frame.equals("banner")) { banner(request, response); return; }
+            if (frame.equals("topleft")) { return; }
+
+            if (request.getServletPath().indexOf("..") != -1) throw new IOException(".. not allowed in image paths");
+            ServletContext cx = getServletContext();
+            String path = cx.getRealPath(request.getServletPath());
+            Mailbox mbox = FileBasedMailbox.getFileBasedMailbox(path, false);
+            if (mbox == null) throw new IOException("no such mailbox: " + path);
+
+            Vec msgs = new Vec();
+            for(Mailbox.Iterator it = mbox.iterator(); it.next();) {
+                String[] s = new String[4];
+                Message m = it.cur();
+                s[0] = (m.from==null?"":m.from.toString(true));
+                s[1] = m.subject;
+                s[2] = (m.date + "").trim().replaceAll(" ","&nbsp;");
+                s[3] = it.num() + "";
+                msgs.addElement(s);
+            }
+            String[][] messages;
+            msgs.copyInto(messages = new String[msgs.size()][]);
+
+            if ("bottom".equals(frame)) { bottom(request, response, messages, mbox); return; }
+            if ("topright".equals(frame)) { topright(request, response, messages); return; }
+        }
+
+        private void bottom(HttpServletRequest request, HttpServletResponse response, String[][] messages, Mailbox mbox)
+            throws IOException {
+            PrintWriter pw = new PrintWriter(response.getWriter());
+            pw.println("<html>");
+            pw.println("  <body>");
+            pw.println("    <pre>");
+            if (request.getParameter("msgnum") != null) {
+                int target = Integer.parseInt(request.getParameter("msgnum"));
+                for(Mailbox.Iterator it = mbox.iterator(); it.next();) {
+                    if (it.num() == target) {
+                        StringBuffer tgt = new StringBuffer();
+                        it.cur().getBody().getStream().transcribe(tgt);
+                        pw.println(tgt.toString());
                         break;
                     }
                 }
-                */
-                Message ret = new Message(stream, new Message.Envelope(null, null, new Date(file.lastModified())));
-                fis.close();
-                return ret;
-            } catch (IOException e) { throw new MailException.IOException(e);
-            } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
-        }
-        public boolean next() {
-            cur++;
-            if (cur >= names.length) return false;
-            String name = names[cur].substring(names[cur].indexOf('.') + 1);
-           if (!(new File(path + File.separatorChar + names[cur])).exists()) return next();
-            seen     = name.indexOf('s') != -1;
-            deleted  = name.indexOf('x') != -1;
-            flagged  = name.indexOf('f') != -1;
-            draft    = name.indexOf('d') != -1;
-            answered = name.indexOf('a') != -1;
-            recent   = name.indexOf('r') != -1;
-            return true;
+            }
+            pw.println("    </pre>");
+            pw.println("  </body>");
+            pw.println("</html>");
+            pw.flush();
+            pw.close();
         }
-        public int num() { return cur; }
-        public int uid() {
-            try { return Integer.parseInt(names[cur].substring(0, names[cur].indexOf('.')));
-            } catch (NumberFormatException nfe) {
-                Log.warn(FileBasedMailbox.class, "NumberFormatException: " + names[cur].substring(0, names[cur].length() - 1));
-                return -1; } }
-
-        private void fixflags() {
-            String newName =
-                names[cur].substring(0, names[cur].indexOf('.') + 1) +
-                (seen ? "s" : "") +
-                (deleted ? "x" : "") +
-                (flagged ? "f" : "") +
-                (draft ? "d" : "") +
-                (recent ? "r" : "") +
-                (answered ? "a" : "");
-            new File(path + File.separatorChar + names[cur]).renameTo(new File(path + File.separatorChar + newName));
-            names[cur] = newName;
+        
+        private void banner(HttpServletRequest request, HttpServletResponse response) throws IOException {
+            String basename = request.getServletPath();
+            String realpath = getServletContext().getRealPath(basename);
+            MailingList list = MailingList.getMailingList(realpath);
+            list.banner(request, response);
         }
 
-        public void    delete() {
-           new File(path + File.separatorChar + names[cur]).delete();
-           // FIXME remove from list?
-       }
-        public void    set(String key, String val) { throw new MailException("not supported"); }
-        public String  get(String key) { throw new MailException("not supported"); }
-
-        public boolean seen() { return seen; }
-        public boolean deleted() { return deleted; }
-        public boolean flagged() { return flagged; }
-        public boolean draft() { return draft; }
-        public boolean answered() { return answered; }
-        public boolean recent() { return recent; }
-        public void    seen(boolean on) { seen = on; fixflags(); }
-        public void    deleted(boolean on) { deleted = on; fixflags(); }
-        public void    flagged(boolean on) { flagged = on; fixflags(); }
-        public void    draft(boolean on) { draft = on; fixflags(); }
-        public void    answered(boolean on) { answered = on; fixflags(); }
-        public void    recent(boolean on) { recent = on; fixflags(); }
-
+        private void topright(HttpServletRequest request, HttpServletResponse response, String[][] messages) throws IOException {
+            PrintWriter pw = new PrintWriter(response.getWriter());
+            String basename = request.getRequestURI();
+            pw.println("<html>");
+            pw.println("  <head>");
+            pw.println("    <style>");
+            pw.println("      a:link    { color: #000; text-decoration: none; }");
+            pw.println("      a:active  { color: #f00; text-decoration: none; }");
+            pw.println("      a:visited { color: #777; text-decoration: none; }");
+            pw.println("      a:hover   { color: #000; text-decoration: none; }");
+            pw.println("      /* a:hover   { color: #00f; text-decoration: none; border-bottom: 1px dotted; } */");
+            pw.println("    </style>");
+            pw.println("  </head>");
+            pw.println("  <body onKeyPress='doKey(event.keyCode)'>");
+            pw.println("    <script>");
+            pw.println("      var chosen = null;");
+            pw.println("      var all = [];");
+            pw.println("      function doKey(x) {");
+            pw.println("          if (chosen == null) { choose(all[0]); return; }");
+            pw.println("          switch(x) {");
+            pw.println("              case 112: if (chosen.id > 0) choose(all[chosen.id-1]); break;");
+            pw.println("              case 110: if (chosen.id < (all.length-1)) choose(all[1+(1*chosen.id)]); break;");
+            pw.println("          }");
+            pw.println("      }");
+            pw.println("      function choose(who) {");
+            pw.println("          who.style.background = '#ffc';");
+            pw.println("          if (chosen != null) chosen.style.background = '#aaa';");
+            pw.println("          parent.parent.bottom.location='"+basename+"?frame=bottom&msgnum='+who.id;");
+            pw.println("          chosen = who;");
+            pw.println("      }");
+            pw.println("    </script>");
+            pw.println("    <table width=100% border=0 cellpadding=0 cellspacing=0>");
+            boolean odd=false;
+            for(int i=0; i<messages.length; i++) {
+                odd = !odd;
+                String[] m = messages[i];
+                pw.println("      <tr style='background: "+(odd?"#e8eef7":"white")+"' id='"+m[3]+"' "+
+                           "onmouseover='this.style.color=\"blue\"' "+
+                           "onmouseout='this.style.color=\"black\"' "+
+                           "onclick='choose(this);'>");
+                pw.println("<td style='padding:5px; padding-bottom:2px'>"+m[0]+"</td>");
+                pw.println("<td style='padding:5px; padding-bottom:2px'>"+m[1]+"</td>");
+                pw.println("<td style='padding:5px; padding-bottom:2px'>"+m[2]+"</td>");
+                pw.println("</tr>");
+                pw.println("<script> all["+i+"] = document.getElementById('"+i+"'); </script>");
+                pw.println("<tr height=1 bgcolor=black><td colspan=3>");
+                pw.println("    <img src=http://www.xwt.org/images/clearpixel.gif></td></tr>");
+            }
+            pw.println("    </table>");
+            pw.println("  </body>");
+            pw.println("</html>");
+            pw.flush();
+            pw.close();
+        }
     }
 }