==============================================================================
org.ibex.mail
-
A mail server offering the same level of flexibility as Java Servlets.
The org.ibex.mail server infrastructure is designed to run within
- Shared configuration of virtual hosts between web-apps and mail-apps
- Authentication infrastructure
+
+______________________________________________________________________________
+JavaScript API
+
+Each script is invoked with the variable 'm' bound to a message to be
+handled. The script MUST return either a Filter or a Target which
+will process the message next. This is a crucial property that
+prevents mail from being lost -- a script can never "lose" a message
+except by explicitly sending it to the target 'ibex.mail.target.dead'.
+All methods which create new messages (such as ibex.mail.message.clone())
+Also take a target -- the newly-created message is immediately directed
+to the target; the current script does not get access to it.
+
+The following properties are available from ibex.mail:
+
+ ibex.mail.
+
+ my. -- this can be read from but only written to if it is null
+ mailbox -- a mail storage object; subproperties are submailboxes; also qualifies as a Target
+ db -- a (read/write) properties file for the current user
+
+ freshMessageId -- returns a fresh message id every time it is read
+
+
+ props[path] -- reads a java-style properties file from [path] and allows read-write access
+ script[path] -- reads a script from 'path', parses it as a JS script, and passes the message to that script
+
+ filter.
+ anonymize
+ html2text
+ pgp
+ singleUseAddress
+ returnReciept
+ dcc
+ vipul
+ spamAssassin
+
+ target.
+ dead -- drops the message on the floor; it vanishes without a trace
+ dir[path] -- uses the directory [path] as a naive file-based storage area
+ send -- sends the outgoing message via smtp
+ bounce(message) -- bounces the message
+ vacation
+ darcs(repo)
+ procmail(procmailrc)
+ lmtp(path)
+ sms(phone#)
+ fax(phone#)
+ nntp(group)
+
+ message.
+ create -- creates a fresh Message object when read from
+ clone(m, t) -- makes a copy of message 'm' and sends it to target 't'
+ pipe(cmdline) -- pipes the message (headers and body) to the shell command 'cmdline'
+
+ crypto.
+ base36
+ .decode
+ .encode
+
+
+
+The following tables describe the keys on objects of various types.
+The type of an object is not visible from the JavaScript world; it is
+provided here only for documentation purposes.
+
+Note that, for example, m.headers.date is not the same as m.date; the
+former is a string; the latter is a Date object (with subfields)
+
+ Message
+ allHeaders [String] -- one massive string holding all the headers
+ headers [Hash] -- the *string* value of each header, keyed on header name
+ subject [String] -- the Subject: header (for convenience)
+ date [Date] -- the message's date
+ to [Address] -- the To: header as an Address object
+ from [Address] -- the From: header as an Address object
+ envelopeTo [Address] -- the SMTP "RCPT TO:" recipient
+ envelopeFrom [Address] -- the SMTP "MAIL FROM:" sender
+ replyto [Address] -- the ReplyTo: header as an Address object
+ messageid [String] -- the MessageId: header; if none is present one will be created
+ cc [Array] -- an array of Adress objects, one for each person cc'd
+ bcc [Array] -- an array of Adress objects, one for each person bcc'd
+ resent *FIXME*
+ traces [Array] -- an array of Trace objects
+
+ deleted [boolean] -- the message's deleted flag
+ read [boolean] -- the message's read flag
+ answered [boolean] -- the message's answered flag
+
+ Address
+ user [String] -- the 'user' part of 'foo <user@host.com>'
+ host [String] -- the 'host.com' part of 'foo <user@host.com>'
+ description [String] -- the 'foo' part of 'foo <user@host.com>'
+
+ Trace
+ returnPath [String] -- FIXME
+ FIXME
+
--- /dev/null
+
+public class MailException extends Exception {
+
+ public static class MailboxFull extends MailException { }
+ public static class RelayingDenied extends MailException { }
+ public static class IOException extends MailException {
+ // FIXME: fill in stack trace
+ final IOException ioe;
+ public IOException(java.io.IOException ioe) { this.ioe = ioe; }
+ }
+
+}
package org.ibex.mail;
+import org.ibex.crypto.*;
// FIXME MIME: RFC2045, 2046, 2049
// NOTE: always use Win32 line endings
// hard line limit: 998 chars
// FEATURE: mailing list header parsing
// FEATURE: delivery status notification (and the sneaky variety)
// FEATURE: threading as in http://www.jwz.org/doc/threading.html
-public class Message {
+
+public class Message extends JSReflection {
public final String allHeaders; // pristine headers
public final Hashtable headers; // hash of headers (not including resent's and traces)
public boolean deleted = false;
public boolean read = false;
public boolean answered = false;
+ public String dumpStoredForm() { throw new Error("StoredMessage.dumpStoredForm() not implemented"); };
}
- public static class Address {
+ public static class Address extends JSReflection {
+ public String coerceToString() {
+ if (description == null || description.equals("")) return user +"@"+ host;
+ return description + " " + "<" + user +"@"+ host + ">";
+ }
public final String user;
public final String host;
public final String description;
}
}
- public static class Base36 {
- public static String encode(long l) {
- StringBuffer ret = new StringBuffer();
- while (l > 0) {
- if ((l % 36) < 10) ret.append((char)(((int)'0') + (int)(l % 36)));
- else ret.append((char)(((int)'A') + (int)((l % 36) - 10)));
- l /= 36;
- }
- }
- }
-
- public Message(ReadStream rs) {
+ // FIXME: support dotTerminatedLikeSMTP
+ public Message(ReadStream rs, boolean dotTermiantedLikeSMTP) {
String key = null;
StringBuffer all = new StringBuffer();
for(String s = rs.readLine(); s != null && !s.equals(""); s = rs.readLine()) {
class IMAPException extends IOException {
}
-interface IMAP {
-}
-
-
public class IMAP extends MessageProtocol {
public static void main(String[] args) throws Exception {
--- /dev/null
+package org.ibex.mail.protocol;
+
+public class Incoming {
+
+ protected void accept(Message m) throws IOException {
+ // currently, we write all inbound messages to the transcript
+ MessageStore.transcript.add(m);
+
+ // FIXME: figure out where the message goes next
+ }
+
+}
public class SMTP extends MessageProtocol {
public SMTP() { setProtocolName("SMTP"); }
- public ServerRequest createRequest(Connection conn) { return new Request((TcpConnection)conn); }
+ public ServerRequest createRequest(Connection conn) { return new Listener((TcpConnection)conn); }
public static class Outgoing {
// recommended retry interval is 30 minutes
}
}
- private class Incoming implements ServerRequest {
+ private class Listener extends Incoming implements ServerRequest {
TcpConnection conn;
- public Incoming(TcpConnection conn) { this.conn = conn; conn.getSocket().setSoTimeout(5 * 60 * 1000); }
+ public Listener(TcpConnection conn) { this.conn = conn; conn.getSocket().setSoTimeout(5 * 60 * 1000); }
public void init() { }
public boolean handleRequest() throws IOException {
ws.println("354 Enter message, ending with \".\" on a line by itself");
StringBuffer data = new StringBuffer();
// move this into the RFC2822 class
- while(true) {
- String line = rs.readLine();
- if (line.equals(".")) break;
- if (line.startsWith("..")) line = line.substring(1);
- data.append(line);
+ boolean good = false;
+ try {
+ good = true;
+ Message m = new Message(line, true);
+ Target.default.accept(m);
+ } finally {
+ //ws.println("251 user not local; will forward");
+ if (good) ws.println("250 OK message accepted for delivery");
+ else { /* FIXME */ }
}
- // FIXME: commit message to disk here
- ws.println("250 OK message accepted for delivery");
- //ws.println("251 user not local; will forward");
} else if (command.toUpperCase().startsWith("HELP")) {
ws.println("214 sorry, you are beyond help. please see a trained professional.");
// FIXME: appallingly inefficient
public class MessageStore {
- private final String STORAGE_ROOT = System.getProperty("org.ibex.mail.MessageStore.ROOT", "/var/org.ibex.mail/");
- public final MessageStore root = new MessageStore(STORAGE_ROOT);
-
- private String path;
- private MessageStore(String path) throws IOException { new File(this.path = path).mkdirs(); }
- public MessageStore slash(String name) { return new MessageStore(path + "/" + name); }
-
- public int[] list() {
- String[] names = new File(path).list();
- int[] ret = new int[names.length];
- for(int i=0, j=0; j<ret.length; i++, j++) {
- try {
- ret[j] = Integer.parseInt(names[i].substring(0, names[i].length - 1));
- } catch (NumberFormatException nfe) {
- Log.warn(MessageStore.class, "NumberFormatException: " + names[i].substring(0, names[i].length - 1));
- j--;
- int[] newret = new int[ret.length - 1];
- System.arrayCopy(ret, 0, newret, 0, newret.length);
- ret = newret;
+ private final String STORAGE_ROOT = System.getProperty("ibex.mail.root",
+ File.separatorChar + "var" + File.separatorChar + "org.ibex.mail");
+
+ //public final FileBased root = new FileBased(STORAGE_ROOT + File.separatorChar);
+ public final FileBased transcript = new FileBased(STORAGE_ROOT + File.separatorChar + "transcript");
+
+ public MessageStore slash(String name) {
+ throw new Error(this.getClass().getName() + " does not support the slash() method"); }
+ public int[] list() { throw new Error(this.getClass().getName() + " does not support the list() method"); }
+ public int add(StoredMessage message) throws IOException {
+ throw new Error(this.getClass().getName() + " does not support the add() method"); }
+ public StoredMessage get(int messagenum) throws IOException {
+ throw new Error(this.getClass().getName() + " does not support the get() method"); }
+ public StoredMessage[] query(int maxResults) {
+ throw new Error(this.getClass().getName() + " does not support the query() method"); }
+
+ /** a fast-write, slow-read place to stash all messages we touch -- in case of a major f*ckup */
+ public static class Transcript {
+ private String path;
+ public Transcript(String path) throws IOException { new File(this.path = path).mkdirs(); }
+ private static String lastTime = null;
+ private static int lastCounter = 0;
+
+ /** returns a message identifier */
+ public synchronized int add(StoredMessage message) throws IOException {
+ File today = new File(path + File.separatorChar + (new SimpleDateFormat("yyyyy.MMMMM.dd").format(new Date())));
+ today.mkdirs();
+
+ String time = new SimpleDateFormat("").format(new Date("hh.mm.ss"));
+ synchronized (Transcript.class) {
+ if (lastTime != null && lastTime.equals(time)) {
+ time += "." + (++lastCounter);
+ } else {
+ lastTime = time;
+ }
}
+
+ File target = new File(today.getPath() + File.separatorChar() + time + ".txt");
+ String msg = message.dumpStoredForm();
+ OutputStream os = new FileOutputStream(target);
+ os.write(msg.getBytes("UTF-8")); // FIXME: right?
+ os.close();
+ return -1; // FIXME
}
- return ret;
}
- /** returns a message identifier */
- public synchronized int add(StoredMessage message) throws IOException {
- int[] all = list();
- int max = 0;
- for(int i=0; i<all.length; i++) max = Math.max(max, all[i]);
- int target = max++;
- File f = new File(path + File.separatorChar + max + ".-");
- FileOutputStream fo = new FileOutputStream(f);
- message.dump(fo);
- fo.close();
- f.renameTo(path + File.separatorChar + max + ".");
- return target;
- }
+ public static FileBased extends MessageStore {
+ private String path;
+ private FileBased(String path) throws IOException { new File(this.path = path).mkdirs(); }
+ public FileBased slash(String name) { return new FileBased(path + "/" + name); }
- public StoredMessage get(int messagenum) throws IOException {
- File f = new File(path + File.separatorChar + messagenum + ".");
- if (!f.exists()) throw new FileNotFoundException(f);
- return StoredMessage.undump(new FileInputStream(f));
- }
+ public int[] list() {
+ String[] names = new File(path).list();
+ int[] ret = new int[names.length];
+ for(int i=0, j=0; j<ret.length; i++, j++) {
+ try {
+ ret[j] = Integer.parseInt(names[i].substring(0, names[i].length - 1));
+ } catch (NumberFormatException nfe) {
+ Log.warn(FileBased.class, "NumberFormatException: " + names[i].substring(0, names[i].length - 1));
+ j--;
+ int[] newret = new int[ret.length - 1];
+ System.arrayCopy(ret, 0, newret, 0, newret.length);
+ ret = newret;
+ }
+ }
+ return ret;
+ }
+
+ /** returns a message identifier */
+ public synchronized int add(StoredMessage message) throws IOException {
+ int[] all = list();
+ int max = 0;
+ for(int i=0; i<all.length; i++) max = Math.max(max, all[i]);
+ int target = max++;
+ File f = new File(path + File.separatorChar + max + ".-");
+ FileOutputStream fo = new FileOutputStream(f);
+ message.dump(fo);
+ fo.close();
+ f.renameTo(path + File.separatorChar + max + ".");
+ return target;
+ }
+
+ public StoredMessage get(int messagenum) throws IOException {
+ File f = new File(path + File.separatorChar + messagenum + ".");
+ if (!f.exists()) throw new FileNotFoundException(f);
+ return StoredMessage.undump(new FileInputStream(f));
+ }
+
+ // query types: stringmatch (headers, body), header element, deletion status, date range, message size
+ public StoredMessage[] query(int maxResults) {
+ throw new RuntimeException("FileBased.query() not implemented yet");
+ }
- // query types: stringmatch (headers, body), header element, deletion status, date range, message size
- public StoredMessage[] query(int maxResults) {
- throw new RuntimeException("MessageStore.query() not implemented yet");
}
}
--- /dev/null
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.mail;
+import org.ibex.js.*;
+
+public class Script {
+
+ public static final JS root =
+ new Script(System.getProperty("ibex.mail.conf", File.separatorChar + "etc" + File.separatorChar + "org.ibex.mail.conf"));
+
+ final JS js;
+ public Script(String filePath) { js = JS.fromReader(CONF, 0, new InputStreamReader(new FileInputStream(CONF))); }
+
+ public void accept(Message m) throws IOException {
+ // currently, we write all inbound messages to the transcript
+ MessageStore.transcript.add(m);
+ Object ret = js.call(m);
+ if (ret instanceof Target) {
+ ((Target)ret).accept(m);
+ } else if (ret instanceof Filter) {
+ ((Filter)f).accept
+ } else {
+ throw new IOException("configuration script returned a " + ret.getClass().getName());
+ }
+ }
+
+ // FIXME: this should extend org.ibex.core.Ibex
+ public static class ScriptEnvironment extends JS {
+
+ // FIXME: duplicated code with org.ibex.core.Ibex; lift?
+ /** lets us put multi-level get/put/call keys all in the same method */
+ private class Sub extends JS {
+ String key;
+ Sub(String key) { this.key = key; }
+ public void put(Object key, Object val) throws JSExn { ScriptEnv.this.put(this.key + "." + key, val); }
+ public Object get(Object key) throws JSExn { return ScriptEnv.this.get(this.key + "." + key); }
+ public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+ return ScriptEnv.this.callMethod(this.key, a0, a1, a2, rest, nargs);
+ }
+ public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+ return ScriptEnv.this.callMethod(this.key + "." + method, a0, a1, a2, rest, nargs);
+ }
+ }
+ private Cache subCache = new Cache(20);
+ private Sub getSub(String s) {
+ Sub ret = (Sub)subCache.get(s);
+ if (ret == null) subCache.put(s, ret = new Sub(s));
+ return ret;
+ }
+
+ public Object get(Object name) throws JSExn {
+ if (name instanceof String && ((String)name).length() == 0) return rr;
+ //#switch(name)
+ case "math": return ibexMath;
+ case "string": return ibexString;
+ case "date": return METHOD;
+ case "regexp": return METHOD;
+ case "log": return getSub("log");
+ case "log.debug": return METHOD;
+ case "log.info": return METHOD;
+ case "log.warn": return METHOD;
+ case "log.error": return METHOD;
+ //#end
+ return super.get(name);
+ }
+
+ public Object callMethod(Object name, Object a, Object b, Object c, Object[] rest, int nargs) throws JSExn {
+ try {
+ //#switch(name)
+ case "date": return new JSDate(a, b, c, rest, nargs);
+ case "log.debug": JS.debug(a== null ? "**null**" : a.toString()); return null;
+ case "log.info": JS.info(a== null ? "**null**" : a.toString()); return null;
+ case "log.warn": JS.warn(a== null ? "**null**" : a.toString()); return null;
+ case "log.error": JS.error(a== null ? "**null**" : a.toString()); return null;
+ //#end
+ switch (nargs) {
+ case 1:
+ //#switch(name)
+ case "regexp": return new JSRegexp(a, null);
+ //#end
+ break;
+ case 2:
+ //#switch(name)
+ case "regexp": return new JSRegexp(a, b);
+ //#end
+ }
+ } catch (RuntimeException e) {
+ // FIXME: maybe JSExn should take a second argument, Exception
+ Log.warn(this, "ibex."+name+"() threw: " + e);
+ throw new JSExn("invalid argument for ibex object method "+name+"()");
+ }
+ throw new JSExn("invalid number of arguments ("+nargs+") for ibex object method "+name+"()");
+ }
+
+ public static final JSMath ibexMath = new JSMath() {
+ private JS gs = new JSScope.Global();
+ public Object get(Object key) throws JSExn {
+ //#switch(key)
+ case "isNaN": return gs.get("isNaN");
+ case "isFinite": return gs.get("isFinite");
+ case "NaN": return gs.get("NaN");
+ case "Infinity": return gs.get("Infinity");
+ //#end
+ return super.get(key);
+ }
+ };
+
+ public static final JS ibexString = new JS() {
+ private JS gs = new JSScope.Global();
+ public void put(Object key, Object val) { }
+ public Object get(Object key) throws JSExn {
+ //#switch(key)
+ case "parseInt": return gs.get("parseInt");
+ case "parseFloat": return gs.get("parseFloat");
+ case "decodeURI": return gs.get("decodeURI");
+ case "decodeURIComponent": return gs.get("decodeURIComponent");
+ case "encodeURI": return gs.get("encodeURI");
+ case "encodeURIComponent": return gs.get("encodeURIComponent");
+ case "escape": return gs.get("escape");
+ case "unescape": return gs.get("unescape");
+ case "fromCharCode": return gs.get("stringFromCharCode");
+ //#end
+ return null;
+ }
+ };
+ }
+}
package org.ibex.mail.target;
+
/** base class for mail message "destinations" */
public class Target {
+ public static final Target default = Script.root;
+ public void accept(Message m) { /* FIXME */ }
}