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 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 FileLock lock;
42 private Prevayler prevayler;
43 private LinkedList<CacheEntry> cache;
46 public static class CacheEntry implements Serializable {
47 public MIME.Headers headers;
50 private String name() { return path.indexOf(slash) == -1 ? path : path.substring(path.lastIndexOf(slash)+1); }
51 public boolean seen() { return name().substring(name().indexOf('.')+1).indexOf('s') != -1; }
52 public int uid() { return Integer.parseInt(name().substring(0, name().indexOf('.'))); }
53 public synchronized void seen(boolean seen) {
54 String newpath = path.substring(0, path.lastIndexOf('.') + 1) + (seen ? "s" : "");
55 new File(path).renameTo(new File(newpath));
58 public CacheEntry(File f) throws IOException {
59 path = f.getAbsolutePath();
60 headers = new MIME.Headers(new Stream(new FileInputStream(f)), true);
64 private FileBasedMailbox(String path) throws MailException, IOException, ClassNotFoundException {
65 new File(this.path = path).mkdirs();
66 new File(path + slash + ".cache").mkdirs();
67 lock = new RandomAccessFile(this.path + slash + ".cache" + slash + "lock", "rw").getChannel().tryLock();
68 if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
69 prevayler = PrevaylerFactory.createPrevayler(new LinkedList<CacheEntry>(), path + slash + ".cache");
70 cache = (LinkedList<CacheEntry>)prevayler.prevalentSystem();
71 for(int i=0; i<cache.size(); i++) uidNext = Math.max(uidNext, cache.get(i).uid()+1);
72 if (cache.size() > 0) return;
74 Log.warn(this, "rebuilding cache for " + path);
75 String[] files = new File(path).list(new FilenameFilter() {
76 public boolean accept(File f, String s) {
77 return s.substring(1).indexOf('.') != -1 && !s.equals("..");
79 for(int i=0; i<files.length; i++) {
80 CacheEntry ce = new CacheEntry(new File(path + slash + files[i]));
81 prevayler.execute(new Rebuild(ce));
82 uidNext = Math.max(uidNext, ce.uid()+1);
84 prevayler.takeSnapshot();
87 private static class Rebuild implements Transaction {
88 public final CacheEntry entry;
89 public Rebuild(CacheEntry entry) { this.entry = entry; }
90 public void executeOn(Object c, Date now) { synchronized(c) { ((LinkedList<CacheEntry>)c).add(entry); } }
93 public Mailbox.Iterator iterator() { return new Iterator(); }
94 public int uidValidity() { return (int)(new File(path).lastModified() & 0xffffffL); }
95 public int uidNext() { return uidNext(false); }
96 public int uidNext(boolean inc) { return inc ? uidNext++ : uidNext; }
97 public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
99 public String[] children() {
101 String[] list = new File(path).list();
102 for(int i=0; i<list.length; i++) if ((new File(path + slash + list[i]).isDirectory())) vec.addElement(list[i]);
103 return (String[])vec.copyInto(new String[vec.size()]);
106 public synchronized void add(Message message, int flags) {
107 Log.info(path, message.summary());
109 final String name = path + slash + uidNext(true) + "." +
110 ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN ? "s" : "");
111 File target = new File(name);
112 File f = new File(target.getCanonicalPath() + "-");
113 FileOutputStream fo = new FileOutputStream(f);
114 Stream stream = new Stream(fo);
115 if (message.envelope != null) {
116 stream.println("X-org.ibex.mail.headers.envelope.From: " + message.envelope.from);
117 stream.println("X-org.ibex.mail.headers.envelope.To: " + message.envelope.to);
119 message.dump(stream);
121 f.renameTo(new File(name));
122 CacheEntry entry = new CacheEntry(new File(name));
123 prevayler.execute(new Rebuild(entry));
124 } catch (IOException e) { throw new MailException.IOException(e); }
127 private class Iterator extends Mailbox.Default.Iterator implements Serializable {
129 private CacheEntry entry() { synchronized(cache) { return cache.get(cur); } }
130 public MIME.Headers head() { return done() ? null : entry().headers; }
131 public boolean done() { synchronized(cache) { if (cur >= cache.size()) return true; } return false; }
132 public boolean next() { cur++; return !done(); }
133 public boolean seen() { return done() ? false : entry().seen(); }
134 public int num() { return cur+1; } // EUDORA insists that message numbers start at 1, not 0
135 public int uid() { return entry().uid(); }
136 public Message cur() {
137 if (done()) return null;
139 File file = new File(entry().path);
140 FileInputStream fis = null;
142 fis = new FileInputStream(file);
143 return new Message(new Stream(fis), new Message.Envelope(null, null, new Date(file.lastModified())));
144 } finally { if (fis != null) fis.close(); }
145 } catch (IOException e) { throw new MailException.IOException(e);
146 } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
148 public void seen(final boolean on) {
150 final CacheEntry entry = entry();
151 prevayler.execute(new Transaction() { public void executeOn(Object c, Date d) { entry.seen(on); } });
153 public void delete() {
155 final CacheEntry entry = entry();
156 prevayler.execute(new Transaction() {
157 public void executeOn(Object c, Date d) {
158 new File(entry.path).delete();