X-Git-Url: http://git.megacz.com/?p=org.ibex.mail.git;a=blobdiff_plain;f=src%2Forg%2Fibex%2Fmail%2FIMAP.java;h=81f1820fbb444142d5b3d8ddab34e7d5a8ba6ba2;hp=9038cc13514ec7f360e5ecec65ee1f8eefaccb95;hb=1975b1d7755c8a92480a8847f9dd3dc476f95ff5;hpb=7783d407061534f9158f68934a77867a461aa0b5 diff --git a/src/org/ibex/mail/IMAP.java b/src/org/ibex/mail/IMAP.java index 9038cc1..81f1820 100644 --- a/src/org/ibex/mail/IMAP.java +++ b/src/org/ibex/mail/IMAP.java @@ -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 @@ -50,7 +52,7 @@ public class IMAP { public static interface Client { public void expunge(int uid); public void list(char separator, String mailbox, boolean lsub, boolean phantom); - public void fetch(int num, int flags, int size, Message m, int muid); // m may be null or incomplete + public void fetchResponse(int num, int flags, int size, Message m, int muid); // m may be null or incomplete } public static interface Server { @@ -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,20 @@ 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); + Mailbox ret = mt==null ? null : mt.getMailbox(); + if (ret==null && throwexn) throw new Server.No("no such mailbox " + name); + return ret; + } + 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 +143,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 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 +167,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 +184,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? @@ -182,17 +206,25 @@ public class IMAP { public void check() { } public void noop() { } public void logout() { } - public void close() { for(Mailbox.Iterator it=selected().iterator(Query.deleted()); it.next();) it.delete(); } + public void close() { + for(Mailbox.Iterator it=selected().iterator(Query.deleted()); it.next();) { + Log.error("imap", "deleting a message due to close(): " + it.cur().summary()); + 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 expunge(Mailbox.Iterator it) { + client.expunge(it.uid()); + Log.error("imap", "deleting a message due to expunge(): " + it.cur().summary()); + 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; + return mb.maxuid(); } 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()); } @@ -206,7 +238,8 @@ public class IMAP { Vec.Int vec = new Vec.Int(); for(Mailbox.Iterator it = selected().iterator(q); it.next();) { vec.addElement(uid ? it.uid() : it.imapNumber()); - it.setFlags(it.getFlags() & ~Mailbox.Flag.RECENT); + if ((it.getFlags() & Mailbox.Flag.RECENT) != 0) + it.setFlags(it.getFlags() & ~Mailbox.Flag.RECENT); } return vec.dump(); } @@ -223,22 +256,22 @@ public class IMAP { else if (style == 0) it.setFlags(flags); else if (style == 1) it.setFlags(it.getFlags() | flags); //it.setFlag(Mailbox.Flag.RECENT, recent); - if (!silent) client.fetch(it.imapNumber(), it.getFlags(), -1, null, it.uid()); + if (!silent) client.fetchResponse(it.imapNumber(), it.getFlags(), -1, null, it.uid()); } } - 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 | - 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()); + // FIXME it would be great if we could avoid instantiating the entire message just because RFC822SIZE was requested + Message message = + ((spec & (BODYSTRUCTURE | RFC822 | RFC822TEXT | RFC822SIZE)) != 0) + ? it.cur() + : ((spec & (ENVELOPE | INTERNALDATE | FIELDS | FIELDSNOT | RFC822SIZE | HEADERNOT | HEADER)) != 0) + ? Message.newMessage(Fountain.Util.concat(it.head(), Fountain.Util.create("\r\n\r\n"))) + : null; + if (message != null) Log.warn("spec", spec); + long size = message == null ? 0 : message.getLength(); + client.fetchResponse(it.imapNumber(), it.getFlags(), (int)size, message,it.uid()); it.setFlags(it.getFlags() & ~Mailbox.Flag.RECENT); } } @@ -249,7 +282,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 +293,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 +307,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 +344,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 +392,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 +420,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 @@ -396,7 +432,7 @@ public class IMAP { // Callbacks ////////////////////////////////////////////////////////////////////////////// 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 fetchResponse(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+"\"");} @@ -408,7 +444,7 @@ 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, Parser.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 == null ? null : o instanceof Query ? (Query)o : null; Message m = o == null ? null : o instanceof Message ? (Message)o : null; boolean e = q == null; @@ -421,8 +457,8 @@ public class IMAP { lastfetch = t; int spec = 0; // spec; see constants for flags + int start = 0, len = 0; String[] headers = null; - int start = -1, end = -1; StringBuffer r = new StringBuffer(); if (e) { r.append(num); r.append(" FETCH ("); } int initlen = r.length(); @@ -449,6 +485,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 initlen) r.append(" "); if (t[i] == null || t[i].s == null) continue; @@ -459,48 +496,64 @@ 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; + // not sure why this was uncommented.... + //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')); 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+">"); } + len = dot == -1 ? -1 : Integer.parseInt(s.substring(s.indexOf('.') + 1)); + if (e) { + if (start == 0 && len == -1) { + } else if (len == -1) { + payload = Fountain.Util.subFountain(payload, start); + } else { + len = (int)Math.min(len, payload.getLength()-start); + payload = Fountain.Util.subFountain(payload, start, len); + } + r.append("]"); + r.append("<"+start+"> "); + } + } else { + if (e) r.append("] "); } - if (e) { r.append("] "); r.append(Printer.qq(payload)); } + if (e) 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()); } else { - api.fetch(q, spec, headers, start, end, uid); + api.fetch(q, spec, headers, start, (len==-1?0:len), uid); } } @@ -622,6 +675,20 @@ public class IMAP { 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 toString() { + // FIXME hack + switch(type) { + case NIL: return "NIL"; + case LIST: return "(" + Printer.join(", ", l) + ")"; + case QUOTED: return s; + case NUMBER: return n+""; + case ATOM: return s; + case BAREWORD: return s; + case SET: return ""; + } + return "???"; + } + 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; } @@ -663,6 +730,7 @@ public class IMAP { else if (flag.equals("\\Draft")) ret |= Mailbox.Flag.DRAFT; else if (flag.equals("\\Answered")) ret |= Mailbox.Flag.ANSWERED; else if (flag.equals("\\Recent")) ret |= Mailbox.Flag.RECENT; + else Log.warn(this, "unknown flag: " + flag); } return ret; } @@ -698,7 +766,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 +891,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('{'); @@ -858,5 +932,4 @@ public class IMAP { 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; - }