1a5d4efb0e43bf07499f8a15162165426659dceb
[org.ibex.mail.git] / src / org / ibex / mail / protocol / GMail.java
1 // Copyright 2000-2005 the Contributors, as shown in the revision logs.
2 // Licensed under the Apache Public Source License 2.0 ("the License").
3 // You may not use this file except in compliance with the License.
4
5 package org.ibex.mail.protocol;
6 import org.ibex.crypto.*;
7 import org.ibex.mail.protocol.*;
8 import org.ibex.jinetd.Listener;
9 import org.ibex.mail.*;
10 import org.ibex.util.*;
11 import org.ibex.net.*;
12 import org.ibex.js.*;
13 import org.ibex.mail.target.*;
14 import java.util.*;
15 import java.net.*;
16 import java.text.*;
17 import java.io.*;
18 import org.ibex.io.*;
19 import org.ibex.io.Fountain;
20
21 public class GMail extends Account {
22
23     private      GMailIMAP imap = new GMailIMAP();
24     private HTTP.Cookie.Jar jar = new HTTP.Cookie.Jar();
25     private      String captcha = null;
26     private       String ctoken = null;
27     private        String email = null;
28     private     String password = null;
29     private     boolean invalid = false;
30     private     String[] labels = new String[0];
31     private    String[] queries = new String[0];
32     private Summary[] summaries = new Summary[0];
33
34     // Constructor, Pooling  ///////////////////////////////////////////////////////////////////////////
35
36     public GMail(String email, String pass) throws IOException {
37         super(email.substring(0, email.indexOf('@')), Address.parse(email));
38         this.email = email; this.password = pass;
39         Log.warn(GMail.class, "logging in " + email);
40         getCookies();
41     }
42
43     public void close() { cache.remove(email, password); invalid = true; }
44
45     private static Hash cache = new Hash();
46     static { HTTP.userAgent = "Mozilla/5.0 (compatible;)"; }
47     public static GMail getGMail(String email, String pass) {
48         try {
49             GMail g = (GMail)cache.get(email, pass);
50             if (g == null) cache.put(email, pass, g = new GMail(email, pass));
51             return g;
52         } catch (Exception e) {
53             Log.error(GMail.class, e);
54             return null;
55         }
56     }
57
58     // IMAP Interface //////////////////////////////////////////////////////////////////////////////
59
60     public IMAP.Server getIMAP() { return imap; }
61     private class GMailIMAP implements IMAP.Server {
62
63         private String query = "?search=inbox&start=0&view=tl";
64         private int validity = new Random().nextInt();
65
66         private IMAP.Client client;
67         public void setClient(IMAP.Client client) { this.client = client; }
68
69         public String[]  capability() { return new String[] { }; }
70         public Hashtable id(Hashtable clientId) { return null; }
71         public void      logout() { GMail.this.close(); }
72         public void      subscribe(String mailbox) { }
73         public void      unsubscribe(String mailbox) { }
74
75         public void      unselect() { summaries = new Summary[0]; }
76         public void      close() { summaries = new Summary[0]; }
77
78         public void      noop() { check(); }
79         public void      expunge() { }
80
81         public void      setFlags(Query q, int flags, boolean uid, boolean silent) { }
82         public void      removeFlags(Query q, int flags, boolean uid, boolean silent) { }
83         public void      addFlags(Query q, int flags, boolean uid, boolean silent) { }
84
85         public void      rename(String from, String to) { }
86         public void      delete(String m) { }
87         public void      create(String m) {
88             String[] newqueries = new String[queries.length+1];
89             System.arraycopy(queries, 0, newqueries, 0, queries.length);
90             newqueries[queries.length] = m;
91             queries = newqueries;
92         }
93
94         public void      append(String m, int flags, Date arrival, String body) { }
95         public void      copy(Query q, String to) { }
96
97         public void      check() { query(query); }
98         public void      select(String mailbox, boolean examineOnly) {
99             String oldquery = query;
100             if (mailbox.equalsIgnoreCase("inbox")) {
101                 query = "?search=inbox&start=0&view=tl";
102                 if (!query.equals(oldquery)) check();
103                 return;
104             }
105             for(int i=0; i<labels.length; i++) {
106                 if (labels[i].equals(mailbox)) {
107                     query = "?search=cat&cat="+mailbox+"&inbox&start=0&view=tl";
108                     if (!query.equals(oldquery)) check();
109                     return;
110                 }
111             }
112             query = "?search=query&q="+URLEncoder.encode(mailbox)+"&start=0&view=tl";
113             if (!query.equals(oldquery)) check();
114         }
115
116         public void      lsub(String start, String ref) { list(true); }
117         public void      list(String start, String ref) { list(false); }
118         private void list(boolean lsub) {
119             client.list('/', "inbox", lsub, false);
120             for(int i=0; i<labels.length; i++) client.list('/', labels[i], lsub, false);
121             for(int i=0; i<queries.length; i++) client.list('/', queries[i], lsub, false);
122         }
123
124         public int[]     search(Query q, boolean uid) { return null; }
125         public void      fetch(Query q, int spec, String[] headers, int start, int end, boolean uid) {
126             if (captchaMessage != null) { client.fetch(1, 0, 100, captchaMessage, 100); return; }
127             for(int i=0; i<summaries.length; i++) try {
128                 final Message m = summaries[i].getMessage();
129                 final int num = i;
130                 if (q.match(new Mailbox.Default.Iterator() {
131                         public Message cur() { return m; }
132                         public Headers head() { return m.headers; }
133                         public boolean next() { return false; }
134                         public int     uid() { return num; }
135                         public int     imapNumber() { return num; }
136                         public int     nntpNumber() { throw new RuntimeException("not supported"); }
137                         public void    delete() { }
138                         public void    set(String key, String val) { }
139                         public String  get(String key) { return null; }
140                         public boolean seen() { return false; }
141                         public boolean deleted() { return false; }
142                         public boolean flagged() { return false; }
143                         public boolean draft() { return false;}
144                         public boolean answered() { return false; }
145                         public boolean recent() { return true; }
146                         public void    seen(boolean on) { }
147                         public void    deleted(boolean on) { }
148                         public void    flagged(boolean on) { }
149                         public void    draft(boolean on) { }
150                         public void    answered(boolean on) { }
151                         public void    recent(boolean on) { }
152                         public int     flags() { return 0; }
153                         public void    addFlags(int flags) { }
154                         public void    removeFlags(int flags) { }
155                         public void    setFlags(int flags) { }
156                     })) {
157                     Log.info(GMail.class, "fetch " + summaries[i].subject);
158                     throw new Error("broken");
159                     //client.fetch(i+1, 0, m.size(), m,/* summaries[i].getIntId()*/ i);
160                 }
161             } catch (Exception e) { Log.warn(this, e); }
162         }
163
164         public int       unseen(String mailbox)      { return summaries.length; }
165         public int       recent(String mailbox)      { return summaries.length; }
166         public int       count(String mailbox)       { return summaries.length; }
167         public int       count()       { return summaries.length; }
168         public int       maxuid()      { return summaries.length; }
169         public int       uidNext(String mailbox)     { return summaries.length+1; }
170         public int       uidValidity(String mailbox) { return validity; }
171     }
172
173     public void getCookies() throws IOException {
174         jar = new HTTP.Cookie.Jar();
175         String params =
176             "continue="     + URLEncoder.encode(gmail) +
177             "&service=mail" +
178             "&Email="       + URLEncoder.encode(user) +
179             "&Passwd="      + password + 
180             "&null=Sign+in" +
181             (captcha != null ? "&captcha="+URLEncoder.encode(captcha) : "") +
182             (captcha != null ? "&ctoken="+URLEncoder.encode(ctoken) : "");
183         Log.info("[request]", params);
184         HTTP http = captcha==null ? new HTTP(login+"?"+params) : new HTTP(login);
185         InputStream reply = captcha==null ?
186             http.GET(null, jar) :
187             http.POST("application/x-www-form-urlencoded", params, null, jar);
188         String result = new String(InputStreamToByteArray.convert(reply));
189         System.err.println(result);
190         captcha = null;
191         
192         if (result.indexOf("top.location") == -1) { doCaptcha(result); return; }
193         result = result.substring(result.indexOf("top.location"));
194         result = result.substring(result.indexOf("CheckCookie?continue=") + "CheckCookie?continue=".length());
195         result = result.substring(0, result.indexOf('\"'));
196         result = URLDecoder.decode(result);
197         Log.warn("[]", "getting " + result);
198
199         // just need the cookie off of this page
200         InputStreamToByteArray.convert(new HTTP(result).GET(null, jar));
201         imap.check();
202         Log.warn("[]", "done");
203     }
204
205     Message captchaMessage = null;
206     public void doCaptcha(String result) throws IOException {
207         Log.warn(GMail.class,"no relocator found; checking for captcha");
208         ctoken = result.substring(result.indexOf("id=\"ctoken\" value=\"") + "id=\"ctoken\" value=\"".length());
209         ctoken = ctoken.substring(0, ctoken.indexOf("\""));
210         String image = result.substring(result.indexOf("Captcha?"));
211         image = image.substring(0, image.indexOf("\""));
212         String str =                                            
213             "From: google@google.com\r\n" +
214             "To: you@yourself.com\r\n" +
215             "Subject: Captcha\r\n" +
216             "Date: Mon Aug 30 19:05:40 PDT 2004\r\n" +
217             "Content-Type: text/html\r\n" +
218             "\r\n" +
219             "<html><body>\r\n" +
220             "Hi there.  Google is lame; please type in the word you see below and " +
221             "click submit.  You might have to click 'get mail' again after that.<br>  " +
222             "<img src=\"https://www.google.com/accounts/"+image+"\">\r\n" +
223             "<form method=get action=http://gmail.megacz.com:8099/Captcha>\r\n"+
224             "  <input type=text name=captcha>\r\n"+
225             "  <input type=hidden name=email value=\""+email+"\">\r\n"+
226             "  <input type=hidden name=pass value="+password+">\r\n"+
227             "  <input type=hidden name=ctoken value=\""+ctoken+"\">\r\n"+
228             "  <input type=submit>\r\n"+
229             "</form>\r\n"+
230             "</body></html>\r\n";
231         try {
232             captchaMessage = Message.newMessage(new Fountain.StringFountain(str));
233         } catch (Message.Malformed e) {
234             Log.warn(this, e);
235             throw new IOException(e.toString());
236         }
237     }
238
239     public synchronized Summary[] query(String query) { 
240         if (captcha != null) return new Summary[0];
241         try {
242             Log.info(GMail.class, "query: " + query);
243             JSArray ret = http(gmail + query, jar);
244             Hashtable h = new Hashtable();
245             for(int i=0; i<ret.size(); i++) {
246                 JSArray j = (JSArray)ret.get(i);
247                 if (j.get(0).equals("t")) {
248                     for(int k=1; k<j.size(); k++) getSummary((String)((JSArray)j.get(k)).get(0), h);
249                 } else if (j.get(0).equals("ct") && labels.length == 0) {
250                     Vec v = new Vec();
251                     j = (JSArray)j.get(1);
252                     for(int k=0; k<j.size(); k++) v.addElement(((JSArray)j.get(k)).get(0));
253                     v.copyInto(labels = new String[v.size()]);
254                 }
255             }
256             Enumeration e = h.keys();
257             Vec v = new Vec();
258             while(e.hasMoreElements()) v.addElement(h.get(e.nextElement()));
259             return summaries = (Summary[])v.copyInto(new Summary[v.size()]);
260         } catch (Exception e) {
261             Log.warn(this, e);
262             return new Summary[0];
263         }
264     }
265
266     public void getSummary(String id, Hashtable ret) {
267         try {
268             JSArray js2 = http(gmail + "?search=query&start=0&view=cv&q=in:anywhere&th=" + URLEncoder.encode(id), jar);
269             for(int i2=0; i2<js2.size(); i2++) {
270                 JSArray args = (JSArray)js2.get(i2);
271                 if (!args.get(0).equals("mi")) continue;
272                 Summary sum = new Summary(args);
273                 Log.info(GMail.class, "summary: " + sum.subject);
274                 ret.put(sum.id, sum);
275             }
276         } catch (Exception e) {
277             e.printStackTrace();
278         }
279     }
280
281     private class Summary {
282         public Address from;
283         public Address to;
284         public String  subject;
285         public Date    date;
286         public String  id;
287         public Message message = null;
288         
289         public int getIntId() { return Math.abs(Integer.parseInt(id.toLowerCase().substring(id.length()-7), 16)); }
290
291         public Message getMessage() throws Message.Malformed, IOException {
292             throw new Error("broken right now");
293             /*
294             if (message != null) return message;
295             Stream thestream =
296                 new Stream(new HTTP(gmail+"?search=query&start=0&view=om&th=" + URLEncoder.encode(id)).GET(null, jar));
297             thestream.readln();
298             return message = Message.newMessage(new Fountain.StringFountainthestream);
299             */
300         }
301         
302         public Summary(JSArray m) {
303             try { this.date = new Date(m.get(9).toString()); } catch (Exception e) { this.date = null; }
304             this.id = m.get(3).toString();
305             this.to = Address.parse(m.get(8).toString() + " <" + m.get(10).toString() + ">");
306             this.from = Address.parse(m.get(6).toString()+"<"+m.get(7).toString()+">");
307             this.subject = m.get(15).toString();
308         }
309     }
310
311     public JSArray http(String url, HTTP.Cookie.Jar jar) throws JSExn, IOException  {
312         Stream stream = new Stream(new HTTP(url).GET(null, jar));                    
313         boolean inscript = false;
314         StringBuffer buf = new StringBuffer("var ret = []; var D = function(x){ret.push(x);};");
315         String s = null;
316         while((s = stream.readln()) != null) {
317             if (s.indexOf("<script>") != -1)  { inscript = true; continue; }
318             if (s.indexOf("</script>") != -1) { inscript = false; continue; }
319             if (inscript) buf.append(s);
320         }
321         buf.append("return ret;");
322         synchronized(GMail.class) {
323             JS js = JSU.fromReader("google", 0, new StringReader(buf.toString()));
324             return (JSArray)js.call(null, JSU.emptyArgs);
325         }
326     }
327
328
329     // HTTP Listener for Captcha requests //////////////////////////////////////////////////////////////////////////////
330     
331     public static void handleRequest(Connection conn) {
332         String top = null;
333         for(String s = conn.readln(); s != null && s.length() > 0; s = conn.readln()) {
334             if (top == null) top = s;
335             Log.warn(GMail.class, s);
336         }
337         if (top.startsWith("GET /Captcha")) {
338             top = top.substring(top.indexOf('?')+1);
339             top = top.substring(0, top.indexOf(' '));
340             StringTokenizer st = new StringTokenizer(top, "&");
341             Hash h = new Hash();
342             while(st.hasMoreTokens()) {
343                 String tok = st.nextToken();
344                 h.put(URLDecoder.decode(tok.substring(0, tok.indexOf('='))),
345                       URLDecoder.decode(tok.substring(tok.indexOf('=')+1)));
346             }
347             ((GMail)cache.get((String)h.get("email"), (String)h.get("pass"))).setCaptcha(h);
348             conn.println("HTTP/1.0 200 OK\r\n");
349             conn.println("Content-Type: text/plain\r\n");
350             conn.println("\r\n");
351             conn.println("<html><body><script>window.close()</script></body></html>\r\n");
352         } else {
353             conn.println("HTTP/1.0 500 Error\r\n\r\n");
354         }
355         conn.flush();
356         conn.close();
357     }
358
359     public void setCaptcha(Hash h) {
360         captcha = (String)h.get("captcha");
361         ctoken = (String)h.get("ctoken");
362         Log.warn(GMail.class, "captcha = " + captcha);
363         Log.warn(GMail.class, "ctoken = " + ctoken);
364         Log.warn(GMail.class, "initting..." + ctoken);
365         try {
366             getCookies();
367             Log.warn(GMail.class, "  done..." + ctoken);
368         } catch (Exception e) {
369             Log.error(this, e);
370         }
371     }
372
373     // Constants //////////////////////////////////////////////////////////////////////////////
374
375     public static final String login = "https://www.google.com/accounts/ServiceLoginBoxAuth";
376     public static final String gmail = "https://gmail.google.com/gmail";
377 }