preliminary GMail support
[org.ibex.mail.git] / src / org / ibex / mail / protocol / GMail.java
1 package org.ibex.mail.protocol;
2 import org.ibex.io.*;
3 import org.ibex.crypto.*;
4 import org.ibex.jinetd.Listener;
5 import org.ibex.jinetd.Worker;
6 import org.ibex.mail.*;
7 import org.ibex.util.*;
8 import org.ibex.net.*;
9 import org.ibex.js.*;
10 import org.ibex.mail.target.*;
11 import java.util.*;
12 import java.net.*;
13 import java.text.*;
14 import java.io.*;
15
16 public class GMail extends Account {
17
18     public static final String login = "https://www.google.com/accounts/ServiceLoginBoxAuth";
19     public static final String gmail = "https://gmail.google.com/gmail";
20
21     private HTTP.Cookie.Jar jar = new HTTP.Cookie.Jar();
22     private String captchaString = null;
23     private String ctoken = null;
24     private String email = null;
25     private String password = null;
26
27
28     // Constructor //////////////////////////////////////////////////////////////////////////////
29
30     private static Hash cache = new Hash();
31     static { HTTP.userAgent = "Mozilla/5.0 (compatible;)"; }
32     public static GMail getGMail(String email, String pass) {
33         try {
34             GMail g = (GMail)cache.get(email, pass);
35             if (g == null) cache.put(email, pass, g = new GMail(email, pass));
36             g.init();
37             return g;
38         } catch (Exception e) {
39             Log.error(GMail.class, e);
40             return null;
41         }
42     }
43
44     public GMail(String email, String pass) throws IOException {
45         super(email.substring(0, email.indexOf('@')), Address.parse(email));
46         this.email = email; this.password = pass;
47         Log.warn(GMail.class, "logging in " + email);
48     }
49
50     void init() throws IOException {
51         if (success) return;
52         Vector v = makeRequest();
53         Message[] m = new Message[v.size()];
54         v.copyInto(m);
55         final Mailbox inbox = new MessageArrayMailbox(m);
56         this.root =
57             new Mailbox.Default() {
58                 public void add(Message m)             { throw new RuntimeException("not supported"); }
59                 public void add(Message m, int i)      { throw new RuntimeException("not supported"); }
60                 public Mailbox.Iterator iterator()     { return new Mailbox.Iterator.NullIterator(); }
61                 public int              uidNext()      { return 500; }
62                 public String[] children() {
63                     if (success) return new String[] { "INBOX" };
64                     return new String[] { "Captcha" };
65                 }
66                 public Mailbox slash(String name, boolean create) { return inbox; }
67             };
68         Log.warn(GMail.class, "    succeeded for " + email);
69     }
70
71
72     // Implementation //////////////////////////////////////////////////////////////////////////////
73
74     public Vector makeRequest() throws IOException {
75         final Vector messages = new Vector();
76         Log.warn(this, "captchaString = " + captchaString);
77         Log.warn(this, "ctoken = " + ctoken);
78         String crud =
79             "continue="     + URLEncoder.encode(gmail) +
80             "&service=mail" +
81             "&Email="       + URLEncoder.encode(user) +
82             "&Passwd="      + password + 
83             "&null=Sign+in" +
84             (captchaString != null ? "&captcha="+URLEncoder.encode(captchaString) : "") +
85             (captchaString != null ? "&ctoken="+URLEncoder.encode(ctoken) : "");
86         captchaString = null;
87         Log.warn("[request]", crud);
88
89         String result =
90             captchaString == null ?
91             new String(InputStreamToByteArray.convert(new HTTP(login+"?"+crud).GET(null, jar)))
92             :
93             new String(InputStreamToByteArray.convert(new HTTP(login).POST("application/x-www-form-urlencoded", crud, null, jar)));
94         System.err.println(result);
95         
96         try {
97             if (result.indexOf("top.location") == -1) {
98                 Log.warn(GMail.class,"no relocator found; checking for captcha");
99                 ctoken = result.substring(result.indexOf("id=\"ctoken\" value=\"") + "id=\"ctoken\" value=\"".length());
100                 ctoken = ctoken.substring(0, ctoken.indexOf("\""));
101
102                 Log.warn(this, "captchaString = " + captchaString);
103                 Log.warn(this, "ctoken = " + ctoken);
104
105                 String image = result.substring(result.indexOf("Captcha?"));
106                 image = image.substring(0, image.indexOf("\""));
107                 Message m = new Message(new Stream(
108                                                    "From: google@google.com\r\n" +
109                                                    "To: you@yourself.com\r\n" +
110                                                    "Subject: Captcha\r\n" +
111                                                    "Date: Mon Aug 30 19:05:40 PDT 2004\r\n" +
112                                                    "Content-Type: text/html\r\n" +
113                                                    "\r\n" +
114                                                    "<html><body>\r\n" +
115                                                    "Hi there.  Google is lame; please type in the word you see below and " +
116                                                    "click submit.  You might have to click 'get mail' again after that.<br>  " +
117                                                    "<img src=\"https://www.google.com/accounts/"+image+"\">\r\n" +
118                                                    "<form method=get action=http://testing.megacz.com:8099/Captcha>\r\n"+
119                                                    "  <input type=text name=captcha>\r\n"+
120                                                    "  <input type=hidden name=email value=\""+email+"\">\r\n"+
121                                                    "  <input type=hidden name=pass value="+password+">\r\n"+
122                                                    "  <input type=hidden name=ctoken value=\""+ctoken+"\">\r\n"+
123                                                    "  <input type=submit>\r\n"+
124                                                    "</form>\r\n"+
125                                                    "</body></html>\r\n"),
126                                         null);
127                 messages.addElement(m);
128                 return messages;
129             }
130         } catch (Exception e) {
131             e.printStackTrace();
132             return messages;
133         }
134         result = result.substring(result.indexOf("top.location"));
135         result = result.substring(result.indexOf("CheckCookie?continue=") + "CheckCookie?continue=".length());
136         result = result.substring(0, result.indexOf('\"'));
137         result = URLDecoder.decode(result);
138         Log.warn("[]", "getting " + result);
139
140         // just need the cookie off of this page
141         String s = new String(InputStreamToByteArray.convert(new HTTP(result).GET(null, jar)));
142         Log.warn("[]", s);
143
144         final Semaphore sem = new Semaphore();
145         try {
146             JSArray ret = grab(gmail + "?search=inbox&start=0&view=tl", jar);
147             for(int i=0; i<ret.length(); i++) {
148                 JSArray j = (JSArray)ret.get(i);
149                 if (!j.elementAt(0).equals("t")) continue;
150                 for(int k=1; k<j.length(); k++) {
151                     final String threadid = (String)((JSArray)j.get(k)).get(0);
152                     sem.dec();
153                     new Thread() { public void run() {
154                         try {
155                             // FIXME: future: pipeline these requests over a single socket (maybe two)
156                             JSArray js2 =
157                                 grab(gmail + "?search=query&start=0&view=cv&q=in:anywhere&th=" + URLEncoder.encode(threadid), jar);
158                             for(int i2=0; i2<js2.length(); i2++) {
159                                 JSArray j2 = (JSArray)js2.get(i2);
160                                 if (j2.elementAt(0).equals("t")) {
161                                     for(int k2=1; k2<j2.length(); k2++) {
162                                         JSArray m = (JSArray)j2.get(k2);
163                                         String from = (String)m.get(4);
164                                         String email = from.substring(from.indexOf("_email_") + "_email_".length());
165                                         email = email.substring(0, email.indexOf("\'"));
166                                         String name = from.substring(from.indexOf('>') + 1);
167                                         name = name.substring(0, name.indexOf("</span>"));
168                                         String subject = (String)m.get(6);
169                                         //Log.warn("[]", name + " <"+email+"> " + subject);
170                                         //Log.warn("[]", );
171                                     }
172                                 } else if (j2.elementAt(0).equals("mi")) {
173                                     JSArray m = j2;
174                                     String date = m.get(9).toString();
175                                     String to = m.get(8).toString();
176                                     String toemail = m.get(10).toString();
177                                     String from = m.get(4).toString();
178                                     String name = m.get(6).toString();
179                                     String email = m.get(7).toString();
180                                     String subject = m.get(15).toString();
181                                     Log.warn("[]", name + " <"+email+"> " + subject);
182                                     Stream thestream = new Stream(new HTTP(gmail+"?search=query&start=0&view=om&th=" +
183                                                                            URLEncoder.encode(threadid)).GET(null, jar));
184                                     thestream.readln();
185                                     messages.addElement(new Message(thestream, null));
186                                 }
187                             }
188                         } catch (Exception e) {
189                             e.printStackTrace();
190                         } finally {
191                             sem.release();
192                         }
193                     } }.start();
194                 }
195             }
196             sem.release();
197             Log.warn(GMail.class, "block");
198             sem.block();
199             Log.warn(GMail.class, "release");
200         } catch (Exception e) {
201             e.printStackTrace();
202         }
203         success = true;
204         for(int i=0; i<connections.size(); i++)
205             try {
206                 ((Connection)connections.elementAt(i)).close();
207             } catch (Exception e) {
208                 Log.error(this, e);
209             }
210         return messages;
211     }
212
213     boolean success = false;
214
215     public JSArray grab(String url, HTTP.Cookie.Jar jar) throws JSExn, IOException  {
216         Stream stream = new Stream(new HTTP(url).GET(null, jar));                    
217         boolean inscript = false;
218         StringBuffer buf = new StringBuffer("var ret = []; var D = function(x){ret.push(x);};");
219         String s = null;
220         while((s = stream.readln()) != null) {
221             if (s.indexOf("<script>") != -1)  { inscript = true; continue; }
222             if (s.indexOf("</script>") != -1) { inscript = false; continue; }
223             if (inscript) buf.append(s);
224         }
225         buf.append("return ret;");
226         synchronized(GMail.class) {
227             JS js = JS.fromReader("google", 0, new StringReader(buf.toString()));
228             return (JSArray)js.call(null, null, null, null, 0);
229         }
230     }
231
232     public Mailbox getMailbox(Class protocol) { return this.root; }
233
234
235     // HTTP Listener for Captcha requests //////////////////////////////////////////////////////////////////////////////
236     
237     public static void handleRequest(Connection conn) {
238         String top = null;
239         for(String s = conn.readln(); s != null && s.length() > 0; s = conn.readln()) {
240             if (top == null) top = s;
241             Log.warn(GMail.class, s);
242         }
243         if (top.startsWith("GET /Captcha")) {
244             top = top.substring(top.indexOf('?')+1);
245             top = top.substring(0, top.indexOf(' '));
246             StringTokenizer st = new StringTokenizer(top, "&");
247             Hash h = new Hash();
248             while(st.hasMoreTokens()) {
249                 String tok = st.nextToken();
250                 h.put(URLDecoder.decode(tok.substring(0, tok.indexOf('='))),
251                       URLDecoder.decode(tok.substring(tok.indexOf('=')+1)));
252             }
253             ((GMail)cache.get((String)h.get("email"), (String)h.get("pass"))).setCaptcha(h);
254             conn.println("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n<html><body><script>window.close()</script></body></html>\r\n");
255         } else {
256             conn.println("HTTP/1.0 500 Error\r\n\r\n");
257         }
258         conn.flush();
259         conn.close();
260     }
261
262     public void setCaptcha(Hash h) {
263         captchaString = (String)h.get("captcha");
264         ctoken = (String)h.get("ctoken");
265         Log.warn(GMail.class, "captchaString = " + captchaString);
266         Log.warn(GMail.class, "ctoken = " + ctoken);
267         Log.warn(GMail.class, "initting..." + ctoken);
268         try {
269             init();
270             Log.warn(GMail.class, "  done..." + ctoken);
271         } catch (Exception e) {
272             Log.error(this, e);
273         }
274     }
275 }