mad changes
[org.ibex.mail.git] / src / org / ibex / mail / protocol / IMAP.java
1 package org.ibex.mail.protocol;
2 import java.net.*;
3 import java.io.*;
4
5 // FEATURE: pipelining
6 // FEATURE: support [charset]
7 public class IMAP extends MessageProtocol {
8
9     public static void main(String[] args) throws Exception {
10         ServerSocket ss = new ServerSocket(143);
11         while(true) {
12             System.out.println("listening");
13             final Socket s = ss.accept();
14             System.out.println("connected");
15             new Thread() {
16                 public void run() {
17                     try {
18                         service(s);
19                     } catch (Exception e) {
20                         e.printStackTrace();
21                     }
22                 }
23             }.start();
24         }
25     }
26
27     private static class Listener extends Incoming {
28         Socket conn;
29         String vhost;
30         Mailbox selected = null;
31         public void init() { }
32         public Listener(Socket conn, String vhost) { this.vhost = vhost; this.conn = conn; this.selected = null; }
33
34         public void login(String user, String password) { if (!auth(user, password)) throw new No("Liar, liar, pants on fire."); }
35         public void capability() { star("CAPABILITY IMAP4rev1"); ok("Completed"); }
36         public void noop() { ok("Completed"); }
37         public void logout() { star("BYE LOGOUT received"); ok("Completed"); }
38         public void delete(Mailbox m) { if (!m.getName().toLowerCase().equals("inbox")) m.delete(); }
39         public void subscribe(String[] args) { ok("SUBSCRIBE ignored"); }
40         public void unsubscribe(String[] args) { ok("UNSUBSCRIBE ignored"); }
41         public void check(String[] args, boolean examineOnly) { ok("check complete"); }
42         public void lsub(String[] args) { list(args); }
43         public void list(String[] args) { star("LIST () \".\" INBOX"); ok("LIST completed"); }
44         public void create(String mailbox) {if(!mailbox.endsWith(".")&&!mailbox.equalsIgnoreCase("inbox"))storage.create(mailbox);}
45         public void rename(Mailbox from, String to) {
46             if (from.getName().toLowerCase().equals("inbox")) {
47                 int[] messages = from.list();
48                 Malbox toBox = getMailbox(to);
49                 for(int i=0; i<messages.length; i++) {
50                     Message m = from.get(messages[i]);
51                     toBox.add(m);
52                     from.delete(messages[i]);
53                 }
54             } else if (to.toLowerCase().equals("inbox")) {
55                 int[] messages = from.list();
56                 Mailbox inbox = getMailbox("inbox");
57                 for(int i=0; i<messages.length; i++) {
58                     Message m = from.get(messages[i]);
59                     inbox.add(m);
60                     from.delete(messages[i]);
61                 }
62                 from.delete();
63             } else {
64                 from.rename(to);
65             }
66         }
67
68         public void status(Mailbox mbox, boolean messages, boolean recent, boolean uidnext, boolean uidvalidity, boolean unseen) {
69             for(int i=0; i<toks.length; i++) {
70                 if (toks[i].equals("MESSAGES")) messages = true;
71                 else if (toks[i].equals("RECENT")) recent = true;
72                 else if (toks[i].equals("UIDNEXT")) uidnext = true;
73                 else if (toks[i].equals("UIDVALIDITY")) uidvalidity = true;
74                 else if (toks[i].equals("UNSEEN")) unseen = true;
75             }
76             int[] messages = mbox.list();
77             int numRecent = 0, numUnseen = 0;
78             for(int i=0; i<messages.length; i++) {
79                 Message m = mbox.get(messages[i]);
80                 if (!m.seen) numUnseen++;
81                 if (m.recent) numRecent++;
82             }
83             star("STATUS " + args[0] + " (" +
84                  (messages ? ("MESSAGES " + messages.length + " ") : "") +
85                  (recent ? ("RECENT " + numRecent + " ") : "") +
86                  (unseen ? ("UNSEEN " + numUnseen + " ") : "") +
87                  (uidvalidity ? ("UIDVALIDITY " + mbox.uidvalidity + " ") : "") +
88                  (uidnext ? ("UIDNEXT " + mbox.uidnext + " ") : "") + ")");
89         }
90
91         public void select(String[] args, boolean examineOnly) {
92             if (args.length < 1) throw new Bad("Not enough arguments");
93             selected = geMailbox(args[0]);
94             if (selected == null) throw new No("No such mailbox");
95             star("EXISTS");
96             star("1 RECENT");
97             star("OK [UNSEEN 12] Message 12 is first unseen");
98             star("OK [UIDVALIDITY 123123123] UIDs valid");
99             star("FLAGS (\Answered \Flagged \Deleted \Seen \Draft)");
100             ok("[READ-WRITE] " + (examineOnly ? "EXAMINE" : "SELECT") + " completed");
101         }
102         public void close(String[] args, boolean examineOnly) {
103             if (selected == null) throw new Bad("no mailbox selected");
104             expunge(new String[] { }, false, true);
105             selected = null;
106             ok("CLOSE completed");
107         }
108         public void expunge(String[] args, boolean examineOnly, boolean silent) {
109             if (selected == null) throw new Bad("no mailbox selected");
110             int[] messages = selected.list();
111             for(int i=0; i<messages.length; i++) {
112                 Message m = selected.get(messages[i]);
113                 if (m.deleted) {
114                     if (!silent) star(m.uid + " EXPUNGE");
115                     if (!examineOnly) selected.delete(i);
116                 }
117             }
118             if (!silent) ok("EXPUNGE completed");
119         }
120
121         public void append(Mailbox m, Token flags, Date arrived, String literal) {
122             Message m = new Message(null, null, new StringReader(literal));
123             if (flags != null) flags.setFlags(m);
124             if (arrived != null) m.arrival = arrived;
125             selected.add(m);
126         }
127
128         public void fetch(Message m, String[] headers, boolean negateHeaders, boolean flags, boolean internalDate,
129                           boolean size, boolean uid, int start, int end, boolean setSeen, boolean envelope,
130                           boolean header, boolean peek) {
131             String reply = "";
132             if (flags)
133                 reply +=
134                     "FLAGS ("
135                     + m.deleted ? "\Deleted " : ""
136                     + m.seen ? "\Seen " : ""
137                     + m.flagged ? "\Flagged " : ""
138                     + m.draft ? "\Draft " : ""
139                     + m.answered ? "\Answered " : ""
140                     + m.recent ? "\Recent " : ""
141                     + ") ";
142             if (size) reply += "RFC822.SIZE " m.rfc822size() + " ";; 
143             if (bodyStructure)    // FIXME
144                 reply += "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" " + m.rfc822size() +" "+ m.numLines() +")";
145             if (envelope)
146                 reply +=
147                     "(" + quotify(m.date) +
148                     " " + quotify(m.subject) +
149                     " " + quotify(new Address[] { m.from }) + " " + 
150                     " " + quotify(new Address[] { m.sender }) + " " + 
151                     " " + quotify(new Address[] { m.replyTo }) + " " + 
152                     " " + quotify(new Address[] { m.to }) + " " + 
153                     " " + quotify(cc) + " " + 
154                     " " + quotify(bcc) + " " + 
155                     " " + quotify(m.headers.get("in-reply-to")) + " " + 
156                     " " + quotify(m.messageId) +
157                     ") ";
158             if (internaldate) reply += quotify(m.arrival) + " ";
159             // FIXME
160         }
161
162         public void copy(int[] set, Mailbox target) {
163             for(int i=0; i<set.length; i++) {
164                 Message m = selected.get(set[i]);
165                 target.add(m);  // sharing problem?  immutability helps
166             }
167         }
168
169         public boolean handleRequest(LineReader r, PrintWriter pw) {
170             pw.println("* OK " + vhost + " " + IMAP.class.getName() + " IMAP4 v0.1 server ready");
171             while(true) {
172                 boolean uid = false;
173                 String s = r.readLine();
174                 if (s.indexOf(' ') == -1) { pw.println("* BAD Invalid tag"); continue; }
175                 String tag = atom();
176                 String command = atom();
177                 if (command.equals("UID")) { uid = true; command = atom(); }
178                 if (command.equals("AUTHENTICATE")) { authenticate(args); }
179                 else if (command.equals("LIST")) list(mailbox(), mailboxPattern()); 
180                 else if (command.equals("LSUB")) lsub(mailbox(), mailboxPattern()); 
181                 else if (command.equals("CAPABILITY")) { capability(); }
182                 else if (command.equals("LOGIN")) login(astring(), astring());
183                 else if (command.equals("LOGOUT")) { logout(); conn.close(); return false; }
184                 else if (command.equals("RENAME")) rename(mailbox(), atom());
185                 else if (command.equals("APPEND")) {
186                     Mailbox m = mailbox();
187                     Token t = token();
188                     Token[] flags = null;
189                     Date arrival = null;
190                     if (t.type == LIST) { flags = t.l(); t = token(); }
191                     if (t.type == QUOTED) { arrival = t.datetime(); t = token(); }
192                     append(m, flags, arrival, t.literal());
193                 } else if (command.equals("EXAMINE")) examine(mailbox());
194                 else if (command.equals("COPY")) copy(set(), mailbox());
195                 else if (command.equals("DELETE")) delete(mailbox());
196                 else if (command.equals("CREATE")) create(mailbox());
197                 else if (command.equals("STORE")) {
198                     int[] messages = set();
199                     String what = atom();
200                     Token[] flags = l();
201                     for(int i=0; i<messages.length; i++) {
202                         Message m = selected.get(messages[i]);
203                         if (what.charAt(0) == 'F') m.deleted = m.seen = m.flagged = m.draft = m.answered = m.recent = false;
204                         for(int i=0; i<flags.length; i++) {
205                             String flag = flags[i].flag();
206                             if (flag.equals("Deleted"))  m.deleted = what.charAt(0) != '-';
207                             if (flag.equals("Seen"))     m.seen = what.charAt(0) != '-';
208                             if (flag.equals("Flagged"))  m.flagged = what.charAt(0) != '-';
209                             if (flag.equals("Draft"))    m.draft = what.charAt(0) != '-';
210                             if (flag.equals("Answered")) m.answered = what.charAt(0) != '-';
211                             if (flag.equals("Recent"))   m.recent = what.charAt(0) != '-';
212                         }
213                         selected.add(m);  // re-add
214                     }
215                 } else if (command.equals("FETCH")) {
216                     Set s = set();
217                     Token t = token();
218                     Token[] tl = null;
219                     int start = -1;
220                     int end = -1;
221                     boolean envelope=false, internaldate=false, size=false, flags=false, uid=false, header=false, peek=false;
222                     if (t.type == Token.LIST) tl = t.l();
223                     else if (t.atom().equals("FULL")) { flags=true; internaldate=true; size=true; envelope=true; body=true; }
224                     else if (t.atom().equals("ALL"))  { flags=true; internaldate=true; size=true; envelope=true; }
225                     else if (t.atom().equals("FAST")) { flags=true; internaldate=true; size=true; }
226                     else tl = new Token[] { t };
227                     else throw new Bad("expected atom or list");
228                     for (int i=0; i<tl.length; i++) {
229                         String s = tl[i].atom();
230                         if (s.startsWith("BODY.PEEK")) { peek = true; s = "BODY" + s.substring(9); }
231                         if (s.startsWith("BODY.1")) s = "BODY" + s.substring(6);
232                         if (s.indexOf('<') != -1) {
233                             start = Integer.parseInt(s.substring(s.indexOf('<')+1, s.indexOf(s.indexOf('<'), '.')));
234                             end = Integer.parseInt(s.substring(s.indexOf(s.indexOf('<'), '.'), s.indexOf('>')));
235                             s = s.substring(0, s.indexOf('<'));
236                         }
237                         if (s.equals("ENVELOPE")) envelope = true;
238                         else if (s.equals("FLAGS")) flags = true;
239                         else if (s.equals("INTERNALDATE")) internaldate = true;
240                         else if (s.equals("RFC822")) body = true;
241                         else if (s.equals("RFC822.HEADER")) header = true;
242                         else if (s.equals("RFC822.SIZE")) size = true;
243                         else if (s.equals("RFC822.TEXT")) body = true;
244                         else if (s.equals("BODY")) body = true;
245                         else if (s.equals("BODY[]")) body = true;
246                         else if (s.equals("BODY[HEADER]")) header=true;
247                         else if (s.startsWith("BODY[HEADER.FIELDS")) throw new No("partial headers not supported");
248                         else if (s.startsWith("BODY[HEADER.FIELDS.NOT")) throw new No("partial headers not supported"):
249                         else if (s.equals("BODYSTRUCTURE")) throw new No("FETCH BODYSTRUCTURE not supported");
250                         else if (s.equals("TEXT")) body = true;
251                         else if (s.equals("MIME")) throw new Bad("FETCH BODY.MIME not supported");
252                         else if (s.equals("UID")) uid = true;
253                         else if (s.startsWith("BODY")) throw new No("FETCH BODY[*] not supported");
254                         else throw new Bad("unrecognized FETCH argument \"" + s + "\"");
255                     }
256                     fetch(selected, flags, internaldate, size, uid, start, end, setseen, envelope, header, peek);
257                 } else if (command.equals("STATUS")) {
258                     Mailbox m = mailbox();
259                     Token[] attrs = l();
260                     boolean messages = false;
261                     boolean recent = false;
262                     boolean uidnext = false;
263                     boolean uidvalidity = false;
264                     boolean unseen = false;
265                     for(int i=0; i<attrs.length; i++) {
266                         String s = attrs[i].atom();
267                         if (s.equals("MESSAGES")) messages = true;
268                         if (s.equals("RECENT")) recent = true;
269                         if (s.equals("UIDNEXT")) uidnext = true;
270                         if (s.equals("UIDVALIDITY")) uidvalidity = true;
271                         if (s.equals("UNSEEN")) unseen = true;
272                     }
273                     status(mbox, messages, recent, uidnext, uidvalidity, unseen);
274                 } else {
275                     throw new Bad("unrecognized command \"" + command + "\"");
276                 }
277             }
278         }
279     }
280
281     public static String quotify(String s){return s==null?"NIL":"\""+s.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\\\"")+"\"";}
282     public static String address(Address a) { return "("+quotify(a.desciption)+" NIL "+quotify(a.user)+" "+quotify(a.host)+")"; }
283     public static String addressList(Object a) {
284         if (a == null) return "NIL";
285         if (a instanceof Address) return "("+address((Address)a)+")";
286         Address[] aa = (Address[])a;
287         StringBuffer ret = new StringBuffer();
288         ret.append("(");
289         for(int i=0; i<aa.length; i++) { ret.append(aa[i]); if (i < aa.length - 1) ret.append(" "); }
290         ret.append(")");
291         return ret.toString();
292     }
293         
294     public static String envelope(Message m) {
295         return
296             "(" + quotify(m.arrival.toString()) +
297             " " + quotify(m.subject) +          
298             " " + addressList(m.from) +      
299             " " + addressList(m.sender) +
300             " " + addressList(m.replyTo) + 
301             " " + addressList(m.to) + 
302             " " + addressList(m.cc) + 
303             " " + addressList(m.bcc) + 
304             " " + quotify(m.headers.get("in-reply-to")) +
305             " " + quotify(m.messageId) +
306             ")";
307     }
308     
309
310     public static Query query() {
311         String s = null;
312         boolean not = false;
313         while(true) {
314             Token t = token();
315             if (t.type == t.LIST) { /* FIXME */ }
316             else if (t.type == t.SET) return new Query.Set(t.set());
317             s = t.atom();
318             if (s.equals("NOT")) { not = true; continue; }
319             if (s.equals("OR")) { return new Query.OR(new Query[] { query(), query() }); }
320             if (s.equals("AND")) { return new Query.AND(new Query[] { query(), query() }); }
321             break;
322         }
323         if (s.startsWith("UN")) { not = true; tok = s.substring(2); }
324         if (s.equals("ANSWERED")) q = new Query.Flag(Query.Flag.ANSWERED);
325         else if (s.equals("DELETED")) q = new Query.Flag(Query.Flag.ANSWERED);
326         else if (s.equals("DRAFT")) q = new Query.Flag(Query.Flag.ANSWERED);
327         else if (s.equals("FLAGGED")) q = new Query.Flag(Query.Flag.ANSWERED);
328         else if (s.equals("RECENT")) q = new Query.Flag(Query.Flag.ANSWERED);
329         else if (s.equals("SEEN")) q = new Query.Flag(Query.Flag.ANSWERED);
330         else if (s.equals("OLD")) { not = true; q = new Query.Flag(Query.Flag.RECENT); }
331         else if (s.equals("NEW"))q=new Query.And(new Query.Flag(Query.Flag.RECENT),new Query.Not(new Query.Flag(Query.Flag.SEEN)));
332         else if (s.equals("KEYWORD")) q = new Query.Header("keyword", flag());
333         else if (s.equals("HEADER")) q = new Query.Header(astring(), astring());
334         else if (s.equals("BCC")) q = new Query.Header("bcc", astring());
335         else if (s.equals("CC")) q = new Query.Header("cc", astring());
336         else if (s.equals("FROM")) q = new Query.Header("from", astring());
337         else if (s.equals("TO")) q = new Query.Header("to", astring());
338         else if (s.equals("SUBJECT")) q = new Query.Header("subject", astring());
339         else if (s.equals("LARGER")) q = new Query.Size(n(), true);
340         else if (s.equals("SMALLER")) q = new Query.Size(n(), false);
341         else if (s.equals("BODY")) q = new Query.FullText(astring(), true, false);
342         else if (s.equals("TEXT")) q = new Query.FullText(astring(), true, true);
343         else if (s.equals("BEFORE")) q = new Query.Arrival(date(), true, false);
344         else if (s.equals("SINCE")) q = new Query.Arrival(date(), false, true);
345         else if (s.equals("ON")) q = new Query.Arrival(date(), true, true);
346         else if (s.equals("SENTBEFORE")) q = new Query.Sent(date(), true, false);
347         else if (s.equals("SENTSINCE")) q = new Query.Sent(date(), false, true);
348         else if (s.equals("SENTON")) q = new Query.Sent(date(), true, true);
349         else if (s.equals("UID")) q = new Query.UID(set());
350     }
351
352     private static class Token {
353         private byte type;
354         private final String s;
355         private final Token[] l;
356         private final int n;
357         private static final byte NIL = 0;
358         private static final byte LIST = 1;
359         private static final byte QUOTED = 2;
360         private static final byte NUMBER = 3;
361         public Token() { n = 0; list = null; s = null; type = NIL; }
362         public Token(String quoted) { s = quoted; l = null; type = QUOTED; n = 0; }
363         public Token(Token[] list) { l = list; s = null; type = LIST; n = 0; }
364         public Token(int number) { n = number; list = null; s = null; type = NUMBER; }
365
366         // assumes token is a flag list or a fag
367         public void setFlags(Message m) { }
368         public void addFlags(Message m) { }
369         public void deleteFlags(Message m) { }
370
371         public String mailboxPattern() {
372             if (type == ATOM) retrn s;
373             if (type == QUOTED) return s;
374             throw new Bad("exepected a mailbox pattern");
375         }
376         public String flag() throws Bad {
377             if (type != ATOM) throw new Bad("expected a flag");
378             return s;  // if first char != backslash, it is a keyword-flag
379         }
380         public int n() throws Bad {
381             if (type != NUMBER) throw new Bad("expected number");
382             return n;
383         }
384         public int nz() throws Bad {
385             if (type != NUMBER) throw new Bad("expected number");
386             if (n == 0) throw new Bad("expected nonzero number");
387             return n;
388         }
389         public String  q() throws Bad {
390             if (type == NIL) return null;
391             if (type != QUOTED) throw new Bad("expected qstring");
392             return s;
393         }
394         public Token[] l() throws Bad {
395             if (type == NIL) return null;
396             if (type != LIST) throw new Bad("expected parenthesized list");
397             return l;
398         }
399         public int[] set() throws Bad {
400             if (type != ATOM) throw new Bad("expected a messageid set");
401             Vec ids = new Vec();
402             StringTokenizer st = new StringTokenizer(s, ",");
403             while(st.hasMoreTokens()) {
404                 String s = st.nextToken();
405                 if (s.indexOf(':') != -1) {
406                     int start = Integer.parseInt(s.substring(0, s.indexOf(':')));
407                     String end_s = s.indexOf(':'+1);
408                     if (end_s.equals("*")) {
409                         ids.addElement(new Integer(start));
410                         ids.addElement(new Integer(-1));
411                     } else {
412                         int end = Integer.parseInt(end_s);
413                         for(int j=start; j<=end; j++) ids.addElement(new Integer(j));
414                     }
415                 } else {
416                     ids.addElement(new Integer(Integer.parseInt(s)));
417                 }
418             }
419             int[] ret = new int[ids.size()];
420             ids.copyInto(ret);
421             return ret;
422         }
423         public Date date() throws Bad {
424             if (type != QUOTED && type != ATOM) throw new Bad("Expected quoted or unquoted date");
425             try { new SimpleDateFormat("dd-MMM-yyyy").parse(s);
426             } catch (ParseException p) { throw new Bad("invalid date format; " + p); }
427         }
428         public Date datetime() throws Bad {
429             if (type != QUOTED && type != ATOM) throw new Bad("Expected quoted or unquoted datetime");
430             try { new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss +zzzz").parse(s);
431             } catch (ParseException p) { throw new Bad("invalid datetime format; " + p); }
432         }
433         public String nstring() throws Bad {
434             if (type == NIL) return null;
435             if (type == QUOTED) return s;
436             throw new Bad("expected NIL or string");
437         }
438         public String astring() throws Bad {
439             if (type == ATOM) return s;
440             if (type == QUOTED) return s;
441             throw new Bad("expected atom or string");
442         }
443         public String atom() throws Bad {
444             if (type != ATOM) throw new Bad("expected atom");
445             for(int i=0; i<s.length(); i++) {
446                 char c = s.charAt(i);
447                 if (c == '(' || c == ')' || c == '{' || c == ' ' || c == '%' || c == '*')
448                     throw new Bad("invalid char in atom: " + c);
449             }
450             return s;
451         }
452         public Mailbox mailbox() throws Bad {
453             if (type == BAREWORD && s.toLowerCase().equals("inbox")) return Mailbox.INBOX;
454             return Mailbox.getMailbox(astring());
455         }
456
457     }
458
459     public static char getc() { return chars[pos++]; }
460     public static char peekc() { return chars[pos]; }
461     public static char ungetc() { pos--; }
462
463     public static Token token() {
464         Vec toks = new Vec();
465         StringBuffer sb = new StringBuffer();
466         char c = getc(); while(c == ' ') c = getc();
467         if (c == '{') {
468             while(peekc() != '}') sb.append(getc());
469             int octets = Integer.parseInt(sb.toString());
470             while(peekc() == ' ') getc();   // whitespace
471             if (peekc() == '\r')  getc();
472             if (peekc() == '\n')  getc();
473             // FIXME: read octets number of octets
474         } else if (c == '\"') {
475             c = getc();
476             if (c == '\\') sb.append(getc());
477             else if (c == '\"') break;
478             else sb.append(c);
479         } else if (c == ')') {
480             return null;
481         } else if (c == '(') {
482             Vec toks = new Vector();
483             do { Token t = token(); if (t != null) toks.addElement(t); } while (t != null);
484             Token[] ret = new Token[toks.size()];
485             toks.copyInto(ret);
486             return ret;
487         } else while(true) {
488             c = peekc();
489             if (c == ' ' || c == '\"' || c == '(' || c == ')' || c == '{') break;
490             sb.append(getc());
491         }
492         return toks.toArray();
493     }
494 }