bogus
[org.ibex.mail.git] / src / org / ibex / mail / protocol / IMAP.java
index f6670f6..a354445 100644 (file)
@@ -1,4 +1,7 @@
 package org.ibex.mail.protocol;
+import org.ibex.io.*;
+import org.ibex.jinetd.Listener;
+import org.ibex.jinetd.Worker;
 import org.ibex.mail.*;
 import org.ibex.util.*;
 import org.ibex.mail.target.*;
@@ -6,7 +9,7 @@ import java.util.*;
 import java.net.*;
 import java.text.*;
 import java.io.*;
-
 // FIXME: this is valid    LSUB "" asdfdas%*%*%*%*SFEFGWEF
 // FIXME: be very careful about where/when we quotify stuff
 // FIXME: 'UID FOO 100:*' must match at least one message even if all UIDs less than 100
@@ -31,14 +34,11 @@ import java.io.*;
 
 public class IMAP {
 
+    public IMAP() { }
     public static final float version = (float)0.1;
 
     // API Class //////////////////////////////////////////////////////////////////////////////
 
-    public static final int
-        PEEK=0x1, BODYSTRUCTURE=0x2, ENVELOPE=0x4, FLAGS=0x8, INTERNALDATE=0x10, FIELDS=0x800, FIELDSNOT=0x1000,
-        RFC822=0x20, RFC822TEXT=0x40, RFC822SIZE=0x80, HEADERNOT=0x100, UID=0x200, HEADER=0x400;
-
     public static interface Client {
         public void expunge(int uid);
         public void list(char separator, String mailbox, boolean lsub, boolean phantom);
@@ -60,7 +60,7 @@ public class IMAP {
         public void      close();
         public void      subscribe(String mailbox);
         public void      unsubscribe(String mailbox);
-        public int       seen(String mailbox);
+        public int       unseen(String mailbox);
         public int       recent(String mailbox);
         public int       count(String mailbox);
         public int       uidNext(String mailbox);
@@ -157,21 +157,25 @@ public class IMAP {
         public void delete(String m0) { delete(mailbox(m0,false)); }
         public void delete(Mailbox m) { if (!m.equals(inbox)) m.destroy(false); else throw new Bad("can't delete inbox"); }
         public void create(String m) { mailbox(m, true); }
-        public void append(String m,int f,Date a,String b){mailbox(m,false).add(new Message(null,null,b,a),f|Mailbox.Flag.RECENT);}
+        public void append(String m,int f,Date a,String b) { try {
+            mailbox(m,false).add(new Message(null,null,b,a),f|Mailbox.Flag.RECENT);
+        } catch (Message.Malformed e) { throw new No(e.getMessage()); } }
         public void check() { }
         public void noop() { }
         public void logout() { }
-        public void close() { for(Mailbox.Iterator it=selected().iterator(Query.deleted()); it.next();) it.delete(); unselect(); }
+        public void close() { for(Mailbox.Iterator it=selected().iterator(Query.deleted()); it.next();) it.delete(); }
         public void expunge() { for(Mailbox.Iterator it = selected().iterator(Query.deleted());it.next();) expunge(it); }
         public void expunge(Mailbox.Iterator it) { client.expunge(it.uid()); it.delete(); }
         public void subscribe(String mailbox) { }
         public void unsubscribe(String mailbox) { }
-        public int seen(String mailbox)        { return mailbox(mailbox, false).count(Query.seen()); }
+        public int unseen(String mailbox)      { return mailbox(mailbox, false).count(Query.not(Query.seen())); }
         public int recent(String mailbox)      { return mailbox(mailbox, false).count(Query.recent()); }
         public int count(String mailbox)       { return mailbox(mailbox, false).count(Query.all()); }
         public int uidNext(String mailbox)     { return mailbox(mailbox, false).uidNext(); }
         public int uidValidity(String mailbox) { return mailbox(mailbox, false).uidValidity(); }
-        public void select(String mailbox, boolean examineOnly) { selected = mailbox(mailbox, false); }
+        public void select(String mailbox, boolean examineOnly) {
+           selected = mailbox(mailbox, false);
+       }
 
         public int[] search(Query q, boolean uid) {
             Vec.Int vec = new Vec.Int();
@@ -193,7 +197,8 @@ public class IMAP {
                 else if (style == 0) it.setFlags(flags);
                 else if (style == 1) it.addFlags(flags);
                 it.recent(recent);
-                if (!silent) client.fetch(it.num(), it.flags(), -1, null, it.uid());
+               // FIXME
+                //if (!silent) client.fetch(it.num(), it.flags(), -1, null, it.uid());
             }
         }            
         public void rename(String from0, String to) {
@@ -204,7 +209,10 @@ public class IMAP {
         }
         public void fetch(Query q, int spec, String[] headers, int start, int end, boolean uid) {
             for(Mailbox.Iterator it = selected().iterator(q); it.next(); ) {
-                client.fetch(it.num(), it.flags(), it.cur().rfc822size(), it.cur(), it.uid());
+                Message message = ((spec & (BODYSTRUCTURE | ENVELOPE | INTERNALDATE | FIELDS | FIELDSNOT | RFC822 |
+                                            RFC822TEXT | RFC822SIZE | HEADERNOT | HEADER)) != 0) ? it.cur() : null;
+                int size = message == null ? 0 : message.size();
+                client.fetch(it.num(), it.flags(), size, message, it.uid());
                 it.recent(false);
             }
         }
@@ -214,32 +222,120 @@ public class IMAP {
     // Single Session Handler //////////////////////////////////////////////////////////////////////////////
 
     /** takes an IMAP.Server and exposes it to the world as an IMAP server on a TCP socket */
-    public static class Listener extends Parser implements Client {
+    public static class Listener implements Worker, Client {
         String selectedName = null;
         Mailbox inbox = null, root = null;
-        final Server api;
-        public Listener(Socket conn, Server.Authenticator auth) throws IOException {
-            this(conn, java.net.InetAddress.getLocalHost().getHostName(), auth); }
-        public Listener(Socket conn, String vhost, Server.Authenticator auth) throws IOException {
-            super(conn, vhost);
-            this.api = new IMAP.MailboxWrapper(auth, this);
+        Server api;
+        Parser parser = null;
+        Connection conn = null;
+        public Listener() { }
+        Parser.Token token() { return parser.token(); }
+        void println(String s) { conn.println(s); }
+        void newline() { parser.newline(); }
+        Query query() { return parser.query(); }
+        public void handleRequest(Connection conn) {
+            this.conn = conn;
+            parser = new Parser(conn);
+            api = new IMAP.MailboxWrapper(new Main.BogusAuthenticator(), this);
+            conn.setTimeout(30 * 60 * 1000);
+            println("* OK " + conn.vhost + " " + IMAP.class.getName() + " IMAP4rev1 [RFC3501] v" + version + " server ready");
+            for(String tag = null;; newline()) try {
+                conn.flush();
+                boolean uid = false;
+                tag = null; Parser.Token tok = token(); if (tok == null) return; tag = tok.astring();
+                String command = token().atom();
+                if (command.equalsIgnoreCase("UID")) { uid = true; command = token().atom(); }
+                int commandKey = ((Integer)commands.get(command.toUpperCase())).intValue();
+                switch(commandKey) {
+                    case LOGIN:        api.login(token().astring(), token().astring()); break;
+                    case CAPABILITY:   println("* CAPABILITY " + Printer.join(" ", api.capability())); break;
+                    case AUTHENTICATE: throw new Server.No("AUTHENTICATE not supported");
+                    case LOGOUT:       api.logout(); println("* BYE"); conn.close(); return;
+                    case LIST:         api.list(token().q(), token().q()); break;
+                    case LSUB:         api.lsub(token().q(), token().q()); break;
+                    case SUBSCRIBE:    api.subscribe(token().astring()); break;
+                    case UNSUBSCRIBE:  api.unsubscribe(token().astring()); break;
+                    case RENAME:       api.rename(token().astring(), token().astring()); break;
+                    case COPY:         selected(); api.copy(Query.set(uid, token().set()), token().astring()); break;
+                    case DELETE:       api.delete(token().atom()); break;
+                    case CHECK:        selected(); api.check(); break;
+                    case NOOP:         api.noop(); break;
+                    case CLOSE:        selected(); api.close(); break;
+                    case EXPUNGE:      selected(); api.expunge(); break;
+                    case UNSELECT:     selected(); api.unselect(); selected = false; break;
+                    case CREATE:       api.create(token().astring()); break;
+                    case FETCH:        selected(); fetch(Query.set(lastuid=uid, token().set()),
+                                                        lastfetch=token().lx(), 0, 0, 0, uid, 0); break;
+                    case SEARCH:       selected(); println("* SEARCH " + Printer.join(api.search(query(), uid))); break;
+                    case EXAMINE:
+                    case SELECT: {
+                        String mailbox = token().astring();
+                        api.select(mailbox, commandKey==EXAMINE);
+                        println("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)");
+                        println("* " + api.count(mailbox)  + " EXISTS");
+                        println("* " + api.recent(mailbox) + " RECENT");
+                        println("* OK [UIDVALIDITY " + api.uidValidity(mailbox) + "] UIDs valid");
+                        println("* OK [UIDNEXT " + api.uidNext(mailbox) + "]");
+                        println("* OK [PERMANENTFLAGS (\\Seen \\Draft \\Answered \\Deleted)]");
+                        selected = true;
+                        break; }
+                    case STATUS: {
+                        String mailbox = token().astring();  // hack for GNUS buggy client
+                        Parser.Token[] list = token().l();
+                        String response = "";
+                        for(int i=0; i<list.length; i++) {
+                            String s = list[i].atom().toUpperCase();
+                            if (i>0) response += " ";
+                            if (s.equals("MESSAGES"))    response += "MESSAGES "    + api.count(mailbox);
+                            if (s.equals("RECENT"))      response += "RECENT "      + api.recent(mailbox);
+                            if (s.equals("UIDNEXT"))     response += "UIDNEXT "     + api.uidNext(mailbox);
+                            if (s.equals("UIDVALIDITY")) response += "UIDVALIDITY " + api.uidValidity(mailbox);
+                            if (s.equals("UNSEEN"))      response += "UNSEEN "      + api.unseen(mailbox);
+                        }
+                        println("* STATUS " + mailbox + " (" + response + ")");
+                        break;
+                    }
+                    case APPEND: { 
+                        String m = token().astring();
+                        int flags = 0;
+                        Date arrival = new Date();
+                        Parser.Token t = token();
+                        if (t.type == t.LIST)   { flags = t.flags();      t = token(); }
+                        if (t.type != t.QUOTED) { arrival = t.datetime(); t = token(); }
+                        api.append(m, flags, arrival, t.q());
+                        break; }
+                    case STORE: {
+                        selected();
+                        Query q = uid ? Query.uid(token().set()) : Query.num(token().set());
+                        String s = token().atom().toUpperCase();
+                        int flags = token().flags();
+                        if (s.equals("FLAGS"))              api.setFlags(q,    flags, uid, false);
+                        else if (s.equals("+FLAGS"))        api.addFlags(q,    flags, uid, false);
+                        else if (s.equals("-FLAGS"))        api.removeFlags(q, flags, uid, false);
+                        else if (s.equals("FLAGS.SILENT"))  api.setFlags(q,    flags, uid, true);
+                        else if (s.equals("+FLAGS.SILENT")) api.addFlags(q,    flags, uid, true);
+                        else if (s.equals("-FLAGS.SILENT")) api.removeFlags(q, flags, uid, true);
+                        else throw new Server.Bad("unknown STORE specifier " + s);
+                        break; }
+                    default: throw new Server.Bad("unrecognized command \"" + command + "\"");
+                }
+                println(tag+" OK "+command+" Completed. " +
+                        (commandKey == LOGIN ? ("[CAPABILITY "+Printer.join(" ", api.capability())+"]") : ""));
+            } catch (Server.Bad b) { println(tag==null ? "* BAD Invalid tag":(tag + " Bad " + b.toString())); Log.warn(this,b);
+            } catch (Server.No n)  { println(tag==null?"* BAD Invalid tag":(tag+" No "  + n.toString())); Log.warn(this,n); }
         }
 
-        private Token[] lastfetch = null; // hack
+        private Parser.Token[] lastfetch = null; // hack
         private boolean lastuid = false;  // hack
-
         private boolean selected = false;
         private void selected() { if (!selected) throw new Server.Bad("no mailbox selected"); }
-        private void star(String s) { try { println("* " + s); } catch (IOException e) { throw new MailException.IOException(e); } }
-        private String qq(String s) { return Printer.qq(s); }
 
         // Callbacks //////////////////////////////////////////////////////////////////////////////
 
-        public void expunge(int uid) { star(uid + " EXPUNGE"); }
-        public void list(char sep, String box, boolean lsub, boolean phantom)
-        {star((lsub?"LSUB":"LIST")+" ("+(phantom?"\\Noselect":"")+") \""+sep+"\" \""+box+"\"");}
-        public void fetch(int num, int flags, int size, Message m, int muid) {
-            fetch(m, lastfetch, num, flags, size, lastuid, muid); }
+        public void expunge(int uid) { println("* " + uid + " EXPUNGE"); }
+        public void fetch(int n, int f, int size, Message m, int muid) { fetch(m, lastfetch, n, f, size, lastuid, muid); }
+        public void list(char sep, String mb, boolean sub, boolean p) {
+            println("* " + (sub?"LSUB":"LIST")+" ("+(p?"\\Noselect":"")+") \""+sep+"\" \""+mb+"\"");}
 
         /**
          *   Parse a fetch request <i>or</i> emit a fetch reply.
@@ -249,10 +345,10 @@ public class IMAP {
          *      - parse the fetch request in Token[] t and return a fetch spec
          *      - emit a fetch reply for the parsed spec with respect to message m
          */
-        private void fetch(Object o, Token[] t, int num, int flags, int size, boolean uid, int muid) {
+        private void fetch(Object o, Parser.Token[] t, int num, int flags, int size, boolean uid, int muid) {
             Query q   = o instanceof Query ? (Query)o : null;
             Message m = o instanceof Message ? (Message)o : null;
-            boolean e = m != null;
+            boolean e = q == null;
 
             lastfetch = t;
             int spec = 0;                              // spec; see constants for flags
@@ -264,25 +360,25 @@ public class IMAP {
             if (uid) {
                 boolean good = false;
                 for(int i=0; i<t.length; i++)
-                    if ((t[i].type == Token.QUOTED || t[i].type == Token.ATOM) &&
+                    if ((t[i].type == Parser.Token.QUOTED || t[i].type == Parser.Token.ATOM) &&
                         t[i].astring().equalsIgnoreCase("UID")) good = true;
                 if (!good) {
-                    Token[] t2 = new Token[t.length + 1];
+                    Parser.Token[] t2 = new Parser.Token[t.length + 1];
                     System.arraycopy(t, 0, t2, 0, t.length);
-                    t2[t2.length - 1] = new Token("UID");
-                    t = t2;
+                    t2[t2.length - 1] = parser.token("UID");
+                    lastfetch = (t = t2);
                 }
             }
-            if (t.length == 0 && (t[0].type == Token.QUOTED || t[0].type == Token.ATOM)) {
+            if (t.length == 0 && (t[0].type == Parser.Token.QUOTED || t[0].type == Parser.Token.ATOM)) {
                 if (t[0].astring().equalsIgnoreCase("ALL"))
-                    t = new Token[] { new Token("FLAGS"), new Token("INTERNALDATE"),
-                                      new Token("ENVELOPE"), new Token("RFC822.SIZE") };
+                    t = new Parser.Token[] { parser.token("FLAGS"), parser.token("INTERNALDATE"),
+                                      parser.token("ENVELOPE"), parser.token("RFC822.SIZE") };
                 else if (t[0].astring().equalsIgnoreCase("FULL"))
-                    t = new Token[] { new Token("FLAGS"), new Token("INTERNALDATE"), new Token("BODY"), 
-                                      new Token("ENVELOPE"), new Token("RFC822.SIZE") };
+                    t = new Parser.Token[] { parser.token("FLAGS"), parser.token("INTERNALDATE"), parser.token("BODY"), 
+                                      parser.token("ENVELOPE"), parser.token("RFC822.SIZE") };
                 else if (t[0].astring().equalsIgnoreCase("FAST"))
-                    t = new Token[] { new Token("FLAGS"), new Token("INTERNALDATE"),
-                                      new Token("RFC822.SIZE") };
+                    t = new Parser.Token[] { parser.token("FLAGS"), parser.token("INTERNALDATE"),
+                                      parser.token("RFC822.SIZE") };
             }
             for(int i=0; i<t.length; i++) {
                 if (r.length() > initlen) r.append(" ");
@@ -294,23 +390,26 @@ public class IMAP {
                 } else if (s.equals("FLAGS")) {        spec|=FLAGS;        if(e){r.append(" ");r.append(Printer.flags(flags));}
                 } else if (s.equals("INTERNALDATE")) { spec|=INTERNALDATE; if(e){r.append(" ");r.append(Printer.date(m.arrival));}
                 } else if (s.equals("RFC822")) {       spec|=RFC822;       if(e){r.append(" ");r.append(Printer.message(m));}
-                } else if (s.equals("RFC822.TEXT")) {  spec|=RFC822TEXT;   if(e){r.append(" ");r.append(qq(m.body));}
-                } else if (s.equals("RFC822.HEADER")){ spec|=HEADER; if(e){r.append(" ");r.append(qq(m.allHeaders+"\r\n"));}
-                } else if (s.equals("RFC822.SIZE")) {  spec|=RFC822SIZE;   if(e){r.append(" ");r.append(m.rfc822size());}
+                } else if (s.equals("RFC822.TEXT")) {  spec|=RFC822TEXT;   if(e){r.append(" ");r.append(Printer.qq(m.body));}
+                } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;if(e){r.append(" ");r.append(Printer.qq(m.allHeaders+"\r\n"));}
+                } else if (s.equals("RFC822.SIZE")) {  spec|=RFC822SIZE;   if(e){r.append(" ");r.append(m.size());}
                 } else if (s.equals("UID")) {          spec|=UID;          if(e){r.append(" ");r.append(muid); }
                 } else if (!(s.equals("BODY.PEEK") || s.equals("BODY"))) { throw new Server.No("unknown fetch argument: " + s);
                 } else {
                     if (s.equalsIgnoreCase("BODY.PEEK"))   spec |= PEEK;
-                    else if (e) api.addFlags(Query.num(new int[] { num, num }), Mailbox.Flag.SEEN, false, false);
-                    if (!(i<t.length - 1 && (t[i+1].type == Token.LIST)))
-                       {if (e){r.append(" ");r.append(qq(m.body));} continue; }
+                    //else if (e) api.addFlags(Query.num(new int[] { num, num }), Mailbox.Flag.SEEN, false, false);
+                    if (i >= t.length - 1 || t[i+1].type != Parser.Token.LIST) {
+                       spec |= BODYSTRUCTURE;
+                       if (e) { r.append(" "); r.append(Printer.bodystructure(m)); } continue;
+                       //{ if (e) { r.append(" "); r.append(Printer.qq(m.body)); } continue; }
+                   }
                     String payload = "";
                     r.append("[");
-                    Token[] list = t[++i].l();
+                    Parser.Token[] list = t[++i].l();
                     s = list.length == 0 ? "" : list[0].s.toUpperCase();
                     r.append(s);
-                    if (list.length == 0)                   { spec |= RFC822TEXT;   if(e) payload = m.body; }
-                    else if (s.equals(""))                  { spec |= RFC822TEXT;   if(e) payload = m.body; }
+                    if (list.length == 0)                   { spec |= RFC822TEXT;   if(e) payload = m.allHeaders+"\r\n"+m.body; }
+                    else if (s.equals(""))                  { spec |= RFC822TEXT;   if(e) payload = m.allHeaders+"\r\n"+m.body; }
                     else if (s.equals("TEXT"))              { spec |= RFC822TEXT;   if(e) payload = m.body; }
                     else if (s.equals("HEADER"))            { spec |= HEADER;       if(e) payload = m.allHeaders+"\r\n"; }
                     else if (s.equals("HEADER.FIELDS"))     { spec |= FIELDS;     payload=headers(r,t[i].l()[1].sl(),false,m,e); }
@@ -325,12 +424,12 @@ public class IMAP {
                         end = dot == -1 ? -1 : Integer.parseInt(s.substring(s.indexOf('.') + 1));
                         if (e) { payload = payload.substring(start, Math.min(end+1,payload.length())); r.append("<"+start+">"); }
                     }
-                    if (e) { r.append(" "); r.append(qq(payload)); }
+                    if (e) { r.append("] "); r.append(Printer.qq(payload)); }
                 }
             }
             if (e) {
                r.append(")");
-               star(r.toString());
+               println("* " + r.toString());
            } else {
                api.fetch(q, spec, headers, start, end, uid);
            }
@@ -345,104 +444,20 @@ public class IMAP {
                     if (m.headers.get(headers[j]) != null) payload += headers[j]+": "+m.headers.get(headers[j])+"\r\n";
                 }
             } else {
+               throw new Server.No("HEADERS.NOT temporarily disaled");
+               /*
                 if (e) for(int j=0; j<headers.length; j++) r.append(headers[j] + (j<headers.length-1?" ":""));
                 if(e) { OUTER: for(Enumeration x=m.headers.keys(); x.hasMoreElements();) {
                     String key = (String)x.nextElement();
                     for(int j=0; j<headers.length; j++) if (key.equalsIgnoreCase(headers[j])) continue OUTER;
                     payload += key + ": " + m.headers.get(key)+"\r\n";
                 } }
+               */
             }
-            if (e) r.append(")]");
+            if (e) r.append(")");
             return payload + "\r\n";
         }
 
-        public boolean handleRequest() throws IOException {
-            star("OK " + vhost + " " + IMAP.class.getName() + " IMAP4rev1 [RFC3501] v" + version + " server ready");
-            for(String tag = null;; newline()) try {
-                flush();
-                boolean uid = false;
-                tag = null; tag = token().astring();
-                String command = token().atom();
-                if (command.equalsIgnoreCase("UID")) { uid = true; command = token().atom(); }
-                int commandKey = ((Integer)commands.get(command.toUpperCase())).intValue();
-                switch(commandKey) {
-                    case LOGIN:        api.login(token().astring(), token().astring()); break;
-                    case CAPABILITY:   star("CAPABILITY " + Printer.join(" ", api.capability())); break;
-                    case AUTHENTICATE: throw new Server.No("AUTHENTICATE not supported");
-                    case LOGOUT:       api.logout(); star("BYE"); conn.close(); return false;
-                    case LIST:         api.list(token().q(), token().q()); break;
-                    case LSUB:         api.lsub(token().q(), token().q()); break;
-                    case SUBSCRIBE:    api.subscribe(token().astring()); break;
-                    case UNSUBSCRIBE:  api.unsubscribe(token().astring()); break;
-                    case RENAME:       api.rename(token().astring(), token().astring()); break;
-                    case EXAMINE:      api.select(token().astring(), true); break;
-                    case COPY:         selected(); api.copy(Query.set(uid, token().set()), token().astring()); break;
-                    case DELETE:       api.delete(token().atom()); break;
-                    case CHECK:        selected(); api.check(); break;
-                    case NOOP:         api.noop(); break;
-                    case CLOSE:        selected(); api.close(); break;
-                    case EXPUNGE:      selected(); api.expunge(); break;
-                    case UNSELECT:     selected(); api.unselect(); selected = false; break;
-                    case CREATE:       api.create(token().astring()); break;
-                    case FETCH:        selected(); fetch(Query.set(lastuid=uid, token().set()),
-                                                        lastfetch=token().l(), 0, 0, 0, uid, 0); break;
-                    case SEARCH:       selected(); star("SEARCH " + Printer.join(api.search(query(), uid))); break;
-                    case SELECT: {
-                        String mailbox = token().astring();
-                        api.select(mailbox, false);
-                        star("FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)");
-                        star(api.count(mailbox)  + " EXISTS");
-                        star(api.recent(mailbox) + " RECENT");
-                        star("OK [UIDVALIDITY " + api.uidValidity(mailbox) + "] UIDs valid");
-                        star("OK [UIDNEXT " + api.uidNext(mailbox) + "]");
-                        star("OK [PERMANENTFLAGS (\\Seen \\Draft \\Answered \\Deleted)]");
-                        selected = true;
-                        break; }
-                    case STATUS: {
-                        String mailbox = token().atom();
-                        Token[] list = token().l();
-                        String response = "";
-                        for(int i=0; i<list.length; i++) {
-                            String s = list[i].atom().toUpperCase();
-                            if (s.equals("MESSAGES"))    response += "MESSAGES "    + api.count(mailbox);
-                            if (s.equals("RECENT"))      response += "RECENT "      + api.seen(mailbox);
-                            if (s.equals("UIDNEXT"))     response += "UNSEEN "      + api.recent(mailbox);
-                            if (s.equals("UIDVALIDITY")) response += "UIDVALIDITY " + api.uidValidity(mailbox);
-                            if (s.equals("UNSEEN"))      response += "UIDNEXT "     + api.uidNext(mailbox);
-                        }
-                        star("STATUS " + selectedName + " (" + response + ")");
-                        break;
-                    }
-                    case APPEND: { 
-                        String m = token().atom();
-                        int flags = 0;
-                        Date arrival = null;
-                        Token t = token();
-                        if (t.type == t.LIST)   { flags = t.flags();      t = token(); }
-                        if (t.type == t.QUOTED) { arrival = t.datetime(); t = token(); }
-                        api.append(m, flags, arrival, token().q());
-                        break; }
-                    case STORE: {
-                        selected();
-                        Query q = uid ? Query.uid(token().set()) : Query.num(token().set());
-                        String s = token().atom().toUpperCase();
-                        int flags = token().flags();
-                        if (s.equals("FLAGS"))              api.setFlags(q,    flags, uid, false);
-                        else if (s.equals("+FLAGS"))        api.addFlags(q,    flags, uid, false);
-                        else if (s.equals("-FLAGS"))        api.removeFlags(q, flags, uid, false);
-                        else if (s.equals("FLAGS.SILENT"))  api.setFlags(q,    flags, uid, true);
-                        else if (s.equals("+FLAGS.SILENT")) api.addFlags(q,    flags, uid, true);
-                        else if (s.equals("-FLAGS.SILENT")) api.removeFlags(q, flags, uid, true);
-                        else throw new Server.Bad("unknown STORE specifier " + s);
-                        break; }
-                    default: throw new Server.Bad("unrecognized command \"" + command + "\"");
-                }
-                println(tag+" OK "+command+" Completed. " +
-                        (commandKey == LOGIN ? ("[CAPABILITY "+Printer.join(" ", api.capability())+"]") : ""));
-            } catch (Server.Bad b) { println(tag==null ? "* BAD Invalid tag":(tag + " Bad " + b.toString())); b.printStackTrace();
-            } catch (Server.No n)  { println(tag==null?"* BAD Invalid tag":(tag+" No "  + n.toString())); n.printStackTrace(); }
-        }
-
         private static final Hashtable commands = new Hashtable();
         private static final int UID = 0;          static { commands.put("UID", new Integer(UID)); }
         private static final int AUTHENTICATE = 1; static { commands.put("AUTHENTICATE", new Integer(AUTHENTICATE)); }
@@ -472,21 +487,23 @@ public class IMAP {
         private static final int SEARCH = 25;      static { commands.put("SEARCH", new Integer(SEARCH)); }
     }
 
-    public abstract static class Parser extends Connection {
-        public Parser(Socket conn, String vhost) throws IOException { super(conn, vhost); }
+    public static class Parser {
+        private Stream stream;
+        public Parser(Stream from) { this.stream = from; }
+        public Token token(String s) { return new Token(s); }
         protected Query query() {
             String s = null;
             boolean not = false;
             Query q = null;
             while(true) {
-                Token t = token();
+                Parser.Token t = token(false);
+                if (t == null) break;
                 if (t.type == t.LIST) throw new Server.No("nested queries not yet supported FIXME");
                 else if (t.type == t.SET) return Query.num(t.set());
                 s = t.atom().toUpperCase();
                 if (s.equals("NOT")) { not = true; continue; }
                 if (s.equals("OR"))    return Query.or(query(), query());    // FIXME parse rest of list
                 if (s.equals("AND"))   return Query.and(query(), query());
-                break;
             }
             if (s.startsWith("UN"))        { not = true; s = s.substring(2); }
             if (s.equals("ANSWERED"))        q = Query.answered();
@@ -523,20 +540,33 @@ public class IMAP {
         class Token {
             public final byte type;
             private final String s;
-            private final Token[] l;
+            private final Parser.Token[] l;
             private final int n;
             private static final byte NIL = 0, LIST = 1, QUOTED = 2, NUMBER = 3, ATOM = 4, BAREWORD = 5, SET = 6;
             public Token()                         { this.s = null; n = 0;      l = null; type = NIL; }
             public Token(String s)                 { this(s, false); }
             public Token(String s, boolean quoted) { this.s = s;    n = 0;      l = null; type = quoted ? QUOTED : ATOM;  }
-            public Token(Token[] list)             { this.s = null; n = 0;      l = list; type = LIST; }
+            public Token(Parser.Token[] list)             { this.s = null; n = 0;      l = list; type = LIST; }
             public Token(int number)               { this.s = null; n = number; l = null; type = NUMBER; }
 
             public String   flag()    { if (type != ATOM) bad("expected a flag"); return s; }
             public int      n()       { if (type != NUMBER) bad("expected number"); return n; }
             public int      nz()      { int n = n(); if (n == 0) bad("expected nonzero number"); return n; }
             public String   q()       { if (type == NIL) return null; if (type != QUOTED) bad("expected qstring"); return s; }
-            public Token[]  l()       { if (type == NIL) return null; if (type != LIST) bad("expected list"); return l; }
+            public Parser.Token[]  l()       { if (type == NIL) return null; if (type != LIST) bad("expected list"); return l; }
+            public Parser.Token[]  lx()      {
+               if (type == LIST) return l;
+               Vec v = new Vec();
+               v.addElement(this);
+               while(true) {
+                   Parser.Token t = token(false);
+                   if (t == null) break;
+                   v.addElement(t);
+               }
+               Parser.Token[] ret = new Parser.Token[v.size()];
+               v.copyInto(ret);
+               return ret;
+           }
             public String   nstring() { if (type==NIL) return null; if (type!=QUOTED) bad("expected nstring"); return s; }
             public String   astring() {
                 if (type != ATOM && type != QUOTED) bad("expected atom or string");
@@ -570,8 +600,13 @@ public class IMAP {
                 while(st.hasMoreTokens()) {
                     String s = st.nextToken();
                     if (s.indexOf(':') == -1) {
-                       ids.addElement(Integer.parseInt(s));
-                       ids.addElement(Integer.parseInt(s));
+                       if (s.equals("*")) {
+                           ids.addElement(0);
+                           ids.addElement(Integer.MAX_VALUE);
+                       } else {
+                           ids.addElement(Integer.parseInt(s));
+                           ids.addElement(Integer.parseInt(s));
+                       }
                        continue; }
                     int start = Integer.parseInt(s.substring(0, s.indexOf(':')));
                     String end_s = s.substring(s.indexOf(':')+1);
@@ -607,30 +642,38 @@ public class IMAP {
             }
         }
 
-        // FIXME: IOException handling
-        public void newline() { try {
-            for(char c = peekc(); c == ' ';) { getc(); c = peekc(); };
-            for(char c = peekc(); c == '\r' || c == '\n';) { getc(); c = peekc(); };
-        } catch (IOException e) { e.printStackTrace(); } } 
+        public void newline() {
+           while (stream.peekc() == '\r' || stream.peekc() == '\n' || stream.peekc() == ' ') {
+               for(char c = stream.peekc(); c == ' ';) { stream.getc(); c = stream.peekc(); };
+               for(char c = stream.peekc(); c == '\r' || c == '\n';) { stream.getc(); c = stream.peekc(); };
+           }
+        }
 
-        public Token token() { try {
+        public Token token() { return token(true); }
+        public Token token(boolean freak) {
             Vec toks = new Vec();
             StringBuffer sb = new StringBuffer();
-            char c = getc(); while (c == ' ') c = getc();
-            if (c == '\r' || c == '\n') bad("unexpected end of line");
+            char c = stream.getc(); while (c == ' ') c = stream.getc();
+            if (c == '\r' || c == '\n') { if (freak) bad("unexpected end of line"); return null; }
             else if (c == '{') {
-                while(peekc() != '}') sb.append(getc());
-                println("+ Ready when you are...");
+                while(stream.peekc() != '}') sb.append(stream.getc());
+                stream.getc();
+                stream.println("+ Ready when you are...");
                 int octets = Integer.parseInt(sb.toString());
-                while(peekc() == ' ') getc();   // whitespace
-                while (getc() != '\n' && getc() != '\r') { }
+                while(stream.peekc() == ' ') stream.getc();   // whitespace
+                while(stream.peekc() == '\n' || stream.peekc() == '\r') stream.getc();
                 byte[] bytes = new byte[octets];
-                fill(bytes);
+                int numread = 0;
+                while(numread < bytes.length) {
+                    int n = stream.read(bytes, numread, bytes.length - numread);
+                    if (n == -1) bad("end of stream while reading IMAP qstring");
+                    numread += n;
+                }
                 return new Token(new String(bytes), true);
             } else if (c == '\"') {
                 while(true) {
-                    c = getc();
-                    if (c == '\\') sb.append(getc());
+                    c = stream.getc();
+                    if (c == '\\') sb.append(stream.getc());
                     else if (c == '\"') break;
                     else sb.append(c);
                 }
@@ -647,13 +690,13 @@ public class IMAP {
                 
             } else while(true) {
                 sb.append(c);
-                c = peekc();
+                c = stream.peekc();
                 if (c == ' ' || c == '\"' || c == '(' || c == ')' || c == '[' || c == ']' ||
                     c == '{' || c == '\n' || c == '\r')
                     return new Token(sb.toString(), false);
-                getc();
+                stream.getc();
             }
-        } catch (IOException e) { Log.warn(this, e); } return null; }
+        }
     }
     
     public static class Printer {
@@ -695,9 +738,9 @@ public class IMAP {
         }
         static String bodystructure(Message m) {
             // FIXME
-            return "(\"TEXT\" \"PLAIN\" () NIL NIL \"7BIT\" "+m.rfc822size()+" "+m.lines+")";
+            return "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"7BIT\" "+m.size()+" "+m.lines+")";
         }
-        static String message(Message m) { return m.rfc822(); }
+        static String message(Message m) { return m.toString(); }
         static String date(Date d) { return d.toString(); }
         static String envelope(Message m) {
             return
@@ -715,7 +758,7 @@ public class IMAP {
         }
         
         public static String qq(String s) {
-            StringBuffer ret = new StringBuffer(s.length() + 20);
+            StringBuffer ret = new StringBuffer();
             ret.append('{');
             ret.append(s.length());
             ret.append('}');
@@ -753,13 +796,12 @@ public class IMAP {
             final Socket s = ss.accept();
             new Thread() { public void run() { try {
                 final Mailbox root = FileBasedMailbox.getFileBasedMailbox(Mailbox.STORAGE_ROOT+File.separatorChar+"imap", true);
-                new Listener(s,
-                            new Server.Authenticator() {
-                                public Mailbox authenticate(String u, String p) {
-                                    if (u.equals("megacz")&&p.equals("pass")) return root;
-                                    return null;
-                                } } ).handleRequest();
+                new Listener();
             } catch (Exception e) { e.printStackTrace(); } } }.start();
         }
     }
+
+    public static final int
+        PEEK=0x1, BODYSTRUCTURE=0x2, ENVELOPE=0x4, FLAGS=0x8, INTERNALDATE=0x10, FIELDS=0x800, FIELDSNOT=0x1000,
+        RFC822=0x20, RFC822TEXT=0x40, RFC822SIZE=0x80, HEADERNOT=0x100, UID=0x200, HEADER=0x400;
 }