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 /** An exceptionally crude implementation of Mailbox relying on POSIXy filesystem semantics */
14 public class FileBasedMailbox extends Mailbox.Default {
16 public static final long MAGIC_DATE = 0;
18 public String toString() { return "[FileBasedMailbox " + path.getAbsolutePath() + "]"; }
19 private static final char slash = File.separatorChar;
20 private static final WeakHashMap<String,FileBasedMailbox> instances = new WeakHashMap<String,FileBasedMailbox>();
21 public Mailbox slash(String name, boolean create) { return getFileBasedMailbox(path.getAbsolutePath()+slash+name, create); }
23 // FIXME: should be a File()
24 public static synchronized FileBasedMailbox getFileBasedMailbox(String path, boolean create) {
26 FileBasedMailbox ret = instances.get(path);
28 if (!create && !(new File(path).exists())) return null;
29 instances.put(path, ret = new FileBasedMailbox(new File(path)));
32 } catch (Exception e) {
33 Log.error(FileBasedMailbox.class, e);
39 // Instance //////////////////////////////////////////////////////////////////////////////
42 private FileLock lock;
43 private Prevayler prevayler;
46 public static class Cache implements Serializable {
47 public final int uidValidity = new Random().nextInt();
48 private final Hashtable<String,Entry> byname = new Hashtable<String,Entry>();
49 private final Hashtable<Integer,Entry> byuid = new Hashtable<Integer,Entry>();
50 private final ArrayList<Entry> linear = new ArrayList<Entry>();
51 public final File dir;
52 private int uidNext = 0;
54 public Cache(File dir) { this.dir = dir; }
56 public void init(Prevayler prevayler) throws IOException {
58 Log.info(this, "initializing maildir " + dir.getParent());
59 boolean invalid = false;
60 ArrayList<Transaction> kill = new ArrayList<Transaction>();
61 for(String s : byname.keySet()) {
62 String name = dir.getParent() + slash + s;
63 if (!new File(name).exists() && !new File(name+"s").exists()) {
64 Log.error(this, "dropping message " + name);
65 kill.add(new Drop(byname.get(s).uid()));
68 for(Transaction t : kill) prevayler.execute(t);
69 for(String file : new File(dir.getParent()).list())
70 if (file.charAt(0)!='.' && !(new File(dir.getParent() + slash + file).isDirectory()))
71 if (get(file) == null) new Entry(this, prevayler, file);
72 Log.info(this, " done initializing maildir " + dir.getParent());
73 prevayler.takeSnapshot();
76 public static class Drop implements Transaction {
78 public Drop(int uid) { this.uid = uid; }
79 public void executeOn(Object o, Date now) {
82 c.byname.remove(e.name);
88 public synchronized int size() { return linear.size(); }
89 public synchronized int uidNext(boolean increment) { return increment ? uidNext++ : uidNext; }
90 public synchronized Entry get(int uid) { return byuid.get(uid); }
91 public synchronized Entry getLinear(int num) { return linear.get(num); }
92 public synchronized Entry get(String name) {
93 if (name.endsWith("s")) name = name.substring(0, name.length() - 1);
94 return byname.get(name);
97 public static class Entry implements Serializable {
98 public final MIME.Headers headers;
99 public final String name;
101 private boolean seen;
103 public Entry(Cache cache, Prevayler prevayler, String name) throws IOException {
104 File f = new File(cache.dir.getParent()+slash+name);
105 seen = f.lastModified() == MAGIC_DATE;
107 headers = new MIME.Headers(new Stream(new FileInputStream(f)), true);
108 prevayler.execute(new Transaction() {
109 public void executeOn(Object o, Date now) {
110 Cache cache = (Cache)o;
111 synchronized(cache) {
112 Entry.this.uid = cache.uidNext(true);
113 cache.linear.add(Entry.this);
114 cache.byuid.put(Entry.this.uid, Entry.this);
115 cache.byname.put(Entry.this.name, Entry.this);
119 public int uid() { return uid; }
120 public boolean seen() { return seen; }
121 public void seen(Cache cache, boolean seen) {
123 String base = cache.dir.getParent() + slash + name;
124 File target = new File(base);
125 if (!target.exists()) target = new File(base + "s");
126 target.setLastModified(seen ? System.currentTimeMillis() : MAGIC_DATE);
128 public void delete(Cache cache) {
129 String base = cache.dir.getParent() + slash + name;
130 File target = new File(base);
131 if (!target.exists()) target = new File(base + "s");
132 if (target.exists()) target.delete();
134 public Message message(Cache cache) { try {
135 String base = cache.dir.getParent() + slash + name;
136 File target = new File(base);
137 if (!target.exists()) target = new File(base + "s");
138 FileInputStream fis = null;
140 fis = new FileInputStream(target);
141 return new Message(new Stream(fis), new Message.Envelope(null, null, new Date(target.lastModified())));
142 } finally { if (fis != null) fis.close(); }
143 } catch (IOException e) { throw new MailException.IOException(e);
144 } catch (Message.Malformed e) { throw new MailException(e.getMessage()); }
149 private static void rmDashRf(File f) throws IOException {
150 if (!f.isDirectory()) { f.delete(); return; }
151 String[] children = f.list();
152 for(int i=0; i<children.length; i++) rmDashRf(new File(f.getAbsolutePath() + slash + children[i]));
156 private FileBasedMailbox(File path) throws MailException, IOException, ClassNotFoundException {
161 lock = new RandomAccessFile(this.path.getAbsolutePath() + slash + ".lock", "rw").getChannel().tryLock();
162 if (lock == null) throw new IOException("unable to lock FileBasedMailbox");
164 File cacheDir = new File(path.getAbsolutePath() + slash + ".cache");
166 prevayler = PrevaylerFactory.createPrevayler(new Cache(cacheDir), cacheDir.getAbsolutePath());
167 cache = (Cache)prevayler.prevalentSystem();
168 } catch (Throwable t) {
169 Log.warn(this, "error while attempting to reconstitute a FileBasedMailbox.cache:");
171 Log.warn(this, "discarding cache...");
173 prevayler = PrevaylerFactory.createPrevayler(new Cache(cacheDir), cacheDir.getAbsolutePath());
174 cache = (Cache)prevayler.prevalentSystem();
176 cache.init(prevayler);
180 public Mailbox.Iterator iterator() { return new Iterator(); }
181 public int uidValidity() { return cache.uidValidity; }
182 public synchronized void add(Message message) { add(message, Mailbox.Flag.RECENT); }
183 public String[] children() {
185 String[] list = path.list();
186 for(int i=0; i<list.length; i++) {
187 File f = new File(path.getAbsolutePath() + slash + list[i]);
188 if (f.isDirectory() && f.getName().charAt(0) != '.') vec.addElement(list[i]);
190 return (String[])vec.copyInto(new String[vec.size()]);
193 public int uidNext() { return cache.uidNext(false); }
194 public synchronized void add(Message message, int flags) {
195 Log.info(path, message.summary());
197 String name; String fullname; File target; File f;
199 name = cache.uidNext(true) + ".";
200 fullname = path.getAbsolutePath() + slash + name;
201 target = new File(fullname);
202 f = new File(target.getCanonicalPath() + "-");
203 Log.error(this, "aieeee!!!! target of add() already exists: " + target.getAbsolutePath());
204 } while (f.exists() || target.exists());
205 FileOutputStream fo = new FileOutputStream(f);
206 Stream stream = new Stream(fo);
207 if (message.envelope != null) {
208 stream.println("X-org.ibex.mail.headers.envelope.From: " + message.envelope.from);
209 stream.println("X-org.ibex.mail.headers.envelope.To: " + message.envelope.to);
211 message.dump(stream);
213 f.renameTo(new File(fullname));
214 if ((flags & Mailbox.Flag.SEEN) == Mailbox.Flag.SEEN) f.setLastModified(MAGIC_DATE);
215 new Cache.Entry(cache, prevayler, name);
216 } catch (IOException e) { throw new MailException.IOException(e); }
219 private class Iterator extends Mailbox.Default.Iterator {
221 private Cache.Entry entry() { return cache.getLinear(cur); }
222 public MIME.Headers head() { return done() ? null : entry().headers; }
223 public boolean done() { return cur >= cache.size(); }
224 public boolean next() { cur++; return !done(); }
225 public boolean seen() { return done() ? false : entry().seen(); }
226 public int num() { return cur+1; } // EUDORA insists that message numbers start at 1, not 0
227 public int uid() { return done() ? -1 : entry().uid(); }
228 public Message cur() { return done() ? null : entry().message(cache); }
229 public void seen(boolean seen) { prevayler.execute(new Seen(uid(), seen)); }
230 public void delete() { if (!done()) prevayler.execute(new Delete(uid())); }
233 private static class Delete implements Transaction {
235 public Delete(int uid) { this.uid = uid; }
236 public void executeOn(Object c, Date d) { ((Cache)c).get(uid).delete((Cache)c); }
238 private static class Seen implements Transaction {
240 private boolean seen;
241 public Seen(int uid, boolean seen) { this.uid = uid; this.seen = seen; }
242 public void executeOn(Object c, Date d) { ((Cache)c).get(uid).seen((Cache)c, seen); }