1 package org.ibex.mail.target;
2 import org.prevayler.*;
3 import org.ibex.mail.*;
4 import org.ibex.util.*;
8 import java.nio.channels.*;
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
16 /** An exceptionally crude implementation of Mailbox relying on POSIXy filesystem semantics */
17 public class FileBasedMailbox extends Mailbox.Default implements Serializable {
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) {
25 FileBasedMailbox ret = instances.get(path);
27 if (!create && !new File(path).exists()) return null;
28 instances.put(path, ret = new FileBasedMailbox(path));
31 } catch (Exception e) {
32 Log.error(FileBasedMailbox.class, e);
38 // Instance //////////////////////////////////////////////////////////////////////////////
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;
47 public static class CacheEntry implements Serializable {
48 public MIME.Headers headers;
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));
59 public CacheEntry(File f) throws IOException {
60 path = f.getAbsolutePath();
61 headers = new MIME.Headers(new Stream(new FileInputStream(f)), true);
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("..");
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();
89 CacheEntry ce = new CacheEntry(new File(path + slash + files[i]));
91 uidNext = Math.max(uidNext, ce.uid()+1);
93 prevayler.execute(new Rebuild(entries));
95 Log.warn(this, "taking snapshot for " + path);
96 prevayler.takeSnapshot();
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) {
105 for(int i=0; i<entries.length; i++) {
106 ((ArrayList<CacheEntry>)c).add(entries[i]);
111 public Mailbox.Iterator iterator() { return new Iterator(); }
113 public int uidValidity() { return uidValidity; }
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); }
119 public String[] children() {
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]);
127 return (String[])vec.copyInto(new String[vec.size()]);
130 public synchronized void add(Message message, int flags) {
131 Log.info(path, message.summary());
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);
143 message.dump(stream);
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); }
151 private class Iterator extends Mailbox.Default.Iterator implements Serializable {
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;
163 File file = new File(entry().path);
164 FileInputStream fis = null;
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()); }
172 public void seen(final boolean on) {
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); } });
179 public void delete() {
181 final CacheEntry entry = entry();
182 prevayler.execute(new Transaction() {
183 public void executeOn(Object c, Date d) {
184 new File(entry.path).delete();