line break
[org.ibex.mail.git] / src / org / ibex / mail / IMAP.java
index 9038cc1..27e6643 100644 (file)
@@ -14,6 +14,8 @@ import java.net.*;
 import java.text.*;
 import java.io.*;
  
+// FEATURE: IDLE extension for blackberries
+
 // 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
@@ -100,7 +102,7 @@ public class IMAP {
 
         Mailbox inbox = null;
         Mailbox selected = null;
-        Mailbox root = null;
+        MailTree root = null;
         Mailbox selected() { if (selected == null) throw new Bad("no mailbox selected"); return selected; }
         final Login auth;
         final Client client;
@@ -108,10 +110,22 @@ public class IMAP {
         public MailboxWrapper(Login auth, Client c) { this.auth=auth; this.client=c;}
         public void setClient(IMAP.Client client) { }
 
+        private String dirname(String name) { return name.substring(0, name.lastIndexOf(sep)); }
+        private String basename(String name) { return name.substring(name.lastIndexOf(sep)+1); }
         private Mailbox mailbox(String name, boolean create) { return mailbox(name, create, true); }
         private Mailbox mailbox(String name, boolean create, boolean throwexn) {
             if (name.equalsIgnoreCase("inbox")) return inbox;
-            Mailbox m = root;
+            if (name.equalsIgnoreCase("trash")) name = "trash";
+            MailTree mt =  mailboxTree(name, create, throwexn);
+            /* FIXME: throw throwexn here
+            if (mt==null || mt.getMailbox()==null)
+                Log.error("mt==null", name);
+            */
+            return mt==null ? null : mt.getMailbox();
+        }
+        private MailTree mailboxTree(String name, boolean create) { return mailboxTree(name, create, true); }
+        private MailTree mailboxTree(String name, boolean create, boolean throwexn) {
+            MailTree m = root;
             for(StringTokenizer st = new StringTokenizer(name, sep + ""); st.hasMoreTokens();)
                 if ((m = m.slash(st.nextToken(), create)) == null) {
                     if (throwexn) throw new Server.No("no such mailbox " + name);
@@ -131,13 +145,13 @@ public class IMAP {
             if (ref.length() == 0) { client.list(sep, start, lsub, false); return; }
             while (start.endsWith(""+sep)) start = start.substring(0, start.length() - 1);
             if (ref.endsWith("%")) ref = ref + sep;
-            String[] children = (start.length() == 0 ? root : mailbox(start, false)).children();
+            String[] children = (start.length() == 0 ? root : mailboxTree(start, false)).children();
             for(int i=0; i<children.length; i++) {
                 String s = children[i], pre = ref, kid = start + (start.length() > 0 ? sep+"" : "") + s;                
                 if (mailbox(kid, false) == null) continue;
-                Mailbox phant = mailbox(kid, false, false);
+                MailTree phant = mailboxTree(kid, false, false);
                 if (phant != null) {
-                    boolean phantom = phant.phantom();
+                    boolean phantom = phant.getMailbox()==null;
                     while(true) {
                         if (pre.length() == 0) {
                             if (s.length() == 0)       client.list(sep, kid, lsub, phantom);
@@ -155,7 +169,7 @@ public class IMAP {
             }
         }
 
-        public String[] capability() { return new String[] { "IMAP4rev1" , "UNSELECT", "ID" }; }
+        public String[] capability() { return new String[] { "IMAP4rev1" , "UNSELECT" /*, "ID"*/ }; }
         public Hashtable id(Hashtable clientId) {
             Hashtable response = new Hashtable();
             response.put("name", IMAP.class.getName());
@@ -172,8 +186,20 @@ public class IMAP {
             for(Mailbox.Iterator it=selected().iterator(q);it.next();) to.insert(it.cur(), it.getFlags() | Mailbox.Flag.RECENT); }
 
         public void unselect() { selected = null; }
-        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 delete(String m0) { mailboxTree(dirname(m0),false).rmdir(basename(m0)); }
+
+        public void rename(String from0, String to) {
+            Mailbox from = mailbox(from0, false);
+            if (from.equals(inbox))                { from.copy(Query.all(), mailbox(to, true)); }
+            else if (to.equalsIgnoreCase("inbox")) { from.copy(Query.all(), mailbox(to, true)); delete(from0); }
+            else mailboxTree(dirname(from0), false)
+                     .rename(dirname(from0),
+                             mailboxTree(dirname(to),
+                                         true /* required by IMAP */),
+                             basename(to));
+        }
+
         public void create(String m) { mailbox(m, true, false); }
         public void append(String m,int f,Date a,String b) { try {
             // FIXME: more efficient streaming here?
@@ -227,18 +253,13 @@ public class IMAP {
             }
         }
 
-        public void rename(String from0, String to) {
-            Mailbox from = mailbox(from0, false);
-            if (from.equals(inbox))                { from.copy(Query.all(), mailbox(to, true)); }
-            else if (to.equalsIgnoreCase("inbox")) { from.copy(Query.all(), mailbox(to, true)); from.destroy(false); }
-            else from.rename(to);
-        }
         public void fetch(Query q, int spec, String[] headers, int start, int end, boolean uid) {
             for(Mailbox.Iterator it = selected().iterator(q); it.next(); ) {
-                Message message = ((spec & (BODYSTRUCTURE | ENVELOPE | INTERNALDATE | FIELDS | FIELDSNOT | RFC822 |
+                Message message =
+                    ((spec & (BODYSTRUCTURE | ENVELOPE | INTERNALDATE | FIELDS | FIELDSNOT | RFC822 |
                                             RFC822TEXT | RFC822SIZE | HEADERNOT | HEADER)) != 0) ? it.cur() : null;
-                int size = message == null ? 0 : message.getLength();
-                client.fetch(it.imapNumber(), it.getFlags(), size, message, it.uid());
+                long size = message == null ? 0 : message.getLength();
+                client.fetch(it.imapNumber(), it.getFlags(), (int)size, message, it.uid());
                 it.setFlags(it.getFlags() & ~Mailbox.Flag.RECENT);
             }
         }
@@ -249,7 +270,8 @@ public class IMAP {
     /** takes an IMAP.Server and exposes it to the world as an IMAP server on a TCP socket */
     public static class Listener implements Client {
         String selectedName = null;
-        Mailbox inbox = null, root = null;
+        Mailbox inbox = null;
+        MailTree root = null;
         Server api;
         Parser parser = null;
         Connection conn = null;
@@ -259,7 +281,7 @@ public class IMAP {
         Parser.Token token(boolean freak) { return parser.token(freak); }
         void println(String s) {
             conn.println(s);
-            Log.info("", s);
+            //Log.info("", s);
         }
         void newline() { parser.newline(); }
         Query query(int max) { return parser.query(max, maxn(true)); }
@@ -273,9 +295,9 @@ public class IMAP {
             } else {
                 Account account = (Account)ret;
                 ((MailboxWrapper)api).root = root = account.getMailbox(IMAP.class);
-                Log.warn(this, "logged in, root="+root);
-                ((MailboxWrapper)api).inbox = inbox = root.slash("INBOX", false);
-                if (inbox == null) ((MailboxWrapper)api).inbox = inbox = root;
+                MailTree ibt = root.slash("INBOX", false);
+                Mailbox ib = ibt==null ? null : ibt.getMailbox();
+                ((MailboxWrapper)api).inbox = inbox = ib;
             }
         }
 
@@ -310,10 +332,10 @@ public class IMAP {
                     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(((lastuid=uid)
-                                                          ? Query.uid(token().set(maxn(uid)))
-                                                          : Query.imapNumber(token().set(maxn(uid)))),
-                                                        lastfetch=token().lx(), 0, 0, 0, uid, 0); break;
+                    case FETCH:        selected(); lastuid = uid; fetch((uid
+                                                                         ? Query.uid(token().set(maxn(uid)))
+                                                                         : Query.imapNumber(token().set(maxn(uid)))),
+                                                                        lastfetch=token().lx(), 0, 0, 0, uid, 0); break;
                     case COPY:         selected(); api.copy(uid
                                                             ? Query.uid(token().set(maxn(uid)))
                                                             : Query.imapNumber(token().set(maxn(uid))), token().astring()); break;
@@ -358,7 +380,8 @@ public class IMAP {
                         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(); }
+                        Parser.Token t2 = token(false);
+                        if (t2 != null) { arrival = t.datetime(); t = t2; }
                         api.append(m, flags, arrival, t.q());
                         break; }
                     case STORE: {
@@ -385,7 +408,8 @@ public class IMAP {
                     return;
                 }
             } 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); }
+            } catch (Server.No n)  { println(tag==null?"* BAD Invalid tag":(tag+" No "  + n.toString())); Log.warn(this,n);
+            }
         }
 
         private Parser.Token[] lastfetch = null; // hack
@@ -449,6 +473,7 @@ public class IMAP {
                     t = new Parser.Token[] { parser.token("FLAGS"), parser.token("INTERNALDATE"),
                                       parser.token("RFC822.SIZE") };
             }
+            boolean looked_at_body = false;
             for(int i=0; i<t.length; i++) {
                 if (r.length() > initlen) r.append(" ");
                 if (t[i] == null || t[i].s == null) continue;
@@ -459,30 +484,30 @@ 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(Printer.qq(getBodyString(m)));}
-                } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;if(e){r.append(" ");r.append(Printer.qq(m.headers.getString()+"\r\n"));}
+                } else if (s.equals("RFC822.TEXT")) {  spec|=RFC822TEXT;   if(e){r.append(" ");r.append(Printer.qq(m.getStream()));}
+                } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;       if(e){r.append(" ");r.append(Printer.qq(m.headers.getStream().append("\r\n")));}
                 } else if (s.equals("RFC822.SIZE")) {  spec|=RFC822SIZE;   if(e){r.append(" ");r.append(m.getLength());}
                 } 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.imapNumber(new int[] { num, num }), Mailbox.Flag.SEEN, false, false);
+                    looked_at_body = true;
                     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; }
+                       //if (e) { r.append(" "); r.append(Printer.qq(m.getBody().getStream())); } continue;
                    }
-                    String payload = "";
+                    Fountain payload = Fountain.Util.create("");
                     r.append("[");
                     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.headers.getString()+"\r\n"+getBodyString(m); }
-                    else if (s.equals("") || s.equals("1")) { spec |= RFC822TEXT;   if(e) payload = m.headers.getString()+"\r\n"+getBodyString(m); }
-                    else if (s.equals("TEXT"))              { spec |= RFC822TEXT;   if(e) payload = getBodyString(m); }
-                    else if (s.equals("HEADER"))            { spec |= HEADER;       if(e) payload = m.headers.getString()+"\r\n"; }
-                    else if (s.equals("HEADER.FIELDS"))     { spec |= FIELDS;     payload=headers(r,t[i].l()[1].sl(),false,m,e); }
-                    else if (s.equals("HEADER.FIELDS.NOT")) { spec |= FIELDSNOT;  payload=headers(r,t[i].l()[1].sl(),true,m,e); }
+                    if (list.length == 0)                   { spec |= RFC822TEXT;   if(e) payload = m; }
+                    else if (s.equals("") || s.equals("1")) { spec |= RFC822TEXT;   if(e) payload = m; }
+                    else if (s.equals("TEXT"))              { spec |= RFC822TEXT;   if(e) payload = m.getBody(); }
+                    else if (s.equals("HEADER"))            { spec |= HEADER;       if(e) payload = Fountain.Util.concat(m.headers, Fountain.Util.create("\r\n")); }
+                    else if (s.equals("HEADER.FIELDS"))     { spec |= FIELDS;       payload=Fountain.Util.create(headers(r,t[i].l()[1].sl(),false,m,e)); }
+                    else if (s.equals("HEADER.FIELDS.NOT")) { spec |= FIELDSNOT;    payload=Fountain.Util.create(headers(r,t[i].l()[1].sl(),true,m,e)); }
                     else if (s.equals("MIME")) {              throw new Server.Bad("MIME not supported"); }
                     else                                      throw new Server.Bad("unknown section type " + s);
                     if (i<t.length - 1 && (t[i+1].s != null && t[i+1].s.startsWith("<"))) {
@@ -491,11 +516,17 @@ public class IMAP {
                         int dot = s.indexOf('.');
                         start = dot == -1 ? Integer.parseInt(s) : Integer.parseInt(s.substring(0, s.indexOf('.')));
                         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) {
+                            //payload = payload.substring(start, Math.min(end+1,payload.getLength())); r.append("<"+start+">");
+                            // FIXME
+                            throw new RuntimeException("this had to be disabled");
+                        }
                     }
-                    if (e) { r.append("] "); r.append(Printer.qq(payload)); }
+                    if (e) { r.append("] "); r.append(Printer.qq(payload.getStream())); }
                 }
             }
+            if ((spec & PEEK) == 0 && looked_at_body && e)
+                api.addFlags(Query.imapNumber(new int[] { num, num }), Mailbox.Flag.SEEN, false, false);
             if (e) {
                r.append(")");
                println("* " + r.toString());
@@ -698,7 +729,7 @@ public class IMAP {
             }
             public Date datetime() {
                 if (type != QUOTED) bad("Expected quoted datetime");
-                try { return new SimpleDateFormat("dd-MM-yyyy hh:mm:ss zzzz").parse(s.trim());
+                try { return new SimpleDateFormat("dd-MMM-yyyy hh:mm:ss zzzz").parse(s.trim());
                 } catch (ParseException p) { throw new Server.Bad("invalid datetime format " + s + " : " + p); }
             }
             public String atom() {
@@ -823,6 +854,12 @@ public class IMAP {
                 ")";
         }
         
+        // FIXME: ugly
+        public static String qq(Stream stream) {
+            StringBuffer sb = new StringBuffer();
+            stream.transcribe(sb);
+            return qq(sb.toString());
+        }
         public static String qq(String s) {
             StringBuffer ret = new StringBuffer();
             ret.append('{');