lots of fixes to FileBasedMailbox
[org.ibex.mail.git] / src / org / ibex / mail / target / FileBasedMailbox.java
1 package org.ibex.mail.target;
2 import org.prevayler.*;
3 import org.ibex.mail.*;
4 import org.ibex.util.*;
5 import org.ibex.io.*;
6 import java.io.*;
7 import java.nio.*;
8 import java.nio.channels.*;
9 import java.net.*;
10 import java.util.*;
11 import java.text.*;
12
13 // FIXME: we can omit UIDNEXT!
14 // FIXME use directory date/time as UIDNEXT and file date/time as UID; need to 'correct' file date/time after changes
15
16 /** An exceptionally crude implementation of Mailbox relying on POSIXy filesystem semantics */
17 public class FileBasedMailbox extends Mailbox.Default implements Serializable {
18
19     public String toString() { return "[FileBasedMailbox " + path + "]"; }
20     private static final char slash = File.separatorChar;
21     private static final WeakHashMap<String,FileBasedMailbox> instances = new WeakHashMap<String,FileBasedMailbox>();
22     public Mailbox slash(String name, boolean create) { return getFileBasedMailbox(path + slash + name, create); }
23     public static synchronized FileBasedMailbox getFileBasedMailbox(String path, boolean create) {
24         try {
25             FileBasedMailbox ret = instances.get(path);
26             if (ret == null) {
27                 if (!create && !new File(path).exists()) return null;
28                 instances.put(path, ret = new FileBasedMailbox(path));
29             }
30             return ret;
31         } catch (Exception e) {
32             Log.error(FileBasedMailbox.class, e);
33             return null;
34         }
35     }
36
37
38     // Instance //////////////////////////////////////////////////////////////////////////////
39
40     private String path;
41     private transient FileLock lock;
42     private transient Prevayler prevayler;
43     private transient ArrayList<CacheEntry> cache;
44     private transient int uidValidity;
45     private transient int uidNext;
46
47     public static class CacheEntry implements Serializable {
48         public MIME.Headers headers;
49         public String path;
50         public boolean seen;
51         private String name() { return path.indexOf(slash) == -1 ? path : path.substring(path.lastIndexOf(slash)+1); }
52         public boolean seen() { return name().substring(name().indexOf('.')+1).indexOf('s') != -1; }
53         public int uid() { return Integer.parseInt(name().substring(0, name().indexOf('.'))); }
54         public synchronized void seen(boolean seen) {
55             String newpath = path.substring(0, path.lastIndexOf('.') + 1) + (seen ? "s" : "");
56             new File(path).renameTo(new File(newpath));
57             path = newpath;
58         }
59         public CacheEntry(File f) throws IOException {
60             path = f.getAbsolutePath();
61             headers = new MIME.Headers(new Stream(new FileInputStream(f)), true);
62         }
63     }        
64
65     private FileBasedMailbox(String path) throws MailException, IOException, ClassNotFoundException {
66         new File(this.path = path).mkdirs();
67         new File(path + slash + ".cache").mkdirs();
68         File uidValidityFile = new File(path + slash + ".uidValidity");
69         if (!uidValidityFile.exists()) uidValidityFile.createNewFile();
70         uidValidity = (int)(uidValidityFile.lastModified() & 0xffffffff);
71         lock = new RandomAccessFile(this.path + slash + ".cache" + slash + "lock", "rw").getChannel().tryLock();
72         if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
73         prevayler = PrevaylerFactory.createPrevayler(new ArrayList<CacheEntry>(), path + slash + ".cache");
74         cache = (ArrayList<CacheEntry>)prevayler.prevalentSystem();
75         for(int i=0; i<cache.size(); i++) if (cache.get(i) != null) uidNext = Math.max(uidNext, cache.get(i).uid()+1);
76         if (cache.size() == 0) {
77             Log.warn(this, "rebuilding cache for " + path);
78             String[] files = new File(path).list(new FilenameFilter() {
79                     public boolean accept(File f, String s) {
80                         return s.substring(1).indexOf('.') != -1 && !s.equals("..");
81                     } });
82             long last = System.currentTimeMillis();
83             CacheEntry[] entries = new CacheEntry[files.length];
84             for(int i=0; i<files.length; i++) {
85                 if (System.currentTimeMillis() - last > 1000 * 1) {
86                     Log.warn(this, "rebuilding cache for " + path + ": " + Math.ceil((((float)i)/((float)files.length)) * 100)+ "%");
87                     last = System.currentTimeMillis();
88                 }
89                 CacheEntry ce = new CacheEntry(new File(path + slash + files[i]));
90                 entries[i] = ce;
91                 uidNext = Math.max(uidNext, ce.uid()+1);
92             }
93             prevayler.execute(new Rebuild(entries));
94         }
95         Log.warn(this, "taking snapshot for " + path);
96         prevayler.takeSnapshot();
97     }
98
99     private static class Rebuild implements Transaction {
100         public final CacheEntry[] entries;
101         public Rebuild(CacheEntry entry) { this.entries = new CacheEntry[] { entry }; }
102         public Rebuild(CacheEntry[] entries) { this.entries = entries; }
103         public void executeOn(Object c, Date now) {
104             synchronized(c) {
105                 for(int i=0; i<entries.length; i++) {
106                     ((ArrayList<CacheEntry>)c).add(entries[i]);
107                 }
108             } }
109     }
110
111     public Mailbox.Iterator  iterator()           { return new Iterator(); }
112
113     public int               uidValidity()        { return uidValidity; }
114
115     public int               uidNext()            { return uidNext(false); }
116     public int               uidNext(boolean inc) { return inc ? uidNext++ : uidNext; }
117     public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
118
119     public String[] children() {
120         Vec vec = new Vec();
121         String[] list = new File(path).list();
122         for(int i=0; i<list.length; i++) {
123             File f = new File(path + slash + list[i]);
124             if (f.isDirectory() && f.getName().charAt(0) != '.')
125                 vec.addElement(list[i]);
126         }
127         return (String[])vec.copyInto(new String[vec.size()]);
128     }
129
130     public synchronized void add(Message message, int flags) {
131         Log.info(path, message.summary());
132         try {
133             final String name = path + slash + uidNext(true) + "." +
134                 ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN ? "s" : "");
135             File target = new File(name);
136             File f = new File(target.getCanonicalPath() + "-");
137             FileOutputStream fo = new FileOutputStream(f);
138             Stream stream = new Stream(fo);
139             if (message.envelope != null) {
140                 stream.println("X-org.ibex.mail.headers.envelope.From: " + message.envelope.from);
141                 stream.println("X-org.ibex.mail.headers.envelope.To: " + message.envelope.to);
142             }
143             message.dump(stream);
144             fo.close();
145             f.renameTo(new File(name));
146             CacheEntry entry = new CacheEntry(new File(name));
147             prevayler.execute(new Rebuild(entry));
148         } catch (IOException e) { throw new MailException.IOException(e); }
149     }
150
151     private class Iterator extends Mailbox.Default.Iterator implements Serializable {
152         int cur = -1;
153         private CacheEntry entry() { synchronized(cache) { return cache.get(cur); } }
154         public MIME.Headers head() { return done() ? null : entry().headers; }
155         public boolean done() { synchronized(cache) { if (cur >= cache.size()) return true; } return false; }
156         public boolean next() { cur++; return !done(); }
157         public boolean seen() { return done() ? false : entry().seen(); }
158         public int num() { return cur+1; }  // EUDORA insists that message numbers start at 1, not 0
159         public int uid() { return done() ? -1 : entry().uid(); }
160         public Message cur() {
161             if (done()) return null;
162             try {
163                 File file = new File(entry().path);
164                 FileInputStream fis = null;
165                 try {
166                     fis = new FileInputStream(file);
167                     return new Message(new Stream(fis), new Message.Envelope(null, null, new Date(file.lastModified())));
168                 } finally { if (fis != null) fis.close(); }
169             } catch (IOException e) { throw new MailException.IOException(e);
170             } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
171         }
172         public void seen(final boolean on) {
173             if (done()) return;
174             final int ptr = this.cur;
175             prevayler.execute(new Transaction() {
176                     public void executeOn(Object c, Date d) {
177                         ((ArrayList<CacheEntry>)c).get(ptr).seen(on); } });
178         }
179         public void delete() {
180             if (done()) return;
181             final CacheEntry entry = entry();
182             prevayler.execute(new Transaction() {
183                     public void executeOn(Object c, Date d) {
184                             new File(entry.path).delete();
185                             cache.remove(entry);
186                     }});
187         }
188     }
189 }