formatting
[org.ibex.mail.git] / src / org / ibex / mail / protocol / IMAP.java
index 935ac05..f60c912 100644 (file)
@@ -1,8 +1,11 @@
+// Copyright 2000-2005 the Contributors, as shown in the revision logs.
+// Licensed under the Apache Public Source License 2.0 ("the License").
+// You may not use this file except in compliance with the License.
+
 package org.ibex.mail.protocol;
 import org.ibex.io.*;
 import org.ibex.crypto.*;
 import org.ibex.jinetd.Listener;
-import org.ibex.jinetd.Worker;
 import org.ibex.mail.*;
 import org.ibex.util.*;
 import org.ibex.mail.target.*;
@@ -38,6 +41,13 @@ public class IMAP {
     public IMAP() { }
     public static final float version = (float)0.2;
 
+    // FIXME this is evil
+    public static String getBodyString(Message m) {
+        StringBuffer sb = new StringBuffer();
+        m.getStream().transcribe(sb);
+        return sb.toString();
+    }
+
     // API Class //////////////////////////////////////////////////////////////////////////////
 
     public static interface Client {
@@ -64,6 +74,8 @@ public class IMAP {
         public int       unseen(String mailbox);
         public int       recent(String mailbox);
         public int       count(String mailbox);
+        public int       count();
+        public int       maxuid();
         public int       uidNext(String mailbox);
         public int       uidValidity(String mailbox);
         public int[]     search(Query q, boolean uid);
@@ -160,7 +172,7 @@ public class IMAP {
         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, false); }
         public void append(String m,int f,Date a,String b) { try {
-            mailbox(m,false).add(Message.newMessage(new Stream(b)),f|Mailbox.Flag.RECENT);
+            mailbox(m,false).add(Message.newMessage(new Fountain.StringFountain(b)),f|Mailbox.Flag.RECENT);
         } catch (Message.Malformed e) { throw new No(e.getMessage()); } }
         public void check() { }
         public void noop() { }
@@ -170,14 +182,20 @@ public class IMAP {
         public void expunge(Mailbox.Iterator it) { client.expunge(it.uid()); it.delete(); }
         public void subscribe(String mailbox) { }
         public void unsubscribe(String mailbox) { }
+        public int maxuid() {
+            int ret = 0;
+            Mailbox mb = selected();
+            if (mb == null) return 0;
+            for(Mailbox.Iterator it = mb.iterator(); it.next(); ) ret = it.uid();
+            return ret;
+        }
         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 count()                     { return selected().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 int uidValidity(String mailbox) { return Math.abs(mailbox(mailbox, false).uidValidity()); }
+        public void select(String mailbox, boolean examineOnly) { selected = mailbox(mailbox, false); }
 
         public int[] search(Query q, boolean uid) {
             Vec.Int vec = new Vec.Int();
@@ -195,12 +213,12 @@ public class IMAP {
         private void doFlags(Query q, int flags, boolean uid, int style, boolean silent) {
             for(Mailbox.Iterator it = selected().iterator(q);it.next();) {
                 boolean recent = it.recent();
+                try { throw new Exception("flags " + flags); } catch (Exception e) { Log.error(this, e); }
                 if (style == -1)     it.removeFlags(flags);
                 else if (style == 0) it.setFlags(flags);
                 else if (style == 1) it.addFlags(flags);
                 it.recent(recent);
-               // FIXME
-                //if (!silent) client.fetch(it.num(), it.flags(), -1, null, it.uid());
+                if (!silent) client.fetch(it.num(), it.flags(), -1, null, it.uid());
             }
         }            
         public void rename(String from0, String to) {
@@ -213,7 +231,7 @@ public class IMAP {
             for(Mailbox.Iterator it = selected().iterator(q); it.next(); ) {
                 Message message = ((spec & (BODYSTRUCTURE | ENVELOPE | INTERNALDATE | FIELDS | FIELDSNOT | RFC822 |
                                             RFC822TEXT | RFC822SIZE | HEADERNOT | HEADER)) != 0) ? it.cur() : null;
-                int size = message == null ? 0 : message.size();
+                int size = message == null ? 0 : message.getLength();
                 client.fetch(it.num(), it.flags(), size, message, it.uid());
                 it.recent(false);
             }
@@ -224,7 +242,7 @@ 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 implements Worker, Client {
+    public static class Listener implements Client {
         String selectedName = null;
         Mailbox inbox = null, root = null;
         Server api;
@@ -235,7 +253,7 @@ public class IMAP {
         Parser.Token token() { return parser.token(); }
         void println(String s) { conn.println(s); }
         void newline() { parser.newline(); }
-        Query query() { return parser.query(); }
+        Query query(int max) { return parser.query(max, maxn(true)); }
 
         public void login(String u, String p) {
             Object ret;
@@ -252,6 +270,8 @@ public class IMAP {
             }
         }
 
+        private int maxn(boolean uid) { return uid ? api.maxuid() : api.count(); }
+
         public void handleRequest(Connection conn) {
             this.conn = conn;
             parser = new Parser(conn);
@@ -274,7 +294,7 @@ public class IMAP {
                     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 COPY:         selected(); api.copy(Query.set(uid, token().set(maxn(uid))), token().astring()); break;
                     case DELETE:       api.delete(token().atom()); break;
                     case CHECK:        selected(); api.check(); break;
                     case NOOP:         api.noop(); break;
@@ -282,9 +302,14 @@ 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(Query.set(lastuid=uid, token().set()),
+                    case FETCH:        selected(); fetch(Query.set(lastuid=uid, token().set(maxn(uid))),
                                                         lastfetch=token().lx(), 0, 0, 0, uid, 0); break;
-                    case SEARCH:       selected(); println("* SEARCH " + Printer.join(api.search(query(), uid))); break;
+                    case SEARCH: {
+                        selected();
+                        int[] result = api.search(query(maxn(uid)), uid);
+                        /*if (result.length > 0)*/ println("* SEARCH " + Printer.join(result));
+                        break;
+                    }
                     case EXAMINE:
                     case SELECT: {
                         String mailbox = token().astring();
@@ -292,9 +317,10 @@ public class IMAP {
                         println("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)");
                         println("* " + api.count(mailbox)  + " EXISTS");
                         println("* " + api.recent(mailbox) + " RECENT");
+                        println("* OK [UNSEEN " + api.unseen(mailbox) + "]");
                         println("* OK [UIDVALIDITY " + api.uidValidity(mailbox) + "] UIDs valid");
                         println("* OK [UIDNEXT " + api.uidNext(mailbox) + "]");
-                        println("* OK [PERMANENTFLAGS (\\Seen \\Draft \\Answered \\Deleted)]");
+                        println("* OK [PERMANENTFLAGS (\\Seen)]");
                         selected = true;
                         break; }
                     case STATUS: {
@@ -324,7 +350,7 @@ public class IMAP {
                         break; }
                     case STORE: {
                         selected();
-                        Query q = uid ? Query.uid(token().set()) : Query.num(token().set());
+                        Query q = uid ? Query.uid(token().set(maxn(uid))) : Query.num(token().set(maxn(uid)));
                         String s = token().atom().toUpperCase();
                         int flags = token().flags();
                         if (s.equals("FLAGS"))              api.setFlags(q,    flags, uid, false);
@@ -339,8 +365,9 @@ public class IMAP {
                 }
                 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); }
+                Log.error("[imap]", conn.dumpLog());
+            } catch (Server.Bad b) { println(tag==null ? "* BAD Invalid tag":(tag + " Bad " + b.toString()));
+            } catch (Server.No n)  { println(tag==null?"* BAD Invalid tag":(tag+" No "  + n.toString())); }
         }
 
         private Parser.Token[] lastfetch = null; // hack
@@ -364,16 +391,22 @@ public class IMAP {
          *      - emit a fetch reply for the parsed spec with respect to message m
          */
         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;
+            Query q   = o == null ? null : o instanceof Query ? (Query)o : null;
+            Message m = o == null ? null : o instanceof Message ? (Message)o : null;
             boolean e = q == null;
 
+            // asynchronous flags update
+            if (size == -1) {
+                println("* " + num + " FETCH (FLAGS " + Printer.flags(flags) + (uid?(" UID "+muid):"") + ")");
+                return;
+            }
+
             lastfetch = t;
             int spec = 0;                              // spec; see constants for flags
             String[] headers = null;
             int start = -1, end = -1;
             StringBuffer r = new StringBuffer();
-            if (e) { r.append(uid ? muid : num); r.append(" FETCH ("); }
+            if (e) { r.append(num); r.append(" FETCH ("); }
             int initlen = r.length();
             if (uid) {
                 boolean good = false;
@@ -406,11 +439,11 @@ public class IMAP {
                 if (s.equals("BODYSTRUCTURE")) {       spec|=BODYSTRUCTURE;if(e){r.append(" ");r.append(Printer.bodystructure(m));}
                 } else if (s.equals("ENVELOPE")) {     spec|=ENVELOPE;     if(e){r.append(" ");r.append(Printer.envelope(m));}
                 } 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.envelope.arrival));}
+                } 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(m.body));}
-                } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;if(e){r.append(" ");r.append(Printer.qq(m.headers.raw+"\r\n"));}
-                } else if (s.equals("RFC822.SIZE")) {  spec|=RFC822SIZE;   if(e){r.append(" ");r.append(m.size());}
+                } 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.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 {
@@ -426,10 +459,10 @@ public class IMAP {
                     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.raw+"\r\n"+m.body; }
-                    else if (s.equals("") || s.equals("1")) { spec |= RFC822TEXT;   if(e) payload = m.headers.raw+"\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.headers.raw+"\r\n"; }
+                    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); }
                     else if (s.equals("MIME")) {              throw new Server.Bad("MIME not supported"); }
@@ -459,7 +492,7 @@ public class IMAP {
             if (!negate) {
                 if(e) for(int j=0; j<headers.length; j++) {
                     r.append(headers[j] + (j<headers.length-1?" ":""));
-                    if (m.headers.gets(headers[j]) != null) payload += headers[j]+": "+m.headers.gets(headers[j])+"\r\n";
+                    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");
@@ -468,7 +501,7 @@ public class IMAP {
                 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.gets(key)+"\r\n";
+                    payload += key + ": " + m.headers.get(key)+"\r\n";
                 } }
                */
             }
@@ -509,49 +542,53 @@ public class IMAP {
         private Stream stream;
         public Parser(Stream from) { this.stream = from; }
         public Token token(String s) { return new Token(s); }
-        protected Query query() {
+        protected Query query(int max, int maxuid) {
             String s = null;
             boolean not = false;
             Query q = null;
+            Query ret = null;
             while(true) {
                 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());
+                else if (t.type == t.SET) return Query.num(t.set(max));
                 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());
+                if (s.equals("NOT"))   return Query.not(query(max, maxuid));
+                if (s.equals("OR"))    return Query.or(query(max, maxuid), query(max, maxuid));    // FIXME parse rest of list
+                if (s.equals("AND"))   return Query.and(query(max, maxuid), query(max, maxuid));
+
+                if (s.startsWith("UN"))        { not = true; s = s.substring(2); }
+                if (s.equals("ANSWERED"))        q = Query.answered();
+                else if (s.equals("DELETED"))    q = Query.deleted();
+                else if (s.equals("ALL"))        q = Query.all();
+                else if (s.equals("DRAFT"))      q = Query.draft();
+                else if (s.equals("FLAGGED"))    q = Query.flagged();
+                else if (s.equals("RECENT"))     q = Query.recent();
+                else if (s.equals("SEEN"))       q = Query.seen();
+                else if (s.equals("OLD"))      { not = true; q = Query.recent(); }
+                else if (s.equals("NEW"))        q = Query.and(Query.recent(), Query.not(Query.seen()));
+                else if (s.equals("KEYWORD"))    q = Query.header("keyword", token().flag());
+                else if (s.equals("HEADER"))     q = Query.header(token().astring(), token().astring());
+                else if (s.equals("BCC"))        q = Query.header("bcc", token().astring());
+                else if (s.equals("CC"))         q = Query.header("cc", token().astring());
+                else if (s.equals("FROM"))       q = Query.header("from", token().astring());
+                else if (s.equals("TO"))         q = Query.header("to", token().astring());
+                else if (s.equals("SUBJECT"))    q = Query.header("subject", token().astring());
+                else if (s.equals("LARGER"))     q = Query.size(token().n(), Integer.MAX_VALUE);
+                else if (s.equals("SMALLER"))    q = Query.size(Integer.MIN_VALUE, token().n());
+                else if (s.equals("BODY"))       q = Query.body(token().astring());
+                else if (s.equals("TEXT"))       q = Query.full(token().astring());
+                else if (s.equals("BEFORE"))     q = Query.arrival(new Date(0), token().date());
+                else if (s.equals("SINCE"))      q = Query.arrival(token().date(), new Date(Long.MAX_VALUE));
+                else if (s.equals("ON"))       { Date d = token().date(); q = Query.arrival(d, new Date(d.getTime() + 24 * 60 * 60)); }
+                else if (s.equals("SENTBEFORE")) q = Query.sent(new Date(0), token().date());
+                else if (s.equals("SENTSINCE"))  q = Query.sent(token().date(), new Date(Long.MAX_VALUE));
+                else if (s.equals("SENTON"))   { Date d = token().date(); q = Query.sent(d, new Date(d.getTime() + 24 * 60 * 60)); }
+                else if (s.equals("UID"))        q = Query.uid(token().set(max));
+                q = not ? Query.not(q) : q;
+                ret = ret == null ? q : Query.and(ret, q);
             }
-            if (s.startsWith("UN"))        { not = true; s = s.substring(2); }
-            if (s.equals("ANSWERED"))        q = Query.answered();
-            else if (s.equals("DELETED"))    q = Query.deleted();
-            else if (s.equals("ALL"))        q = Query.all();
-            else if (s.equals("DRAFT"))      q = Query.draft();
-            else if (s.equals("FLAGGED"))    q = Query.flagged();
-            else if (s.equals("RECENT"))     q = Query.recent();
-            else if (s.equals("SEEN"))       q = Query.seen();
-            else if (s.equals("OLD"))      { not = true; q = Query.recent(); }
-            else if (s.equals("NEW"))        q = Query.and(Query.recent(), Query.not(Query.seen()));
-            else if (s.equals("KEYWORD"))    q = Query.header("keyword", token().flag());
-            else if (s.equals("HEADER"))     q = Query.header(token().astring(), token().astring());
-            else if (s.equals("BCC"))        q = Query.header("bcc", token().astring());
-            else if (s.equals("CC"))         q = Query.header("cc", token().astring());
-            else if (s.equals("FROM"))       q = Query.header("from", token().astring());
-            else if (s.equals("TO"))         q = Query.header("to", token().astring());
-            else if (s.equals("SUBJECT"))    q = Query.header("subject", token().astring());
-            else if (s.equals("LARGER"))     q = Query.size(token().n(), Integer.MAX_VALUE);
-            else if (s.equals("SMALLER"))    q = Query.size(Integer.MIN_VALUE, token().n());
-            else if (s.equals("BODY"))       q = Query.body(token().astring());
-            else if (s.equals("TEXT"))       q = Query.full(token().astring());
-            else if (s.equals("BEFORE"))     q = Query.arrival(new Date(0), token().date());
-            else if (s.equals("SINCE"))      q = Query.arrival(token().date(), new Date(Long.MAX_VALUE));
-            else if (s.equals("ON"))       { Date d = token().date(); q = Query.arrival(d, new Date(d.getTime() + 24 * 60 * 60)); }
-            else if (s.equals("SENTBEFORE")) q = Query.sent(new Date(0), token().date());
-            else if (s.equals("SENTSINCE"))  q = Query.sent(token().date(), new Date(Long.MAX_VALUE));
-            else if (s.equals("SENTON"))   { Date d = token().date(); q = Query.sent(d, new Date(d.getTime() + 24 * 60 * 60)); }
-            else if (s.equals("UID"))        q = Query.uid(token().set());
-            return not ? Query.not(q) : q;
+            return ret;
         }
 
         private static void bad(String s) { throw new Server.Bad(s); }
@@ -611,30 +648,27 @@ public class IMAP {
                 }
                 return ret;
             }
-            public int[] set() {
+            public int[] set(int largest) {
                 if (type != ATOM) bad("expected a messageid set");
                 Vec.Int ids = new Vec.Int();
                 StringTokenizer st = new StringTokenizer(s, ",");
                 while(st.hasMoreTokens()) {
                     String s = st.nextToken();
                     if (s.indexOf(':') == -1) {
-                       if (s.equals("*")) {
-                           ids.addElement(0);
-                           ids.addElement(Integer.MAX_VALUE);
-                       } else {
-                           ids.addElement(Integer.parseInt(s));
-                           ids.addElement(Integer.parseInt(s));
-                       }
-                       continue; }
+                        if (s.equals("*")) {
+                            ids.addElement(largest);
+                            ids.addElement(largest);
+                        } 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);
-                    if (end_s.equals("*")) { ids.addElement(start); ids.addElement(Integer.MAX_VALUE); }
-                    else {
-                        int end = Integer.parseInt(end_s);
-                        for(int j=Math.min(start,end); j<=Math.max(start,end); j++) {
-                            ids.addElement(j);
-                            ids.addElement(j);
-                       }
+                    int end = end_s.equals("*") ? largest : Integer.parseInt(end_s);
+                    for(int j=Math.min(start,end); j<=Math.max(start,end); j++) {
+                        ids.addElement(j);
+                        ids.addElement(j);
                     }
                 }
                 return ids.dump();
@@ -757,21 +791,21 @@ public class IMAP {
         }
         static String bodystructure(Message m) {
             // FIXME
-            return "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"7BIT\" "+m.size()+" "+m.lines+")";
+            return "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"7BIT\" "+m.getLength()+" "+m.getNumLines()+")";
         }
         static String message(Message m) { return m.toString(); }
         static String date(Date d) { return "\""+d.toString()+"\""; }
         static String envelope(Message m) {
             return
-                "(" + quotify(m.envelope.arrival.toString()) +
+                "(" + quotify(m.arrival.toString()) +
                 " " + quotify(m.subject) +          
                 " " + addressList(m.from) +      
-                " " + addressList(m.headers.gets("sender")) +
+                " " + addressList(m.headers.get("sender")) +
                 " " + addressList(m.replyto) + 
                 " " + addressList(m.to) + 
                 " " + addressList(m.cc) + 
                 " " + addressList(m.bcc) + 
-                " " + quotify((String)m.headers.gets("in-reply-to")) +
+                " " + quotify((String)m.headers.get("in-reply-to")) +
                 " " + quotify(m.messageid) +
                 ")";
         }
@@ -779,7 +813,7 @@ public class IMAP {
         public static String qq(String s) {
             StringBuffer ret = new StringBuffer();
             ret.append('{');
-            ret.append(s.length());
+            ret.append(s.getBytes().length);
             ret.append('}');
             ret.append('\r');
             ret.append('\n');