change num() to imapNumber() and nntpNumber(), add comments about semantics
[org.ibex.mail.git] / src / org / ibex / mail / protocol / GMail.java
index 058f821..c43df9f 100644 (file)
+// 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.mail.protocol.*;
 import org.ibex.jinetd.Listener;
-import org.ibex.jinetd.Worker;
 import org.ibex.mail.*;
 import org.ibex.util.*;
+import org.ibex.net.*;
+import org.ibex.js.*;
 import org.ibex.mail.target.*;
 import java.util.*;
 import java.net.*;
 import java.text.*;
 import java.io.*;
+import org.ibex.io.*;
+import org.ibex.io.Fountain;
 
 public class GMail extends Account {
-    public Mailbox getMailbox(Class protocol) { return this.root; }
-    public GMail(String user, String pass) {
-        super(user, Address.parse(user + "@gmail.com"));
-        Log.warn(GMail.class, "logging in " + user + "@gmail.com...");
+
+    private      GMailIMAP imap = new GMailIMAP();
+    private HTTP.Cookie.Jar jar = new HTTP.Cookie.Jar();
+    private      String captcha = null;
+    private       String ctoken = null;
+    private        String email = null;
+    private     String password = null;
+    private     boolean invalid = false;
+    private     String[] labels = new String[0];
+    private    String[] queries = new String[0];
+    private Summary[] summaries = new Summary[0];
+
+    // Constructor, Pooling  ///////////////////////////////////////////////////////////////////////////
+
+    public GMail(String email, String pass) throws IOException {
+        super(email.substring(0, email.indexOf('@')), Address.parse(email));
+        this.email = email; this.password = pass;
+        Log.warn(GMail.class, "logging in " + email);
+        getCookies();
+    }
+
+    public void close() { cache.remove(email, password); invalid = true; }
+
+    private static Hash cache = new Hash();
+    static { HTTP.userAgent = "Mozilla/5.0 (compatible;)"; }
+    public static GMail getGMail(String email, String pass) {
         try {
-            Process p =
-                Runtime.getRuntime().exec(new String[] {
-                    "/usr/bin/python",
-                    "/usr/local/libgmail/demos/archive.py",
-                    user,
-                    pass
-                    });
-            final Mailbox inbox = new MessageArrayMailbox(Mbox.parse(new Stream(p.getInputStream())));
-            this.root =
-                new Mailbox.Default() {
-                        public void add(Message m) { throw new RuntimeException("not supported"); }
-                        public void add(Message m, int i) { throw new RuntimeException("not supported"); }
-                        public int              uidValidity()  { return 1; }
-                        public Mailbox.Iterator iterator()     { return new Mailbox.Iterator.NullIterator(); }
-                        public int              uidNext()      { return 500; }
-                        public String[] children() { return new String[] { "gmail" }; }
-                        public Mailbox slash(String name, boolean create) { return inbox; }
-                    };
-            p.waitFor();
-            Log.warn(GMail.class, "    succeeded for " + user + "@gmail.com!");
+            GMail g = (GMail)cache.get(email, pass);
+            if (g == null) cache.put(email, pass, g = new GMail(email, pass));
+            return g;
         } catch (Exception e) {
             Log.error(GMail.class, e);
+            return null;
+        }
+    }
+
+    // IMAP Interface //////////////////////////////////////////////////////////////////////////////
+
+    public IMAP.Server getIMAP() { return imap; }
+    private class GMailIMAP implements IMAP.Server {
+
+        private String query = "?search=inbox&start=0&view=tl";
+        private int validity = new Random().nextInt();
+
+        private IMAP.Client client;
+        public void setClient(IMAP.Client client) { this.client = client; }
+
+        public String[]  capability() { return new String[] { }; }
+        public Hashtable id(Hashtable clientId) { return null; }
+        public void      logout() { GMail.this.close(); }
+        public void      subscribe(String mailbox) { }
+        public void      unsubscribe(String mailbox) { }
+
+        public void      unselect() { summaries = new Summary[0]; }
+        public void      close() { summaries = new Summary[0]; }
+
+        public void      noop() { check(); }
+        public void      expunge() { }
+
+        public void      setFlags(Query q, int flags, boolean uid, boolean silent) { }
+        public void      removeFlags(Query q, int flags, boolean uid, boolean silent) { }
+        public void      addFlags(Query q, int flags, boolean uid, boolean silent) { }
+
+        public void      rename(String from, String to) { }
+        public void      delete(String m) { }
+        public void      create(String m) {
+            String[] newqueries = new String[queries.length+1];
+            System.arraycopy(queries, 0, newqueries, 0, queries.length);
+            newqueries[queries.length] = m;
+            queries = newqueries;
+        }
+
+        public void      append(String m, int flags, Date arrival, String body) { }
+        public void      copy(Query q, String to) { }
+
+        public void      check() { query(query); }
+        public void      select(String mailbox, boolean examineOnly) {
+            String oldquery = query;
+            if (mailbox.equalsIgnoreCase("inbox")) {
+                query = "?search=inbox&start=0&view=tl";
+                if (!query.equals(oldquery)) check();
+                return;
+            }
+            for(int i=0; i<labels.length; i++) {
+                if (labels[i].equals(mailbox)) {
+                    query = "?search=cat&cat="+mailbox+"&inbox&start=0&view=tl";
+                    if (!query.equals(oldquery)) check();
+                    return;
+                }
+            }
+            query = "?search=query&q="+URLEncoder.encode(mailbox)+"&start=0&view=tl";
+            if (!query.equals(oldquery)) check();
+        }
+
+        public void      lsub(String start, String ref) { list(true); }
+        public void      list(String start, String ref) { list(false); }
+        private void list(boolean lsub) {
+            client.list('/', "inbox", lsub, false);
+            for(int i=0; i<labels.length; i++) client.list('/', labels[i], lsub, false);
+            for(int i=0; i<queries.length; i++) client.list('/', queries[i], lsub, false);
+        }
+
+        public int[]     search(Query q, boolean uid) { return null; }
+        public void      fetch(Query q, int spec, String[] headers, int start, int end, boolean uid) {
+            if (captchaMessage != null) { client.fetch(1, 0, 100, captchaMessage, 100); return; }
+            for(int i=0; i<summaries.length; i++) try {
+                final Message m = summaries[i].getMessage();
+                final int num = i;
+                if (q.match(new Mailbox.Iterator() {
+                        public Message cur() { return m; }
+                        public Headers head() { return m.headers; }
+                        public boolean next() { return false; }
+                        public int     uid() { return num; }
+                        public int     imapNumber() { return num; }
+                        public int     nntpNumber() { throw new RuntimeException("not supported"); }
+                        public void    delete() { }
+                        public void    set(String key, String val) { }
+                        public String  get(String key) { return null; }
+                        public boolean seen() { return false; }
+                        public boolean deleted() { return false; }
+                        public boolean flagged() { return false; }
+                        public boolean draft() { return false;}
+                        public boolean answered() { return false; }
+                        public boolean recent() { return true; }
+                        public void    seen(boolean on) { }
+                        public void    deleted(boolean on) { }
+                        public void    flagged(boolean on) { }
+                        public void    draft(boolean on) { }
+                        public void    answered(boolean on) { }
+                        public void    recent(boolean on) { }
+                        public int     flags() { return 0; }
+                        public void    addFlags(int flags) { }
+                        public void    removeFlags(int flags) { }
+                        public void    setFlags(int flags) { }
+                    })) {
+                    Log.info(GMail.class, "fetch " + summaries[i].subject);
+                    throw new Error("broken");
+                    //client.fetch(i+1, 0, m.size(), m,/* summaries[i].getIntId()*/ i);
+                }
+            } catch (Exception e) { Log.warn(this, e); }
+        }
+
+        public int       unseen(String mailbox)      { return summaries.length; }
+        public int       recent(String mailbox)      { return summaries.length; }
+        public int       count(String mailbox)       { return summaries.length; }
+        public int       count()       { return summaries.length; }
+        public int       maxuid()      { return summaries.length; }
+        public int       uidNext(String mailbox)     { return summaries.length+1; }
+        public int       uidValidity(String mailbox) { return validity; }
+    }
+
+    public void getCookies() throws IOException {
+        jar = new HTTP.Cookie.Jar();
+        String params =
+            "continue="     + URLEncoder.encode(gmail) +
+            "&service=mail" +
+            "&Email="       + URLEncoder.encode(user) +
+            "&Passwd="      + password + 
+            "&null=Sign+in" +
+            (captcha != null ? "&captcha="+URLEncoder.encode(captcha) : "") +
+            (captcha != null ? "&ctoken="+URLEncoder.encode(ctoken) : "");
+        Log.info("[request]", params);
+        HTTP http = captcha==null ? new HTTP(login+"?"+params) : new HTTP(login);
+        InputStream reply = captcha==null ?
+            http.GET(null, jar) :
+            http.POST("application/x-www-form-urlencoded", params, null, jar);
+        String result = new String(InputStreamToByteArray.convert(reply));
+        System.err.println(result);
+        captcha = null;
+        
+        if (result.indexOf("top.location") == -1) { doCaptcha(result); return; }
+        result = result.substring(result.indexOf("top.location"));
+        result = result.substring(result.indexOf("CheckCookie?continue=") + "CheckCookie?continue=".length());
+        result = result.substring(0, result.indexOf('\"'));
+        result = URLDecoder.decode(result);
+        Log.warn("[]", "getting " + result);
+
+        // just need the cookie off of this page
+        InputStreamToByteArray.convert(new HTTP(result).GET(null, jar));
+        imap.check();
+        Log.warn("[]", "done");
+    }
+
+    Message captchaMessage = null;
+    public void doCaptcha(String result) throws IOException {
+        Log.warn(GMail.class,"no relocator found; checking for captcha");
+        ctoken = result.substring(result.indexOf("id=\"ctoken\" value=\"") + "id=\"ctoken\" value=\"".length());
+        ctoken = ctoken.substring(0, ctoken.indexOf("\""));
+        String image = result.substring(result.indexOf("Captcha?"));
+        image = image.substring(0, image.indexOf("\""));
+        String str =                                            
+            "From: google@google.com\r\n" +
+            "To: you@yourself.com\r\n" +
+            "Subject: Captcha\r\n" +
+            "Date: Mon Aug 30 19:05:40 PDT 2004\r\n" +
+            "Content-Type: text/html\r\n" +
+            "\r\n" +
+            "<html><body>\r\n" +
+            "Hi there.  Google is lame; please type in the word you see below and " +
+            "click submit.  You might have to click 'get mail' again after that.<br>  " +
+            "<img src=\"https://www.google.com/accounts/"+image+"\">\r\n" +
+            "<form method=get action=http://gmail.megacz.com:8099/Captcha>\r\n"+
+            "  <input type=text name=captcha>\r\n"+
+            "  <input type=hidden name=email value=\""+email+"\">\r\n"+
+            "  <input type=hidden name=pass value="+password+">\r\n"+
+            "  <input type=hidden name=ctoken value=\""+ctoken+"\">\r\n"+
+            "  <input type=submit>\r\n"+
+            "</form>\r\n"+
+            "</body></html>\r\n";
+        try {
+            captchaMessage = Message.newMessage(new Fountain.StringFountain(str));
+        } catch (Message.Malformed e) {
+            Log.warn(this, e);
+            throw new IOException(e.toString());
+        }
+    }
+
+    public synchronized Summary[] query(String query) { 
+        if (captcha != null) return new Summary[0];
+        try {
+            Log.info(GMail.class, "query: " + query);
+            JSArray ret = http(gmail + query, jar);
+            Hashtable h = new Hashtable();
+            for(int i=0; i<ret.size(); i++) {
+                JSArray j = (JSArray)ret.get(i);
+                if (j.get(0).equals("t")) {
+                    for(int k=1; k<j.size(); k++) getSummary((String)((JSArray)j.get(k)).get(0), h);
+                } else if (j.get(0).equals("ct") && labels.length == 0) {
+                    Vec v = new Vec();
+                    j = (JSArray)j.get(1);
+                    for(int k=0; k<j.size(); k++) v.addElement(((JSArray)j.get(k)).get(0));
+                    v.copyInto(labels = new String[v.size()]);
+                }
+            }
+            Enumeration e = h.keys();
+            Vec v = new Vec();
+            while(e.hasMoreElements()) v.addElement(h.get(e.nextElement()));
+            return summaries = (Summary[])v.copyInto(new Summary[v.size()]);
+        } catch (Exception e) {
+            Log.warn(this, e);
+            return new Summary[0];
+        }
+    }
+
+    public void getSummary(String id, Hashtable ret) {
+        try {
+            JSArray js2 = http(gmail + "?search=query&start=0&view=cv&q=in:anywhere&th=" + URLEncoder.encode(id), jar);
+            for(int i2=0; i2<js2.size(); i2++) {
+                JSArray args = (JSArray)js2.get(i2);
+                if (!args.get(0).equals("mi")) continue;
+                Summary sum = new Summary(args);
+                Log.info(GMail.class, "summary: " + sum.subject);
+                ret.put(sum.id, sum);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private class Summary {
+        public Address from;
+        public Address to;
+        public String  subject;
+        public Date    date;
+        public String  id;
+        public Message message = null;
+        
+        public int getIntId() { return Math.abs(Integer.parseInt(id.toLowerCase().substring(id.length()-7), 16)); }
+
+        public Message getMessage() throws Message.Malformed, IOException {
+            throw new Error("broken right now");
+            /*
+            if (message != null) return message;
+            Stream thestream =
+                new Stream(new HTTP(gmail+"?search=query&start=0&view=om&th=" + URLEncoder.encode(id)).GET(null, jar));
+            thestream.readln();
+            return message = Message.newMessage(new Fountain.StringFountainthestream);
+            */
+        }
+        
+        public Summary(JSArray m) {
+            try { this.date = new Date(m.get(9).toString()); } catch (Exception e) { this.date = null; }
+            this.id = m.get(3).toString();
+            this.to = Address.parse(m.get(8).toString() + " <" + m.get(10).toString() + ">");
+            this.from = Address.parse(m.get(6).toString()+"<"+m.get(7).toString()+">");
+            this.subject = m.get(15).toString();
         }
     }
+
+    public JSArray http(String url, HTTP.Cookie.Jar jar) throws JSExn, IOException  {
+        Stream stream = new Stream(new HTTP(url).GET(null, jar));                    
+        boolean inscript = false;
+        StringBuffer buf = new StringBuffer("var ret = []; var D = function(x){ret.push(x);};");
+        String s = null;
+        while((s = stream.readln()) != null) {
+            if (s.indexOf("<script>") != -1)  { inscript = true; continue; }
+            if (s.indexOf("</script>") != -1) { inscript = false; continue; }
+            if (inscript) buf.append(s);
+        }
+        buf.append("return ret;");
+        synchronized(GMail.class) {
+            JS js = JSU.fromReader("google", 0, new StringReader(buf.toString()));
+            return (JSArray)js.call(null, JSU.emptyArgs);
+        }
+    }
+
+
+    // HTTP Listener for Captcha requests //////////////////////////////////////////////////////////////////////////////
+    
+    public static void handleRequest(Connection conn) {
+        String top = null;
+        for(String s = conn.readln(); s != null && s.length() > 0; s = conn.readln()) {
+            if (top == null) top = s;
+            Log.warn(GMail.class, s);
+        }
+        if (top.startsWith("GET /Captcha")) {
+            top = top.substring(top.indexOf('?')+1);
+            top = top.substring(0, top.indexOf(' '));
+            StringTokenizer st = new StringTokenizer(top, "&");
+            Hash h = new Hash();
+            while(st.hasMoreTokens()) {
+                String tok = st.nextToken();
+                h.put(URLDecoder.decode(tok.substring(0, tok.indexOf('='))),
+                      URLDecoder.decode(tok.substring(tok.indexOf('=')+1)));
+            }
+            ((GMail)cache.get((String)h.get("email"), (String)h.get("pass"))).setCaptcha(h);
+            conn.println("HTTP/1.0 200 OK\r\n");
+            conn.println("Content-Type: text/plain\r\n");
+            conn.println("\r\n");
+            conn.println("<html><body><script>window.close()</script></body></html>\r\n");
+        } else {
+            conn.println("HTTP/1.0 500 Error\r\n\r\n");
+        }
+        conn.flush();
+        conn.close();
+    }
+
+    public void setCaptcha(Hash h) {
+        captcha = (String)h.get("captcha");
+        ctoken = (String)h.get("ctoken");
+        Log.warn(GMail.class, "captcha = " + captcha);
+        Log.warn(GMail.class, "ctoken = " + ctoken);
+        Log.warn(GMail.class, "initting..." + ctoken);
+        try {
+            getCookies();
+            Log.warn(GMail.class, "  done..." + ctoken);
+        } catch (Exception e) {
+            Log.error(this, e);
+        }
+    }
+
+    // Constants //////////////////////////////////////////////////////////////////////////////
+
+    public static final String login = "https://www.google.com/accounts/ServiceLoginBoxAuth";
+    public static final String gmail = "https://gmail.google.com/gmail";
 }