added tons of stuff, including js support
authoradam <adam@megacz.com>
Sun, 2 May 2004 07:36:50 +0000 (07:36 +0000)
committeradam <adam@megacz.com>
Sun, 2 May 2004 07:36:50 +0000 (07:36 +0000)
darcs-hash:20040502073650-5007d-d7d14fa7487b0a3380a57f5dfcf8f1f3f0c96a51.gz

doc/README
src/org/ibex/mail/MailException.java [new file with mode: 0644]
src/org/ibex/mail/Message.java
src/org/ibex/mail/protocol/IMAP.java
src/org/ibex/mail/protocol/Incoming.java [new file with mode: 0644]
src/org/ibex/mail/protocol/SMTP.java
src/org/ibex/mail/store/MessageStore.java
src/org/ibex/mail/target/Script.java [new file with mode: 0644]
src/org/ibex/mail/target/Target.java

index c5bd6cc..e26737a 100644 (file)
@@ -1,7 +1,6 @@
 ==============================================================================
 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
@@ -15,3 +14,101 @@ including:
    - 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
+
diff --git a/src/org/ibex/mail/MailException.java b/src/org/ibex/mail/MailException.java
new file mode 100644 (file)
index 0000000..b869042
--- /dev/null
@@ -0,0 +1,12 @@
+
+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; }
+    }
+
+}
index 22d94dd..3effcfa 100644 (file)
@@ -1,4 +1,5 @@
 package org.ibex.mail;
+import org.ibex.crypto.*;
 // FIXME MIME: RFC2045, 2046, 2049
 // NOTE: always use Win32 line endings
 // hard line limit: 998 chars
@@ -13,7 +14,8 @@ package org.ibex.mail;
 // 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)
@@ -39,9 +41,14 @@ public class Message {
         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;
@@ -75,18 +82,8 @@ public class Message {
         }
     }
 
-    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()) {
index a504848..fc72869 100644 (file)
@@ -5,10 +5,6 @@ import java.io.*;
 class IMAPException extends IOException {
 }
 
-interface IMAP {
-}
-
-
 public class IMAP extends MessageProtocol {
 
     public static void main(String[] args) throws Exception {
diff --git a/src/org/ibex/mail/protocol/Incoming.java b/src/org/ibex/mail/protocol/Incoming.java
new file mode 100644 (file)
index 0000000..42c5f9a
--- /dev/null
@@ -0,0 +1,12 @@
+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
+    }
+
+}
index 0074285..82838a5 100644 (file)
@@ -2,7 +2,7 @@ package org.ibex.mail.protocol;
 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
@@ -28,9 +28,9 @@ public class SMTP extends MessageProtocol {
         }
     }
 
-    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 {
@@ -86,15 +86,16 @@ public class SMTP extends MessageProtocol {
                     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.");
index d2aec48..2076e19 100644 (file)
@@ -6,53 +6,99 @@ import java.net.*;
 // 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");
     }
 
 }
diff --git a/src/org/ibex/mail/target/Script.java b/src/org/ibex/mail/target/Script.java
new file mode 100644 (file)
index 0000000..06c14ee
--- /dev/null
@@ -0,0 +1,126 @@
+// 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;
+                }
+            };
+    }
+}
index 97e3eb1..4b16725 100644 (file)
@@ -1,4 +1,7 @@
 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 */ }
 }