Source code: org/mrbook/mrpostman/another/AnotherMailSession.java
1 /*
2 * -*- mode: java; c-basic-indent: 4; indent-tabs-mode: nil -*-
3 * :indentSize=4:noTabs=true:tabSize=4:indentOnTab=true:indentOnEnter=true:mode=java:
4 * ex: set tabstop=4 expandtab:
5 *
6 * MrPostman - webmail <-> email gateway
7 * Copyright (C) 2002-2003 MrPostman Development Group
8 * Projectpage: http://mrbook.org/mrpostman/
9 *
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 * In particular, this implies that users are responsible for
21 * using MrPostman after reading the terms and conditions given
22 * by their web-mail provider.
23 *
24 * You should have received a copy of the GNU General Public License
25 * Named LICENSE in the base directory of this distribution,
26 * if not, write to the Free Software
27 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 */
29
30 package org.mrbook.mrpostman.another;
31
32 import com.sonalb.net.http.HTTPRedirectHandler;
33 import com.sonalb.net.http.cookie.CookieJar;
34
35 import org.mrbook.mrpostman.MailSessionException;
36 import org.mrbook.mrpostman.ModuleInfo;
37 import org.mrbook.mrpostman.ModuleOption;
38
39 /*
40 Another.com module (v0.6) for MrPostman.
41 Supports retrieval of all Inbox messages.
42
43 Ensure your account username in your mail program is set abc@another.com
44 Note: MrPostman will not recognize the custom addresses you may have created.
45 Ensure you have created a folder on another.com called 'READ'.
46
47 This module currently will NOT delete messages.
48 This module cannot determine if a message is read or unread (another.com doesn't
49 indicate this to us except by colour - which is user configurable!).
50 Rather than marking a message as read it will move it from the INBOX to a folder
51 called READ. You must have a folder with this name for this to work!
52 All message bodies are returned as HTML.
53
54 Written by Chris Humphreys Nov 2002 <chris@sonicdreams.org>
55 Based the YahooMainSession by Hector Urtubia <urtubia@mrbook.org>
56 Uses jCookie by Sonal Bansal.
57
58 This program is free software; you can redistribute it and/or modify
59 it under the terms of the GNU General Public License as published by
60 the Free Software Foundation; either version 2 of the License, or
61 (at your option) any later version.
62
63 This program is distributed in the hope that it will be useful,
64 but WITHOUT ANY WARRANTY; without even the implied warranty of
65 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
66 GNU General Public License for more details.
67
68 You should have received a copy of the GNU General Public License
69 along with this program; if not, write to the Free Software
70 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
71 */
72 import org.mrbook.mrpostman.WebMailSession;
73 import org.mrbook.mrpostman.another.summary.HeaderInfo;
74
75 import java.io.BufferedReader;
76 import java.io.ByteArrayOutputStream;
77 import java.io.IOException;
78 import java.io.InputStream;
79 import java.io.InputStreamReader;
80 import java.io.PrintWriter;
81
82 import java.net.HttpURLConnection;
83 import java.net.URL;
84
85 import java.util.Iterator;
86 import java.util.LinkedHashSet;
87 import java.util.LinkedList;
88 import java.util.logging.Level;
89 import java.util.logging.Logger;
90 import java.util.regex.Matcher;
91 import java.util.regex.Pattern;
92
93
94 public class AnotherMailSession extends WebMailSession {
95 public static final String CVSID = "$Id: AnotherMailSession.java,v 1.14 2003/02/09 23:38:13 lbruand Exp $";
96 private static final String[] AUTHORS = {"Chris Humphreys <chris@sonicdreams.org>"};
97 private static final ModuleOption[] OPTIONS = {
98 new ModuleOption("another.markasread", "true"), new ModuleOption("another.debugpagehtml", "false"),
99 new ModuleOption("another.createsummary", "true")
100 };
101
102 /**
103 * Our hardcoded ModuleInfo data.
104 */
105 private static final ModuleInfo MODULE_INFO = new ModuleInfo("another", AUTHORS, "0.5",
106 "http://mrpostman.sourceforge.net/updates/another", "/en/another/index.html", OPTIONS);
107 private static Logger logger = Logger.getLogger("org.mrbook.mrpostman.another.AnotherMailSession");
108 private static final String UserAgent = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:0.9.4) Gecko/20010914";
109 private static final String baseUrl = "http://www.another.com/";
110 private static final String httpLoginPageURL = baseUrl;
111 private static String loginURL = baseUrl + "servlet/login";
112 private static String logoutURL = baseUrl + "logout.jsp";
113 private static String msgReadBaseUrl = baseUrl + "servlet/viewMessage";
114 private static String inboxUrl = baseUrl + "inbox.jsp?folder=INBOX&bob=yes";
115 private static String moveToReadUrl = baseUrl + "inbox.jsp?action=READ";
116 public static int INVALID_ID = -200;
117 public static int PAGE_STRUCTURE_CHANGED = -200;
118 public static int LOGIN_OK = 100;
119 public static int SYSTEM_ERROR = -200;
120
121 /**
122 * Flag message as read after RETR
123 */
124 public static final boolean FLAG_MSG_AS_READ = true;
125
126 /**
127 * Include full html for the retrieved pages. Useful if the page structure changes.
128 */
129 public static final boolean DEBUG_PAGE_HTML = false;
130
131 /**
132 * Generate summary information for all actions. If this is enabled, a maildrop summary will be output to the log.
133 */
134 public static final boolean CREATE_SUMMARY_INFO = true;
135 private LinkedHashSet inboxPageUrls = null;
136 private LinkedList msgHashCodes = null;
137 private LinkedList readMessages = null;
138 private boolean authenticated = false;
139 int numMessages = 0;
140 private String cb = null;
141 private CookieJar cj = null;
142 private AnotherMailDropSummary summaryInfo = null;
143
144 /**
145 * Perform the login and retrieve the authentication cookie needed for subsequent requests.
146 * We'll do this by:
147 * 1 Retrieving the www.another.com main page (to create a session cookie with this service)
148 * 2 Posting a login block
149 * 3 Verifying the page is not an error page
150 */
151 public int login(String username, String password, boolean secureLogin) {
152 System.getProperties().put("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
153 java.security.Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
154
155 //strip the extension off the user name (if present)...
156 int extIndex = username.indexOf("@");
157
158 if (extIndex > -1) {
159 username = username.substring(0, extIndex);
160 }
161
162 // Retrieve the main page and retrieve the session cookie
163 //Matcher logInMatcher = null, ctMatcher = null;
164 cj = new CookieJar();
165 BufferedReader rd = null;
166
167 try {
168 URL url = new URL(httpLoginPageURL);
169 logger.fine("url: " + url);
170 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
171 conn.setRequestMethod("GET");
172 conn.setRequestProperty("User-Agent", UserAgent);
173 conn.setRequestProperty("Conten-type", "application/x-www-form-urlencoded");
174 conn.setRequestProperty("Accept", "*/*");
175 conn.setRequestProperty("Allowed", "GET HEAD PUT");
176 conn.setInstanceFollowRedirects(false);
177 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
178 hrh.connect();
179 conn = hrh.getConnection();
180 cj.addAll(hrh.getCookieJar());
181
182 rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
183 String line;
184
185 while ((line = rd.readLine()) != null) {
186 logger.finest("html: " + line);
187 }
188 } catch (Exception e) {
189 logger.log(Level.SEVERE, "should not happen", e);
190 } finally {
191 try {
192 if (rd != null) {
193 rd.close();
194 }
195 } catch (IOException ie) {
196 logger.log(Level.SEVERE, "should not happen", ie);
197 }
198 }
199
200 //About to perform login..
201 try {
202 URL url = new URL(loginURL + createPostRequest(username, password));
203 logger.fine("url: " + url);
204 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
205 conn.setRequestProperty("User-Agent", UserAgent);
206 conn.setRequestProperty("Conten-type", "application/x-www-form-urlencoded");
207 conn.setRequestMethod("GET");
208 conn.setRequestProperty("Allowed", "GET HEAD PUT");
209 conn.setInstanceFollowRedirects(false);
210 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
211 hrh.setCookieJar(cj);
212 hrh.connect();
213 conn = hrh.getConnection();
214 cj.addAll(hrh.getCookieJar());
215
216 rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
217 String line;
218 String failure1 = "We're so sorry, something went wrong.";
219 String failure2 = "now why didn't that work?";
220
221 Matcher failureMatcher = null;
222
223 //verify page does not contain password error text...
224 while ((line = rd.readLine()) != null) {
225 logger.finest("html: " + line);
226
227 if ((line.indexOf(failure1) > -1) || (line.indexOf(failure2) > -1)) {
228 logger.warning("ID/Password does not exist/wrong");
229 return INVALID_ID;
230 }
231 }
232 } catch (Exception e) {
233 logger.log(Level.SEVERE, "should not happen", e);
234 return SYSTEM_ERROR;
235 } finally {
236 try {
237 if (rd != null) {
238 rd.close();
239 }
240 } catch (IOException ie) {
241 logger.log(Level.SEVERE, "should not happen", ie);
242 }
243 }
244
245 // Lets retrieve the inbox URL, read the number of messages and determine links to the inbox pages...
246 // Each inbox page is determined by a 'hi=XX' id which represents take the top page size from the
247 // bottom XX set of messages. Therefor the hi id actually decreases as you advance pages.
248 // A 'hi' id uniquely identifies a page, but the link may be repeated in the pages (first, last, 1..2..etc),
249 // so we have to remove duplicates as we collect.
250 // We also need to check for the existance of the 'READ' folder
251 numMessages = 0;
252
253 if (inboxPageUrls == null) {
254 inboxPageUrls = new LinkedHashSet();
255 }
256 BufferedReader br = null;
257
258 try {
259 URL url = new URL(inboxUrl);
260 logger.fine("url: " + url);
261 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
262 conn.setRequestProperty("User-Agent", UserAgent);
263 conn.setRequestProperty("Conten-type", "application/x-www-form-urlencoded");
264 conn.setRequestMethod("GET");
265 conn.setRequestProperty("Allowed", "GET HEAD PUT");
266 conn.setInstanceFollowRedirects(false);
267 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
268 hrh.setCookieJar(cj);
269 hrh.connect();
270 cj.addAll(hrh.getCookieJar());
271 br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
272 String line;
273
274 Pattern inboxPattern = Pattern.compile("<B>INBOX</B>:\\s*(\\d+)\\s*msg");
275 Matcher inboxMatcher = null;
276
277 Pattern pagePattern = Pattern.compile(
278 "<A HREF=\"/inbox.jsp\\?cb=(\\d+)\\&folder=INBOX\\&bob=no\\&hi=(\\d+)\">");
279
280 Matcher pageMatcher = null;
281
282 //<A HREF="/inbox.jsp?folder=READ
283 while ((line = br.readLine()) != null) {
284 logger.finest("html: " + line);
285 inboxMatcher = inboxPattern.matcher(line);
286
287 if (inboxMatcher.find()) {
288 numMessages = Integer.parseInt(inboxMatcher.group(1));
289 }
290 pageMatcher = pagePattern.matcher(line);
291
292 if (pageMatcher.find()) {
293 cb = pageMatcher.group(1);
294 Integer hiId = new Integer(pageMatcher.group(2));
295
296 //add this 'hi' id to our set of pages...
297 inboxPageUrls.add(hiId); //our set will handle duplicates
298 }
299 }
300 } catch (Exception e) {
301 logger.log(Level.SEVERE, "should not happen", e);
302 return SYSTEM_ERROR;
303 } finally {
304 try {
305 if (rd != null) {
306 rd.close();
307 }
308 } catch (IOException ie) {
309 logger.log(Level.SEVERE, "should not happen", ie);
310 }
311 }
312
313 logger.fine("Number of messages: " + numMessages);
314
315 if (logger.isLoggable(Level.FINEST)) {
316 logger.finest("cb: " + cb);
317 Iterator it = inboxPageUrls.iterator();
318
319 while (it.hasNext()) {
320 logger.finest("HiIDs: " + (Integer) it.next());
321 }
322 }
323
324 //Construct the summary information (if enabled)...
325 if (CREATE_SUMMARY_INFO) {
326 summaryInfo = new AnotherMailDropSummary();
327 summaryInfo.setUsername(username);
328 summaryInfo.setNumberOfMessages(numMessages);
329 }
330
331 //We must have at least one mailbox page (or else something has gone horribly wrong!)
332 if (inboxPageUrls.size() > 0) {
333 authenticated = true;
334
335 return LOGIN_OK;
336 } else {
337 return PAGE_STRUCTURE_CHANGED;
338 }
339 }
340
341 /**
342 *Build the another.com login service post parameters
343 */
344 private String createPostRequest(String username, String password) {
345 StringBuffer line = new StringBuffer();
346 line.append("?uname=");
347 line.append(username);
348 line.append("&pword=");
349 line.append(password);
350 line.append("&folder=INBOX");
351 line.append("&uclass=funmail&o=funmail\n");
352 return line.toString();
353 }
354
355 /**
356 * Logout from Another.com's service
357 */
358 private void logout() throws MailSessionException {
359 boolean loggedOut = false;
360 BufferedReader rd = null;
361
362 try {
363 URL url = new URL(logoutURL);
364 logger.fine("url: " + url);
365 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
366 conn.setRequestMethod("GET");
367 conn.setRequestProperty("User-Agent", UserAgent);
368 conn.setRequestProperty("Conten-type", "application/x-www-form-urlencoded");
369 conn.setRequestProperty("Accept", "*/*");
370 conn.setRequestProperty("Allowed", "GET HEAD PUT");
371 conn.setInstanceFollowRedirects(false);
372 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
373 hrh.connect();
374 conn = hrh.getConnection();
375 cj.addAll(hrh.getCookieJar());
376
377 rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
378 String line;
379
380 while ((line = rd.readLine()) != null) {
381 logger.fine("url: " + url);
382
383 if (line.indexOf("another.com: goodbye - come back soon!") > -1) {
384 loggedOut = true;
385 }
386 }
387 } catch (Exception e) {
388 logger.log(Level.SEVERE, "should not happen", e);
389 throw new MailSessionException("Logout failed : " + e.getMessage());
390 } finally {
391 try {
392 if (rd != null) {
393 rd.close();
394 }
395 } catch (IOException ie) {
396 logger.log(Level.SEVERE, "should not happen", ie);
397 }
398 }
399
400 if (loggedOut) {
401 logger.info("Logged out successfully");
402 } else {
403 logger.info("Log out failed");
404 }
405 }
406
407 /**
408 * Return the number of messages in the inbox (read and unread - we can't tell the difference!)
409 * In order to facilitate simple message retrieval, we'll take this opportunity to read all the
410 * message urls and hashcodes (used to delete or move a message) into our list.
411 * As the inbox will be spread across multiple pages, we'll have to do this once for each page.
412 * We already collected the page urls in the login() method :)
413 */
414
415 //public int numMessages() throws MailSessionException{
416 public int numMessages() {
417 //This may be called repeatedly - so we will do the processing only once...
418 if (msgHashCodes == null) {
419 try {
420 msgHashCodes = new LinkedList();
421 Iterator it = inboxPageUrls.iterator();
422
423 while (it.hasNext()) {
424 readMessageInfoForPage(((Integer) it.next()).intValue());
425 }
426
427 for (int i = 0; i < msgHashCodes.size(); i++) {
428 MailIdentifier mi = (MailIdentifier) msgHashCodes.get(i);
429 logger.fine(mi.mailID + " : " + mi.hashId + "=" + mi.hashCode);
430 }
431 } catch (MailSessionException me) {
432 //Update the summary information
433 if (CREATE_SUMMARY_INFO) {
434 summaryInfo.addGeneralError(me.getMessage());
435 }
436
437 //Pass this on to the PopConnector to allow it to return error to client
438 //throw me;
439 }
440 }
441
442 //return the total message count, which we calculated in the login() method
443 return numMessages;
444 }
445
446 /**
447 * Retrieve all the message urls and hashcodes for this particular mailbox page.
448 */
449 private void readMessageInfoForPage(int hiID) throws MailSessionException {
450 if (!authenticated) {
451 return;
452 }
453 BufferedReader br = null;
454
455 try {
456 URL url = new URL(inboxUrl + "&hi=" + Integer.toString(hiID));
457 logger.fine("url: " + url);
458 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
459 conn.setRequestProperty("User-Agent", UserAgent);
460 conn.setRequestProperty("Conten-type", "application/x-www-form-urlencoded");
461 conn.setRequestMethod("GET");
462 conn.setRequestProperty("Allowed", "GET HEAD PUT");
463 conn.setInstanceFollowRedirects(false);
464 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
465 hrh.setCookieJar(cj);
466 hrh.connect();
467 cj.addAll(hrh.getCookieJar());
468 br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
469 String line;
470
471 Pattern messagePattern = Pattern.compile(
472 "<A HREF=\"/servlet/viewMessage\\?cb=\\d+\\&action=msg&id=(\\d+)\\&folder=INBOX\">");
473 Matcher messageMatcher = null;
474
475 Pattern hashCodePattern = Pattern.compile(
476 "<INPUT TYPE=\"HIDDEN\" NAME=\"MessageHash(\\d+)\" VALUE=\"(\\S+)\">");
477 Matcher hashCodeMatcher = null;
478
479 MailIdentifier mi = null;
480
481 while ((line = br.readLine()) != null) {
482 logger.finest("html: " + line);
483 messageMatcher = messagePattern.matcher(line);
484
485 if (messageMatcher.find()) {
486 int msgId = Integer.parseInt(messageMatcher.group(1));
487 logger.fine("Found Message ID: " + msgId);
488 mi = new MailIdentifier();
489 mi.mailID = msgId;
490 }
491
492 //They must come in pairs - just not on the same line, so we are guaranteed to get this just after the mailId
493 hashCodeMatcher = hashCodePattern.matcher(line);
494
495 if (hashCodeMatcher.find()) {
496 mi.hashCode = hashCodeMatcher.group(2);
497 mi.hashId = hashCodeMatcher.group(1);
498 logger.fine("Found HashCode" + mi.hashId + "=" + mi.hashCode);
499 //Now add this into our list, indexed by mid
500 msgHashCodes.add(mi.mailID, mi);
501 }
502 }
503 } catch (Exception e) {
504 logger.log(Level.SEVERE, "should not happen", e);
505 throw new MailSessionException("readMessageInfoForPage(" + hiID + ") failed : " + e.getMessage());
506 } finally {
507 try {
508 if (br != null) {
509 br.close();
510 }
511 } catch (IOException ie) {
512 logger.log(Level.SEVERE, "should not happen", ie);
513 }
514 }
515 }
516
517 /**
518 * Retrieve and output the message.
519 * If the option is enabled, move the message into the READ folder.
520 * This is called by the RETR command.
521 */
522
523 //public void outputMessage(int num, PrintWriter pw) throws MailSessionException{
524 public void outputMessage(int num, PrintWriter pw) {
525 try {
526 retrieveMessage(num, pw, -1);
527
528 //Add the summary info....
529 if (CREATE_SUMMARY_INFO) {
530 summaryInfo.retrSuccess(num);
531 }
532
533 if (FLAG_MSG_AS_READ) {
534 //Add this message to our list of read messages....
535 MailIdentifier mi = (MailIdentifier) msgHashCodes.get(num - 1);
536
537 if (readMessages == null) {
538 readMessages = new LinkedList();
539 }
540 readMessages.add(mi); //this adds it to the move-to-read list
541 }
542 } catch (MailSessionException me) {
543 //Update the summary information
544 if (CREATE_SUMMARY_INFO) {
545 summaryInfo.retrFailure(num, me.getMessage());
546 }
547
548 //Pass this on to the PopConnector to allow it to return error to client
549 //throw me;
550 }
551 }
552
553 /**
554 * Retrieve the email header and numLines of the email body.
555 * The message will not be marked as read.
556 * This is called by the TOP command.
557 */
558
559 //public void outputLinesMessage(int num, PrintWriter pw, int numLines) throws MailSessionException
560 public void outputLinesMessage(int num, PrintWriter pw, int numLines) {
561 try {
562 retrieveMessage(num, pw, numLines);
563
564 if (CREATE_SUMMARY_INFO) {
565 summaryInfo.topSuccess(num);
566 }
567 } catch (MailSessionException me) {
568 //Update the summary information
569 if (CREATE_SUMMARY_INFO) {
570 summaryInfo.topFailure(num, me.getMessage());
571 }
572
573 //Pass this on to the PopConnector to allow it to return error to client
574 //throw me;
575 }
576 }
577
578 /**
579 * Retrieve the msg specified by the supplied msg number (indexed from 1!)
580 * This will use other methods to verify if the mail has attachements, retrieve the body text (always HTML)
581 * and attachements.
582 */
583 private void retrieveMessage(int num, PrintWriter pw, int numLines)
584 throws MailSessionException {
585 logger.fine("Retrieve Email #:" + num + ((numLines != -1) ? (" numlines=" + numLines) : " full"));
586
587 if (msgHashCodes == null) {
588 numMessages();
589 }
590
591 if (num > numMessages) {
592 logger.severe("outputMessage() called with invalid message number " + num + " number of messages = "
593 + numMessages);
594 return;
595 }
596
597 //First parse the header and message body...
598 MailMessage message = parseMessageHeaderAndBody(num, numLines);
599
600 //Add the summary info....
601 if (CREATE_SUMMARY_INFO) {
602 summaryInfo.setMsgHeader(num, message.header);
603 }
604
605 //now check for any attachments...if this is a TOP command we won't bother with the attachments...
606 if (numLines == -1) {
607 message.attachments = checkForAttachments(num);
608 }
609
610 //Output the message...
611 outputMessageHeader(message, pw);
612 outputMessageBody(message, pw);
613
614 if ((message.attachments != null) && (message.attachments.size() > 0)) {
615 Iterator it = message.attachments.iterator();
616
617 while (it.hasNext()) {
618 AttachmentIdentifier ai = (AttachmentIdentifier) it.next();
619 logger.fine("Handling attachment (" + ai.partId + "," + ai.filename + ")");
620 outputAttachment(message, ai, pw);
621 }
622
623 //Now we must output the closing boundary...
624 pw.print("--" + message.boundary + "--");
625 pw.print("\r\n");
626 }
627 pw.flush();
628 }
629
630 /**
631 * Determine the attachment info for any attached files
632 * Does this by reading the message and searching for the download href
633 */
634 private LinkedList checkForAttachments(int num) throws MailSessionException {
635 logger.fine("checkForAttachments: " + num);
636 BufferedReader br = null;
637
638 try {
639 // note no printInBaby option
640 URL url = new URL(msgReadBaseUrl + "?cb=" + cb + "&action=msg&folder=INBOX&id=" + (num - 1));
641 logger.fine("Read URL:\n" + url);
642 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
643 conn.setRequestProperty("User-Agent", UserAgent);
644 conn.setRequestMethod("GET");
645 conn.setRequestProperty("Allowed", "GET HEAD PUT");
646 conn.setInstanceFollowRedirects(false);
647 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
648 hrh.setCookieJar(cj);
649 hrh.connect();
650 cj.addAll(hrh.getCookieJar());
651 br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
652 Pattern attachPattern = Pattern.compile(
653 "<A HREF=\"/servlet/downloadPart/(\\S+)\\?partID=(\\S+)\" TARGET=\"another_download\" TITLE=\"download file to your computer\">");
654 Matcher attachMatcher = null;
655 String line = null;
656 LinkedList attachmentList = new LinkedList();
657
658 while ((line = br.readLine()) != null) {
659 logger.finest("html: " + line);
660 attachMatcher = attachPattern.matcher(line);
661
662 if (attachMatcher.find()) {
663 AttachmentIdentifier at = new AttachmentIdentifier();
664 at.partId = attachMatcher.group(2);
665 at.filename = attachMatcher.group(1);
666 logger.fine("Found Attachment: partId=" + at.partId + ", filename=" + at.filename);
667 attachmentList.add(at);
668 }
669 }
670 return attachmentList;
671 } catch (Exception e) {
672 logger.log(Level.SEVERE, "should not happen", e);
673 throw new MailSessionException("checkForAttachements(" + num + ") failed: " + e.getMessage());
674 } finally {
675 try {
676 if (br != null) {
677 br.close();
678 }
679 } catch (IOException ie) {
680 logger.log(Level.SEVERE, "should not happen", ie);
681 }
682 }
683 }
684
685 /**
686 * Retrieve the msg specified by the supplied msg number (indexed from 1!)
687 * Strip the HTML tags and parse the message header. Output the message body to
688 * the supplied PrintWriter
689 * if numLines != -1 then limit the number of body lines to be this value (used for the TOP command)
690 * Assumptions: the msg will be surrounded by 'pre' and '/pre' tags.
691 */
692 private MailMessage parseMessageHeaderAndBody(int num, int numLines)
693 throws MailSessionException {
694 logger.fine("Retrieve Email Body #:" + num + ((numLines != -1) ? (" numlines=" + numLines) : " full"));
695 BufferedReader br = null;
696
697 try { //msgReadBaseUrl
698 URL url = new URL(msgReadBaseUrl + "?cb=" + cb + "&action=msg&folder=INBOX&id=" + (num - 1)
699 + "&printable=printInBaby");
700 logger.fine("url: " + url);
701 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
702 conn.setRequestProperty("User-Agent", UserAgent);
703 conn.setRequestMethod("GET");
704 conn.setRequestProperty("Allowed", "GET HEAD PUT");
705 conn.setInstanceFollowRedirects(false);
706 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
707 hrh.setCookieJar(cj);
708 hrh.connect();
709 cj.addAll(hrh.getCookieJar());
710 br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
711 String line = null;
712
713 //Strip all html tags from the text
714 boolean insideTag = false;
715 boolean waitingForStartOfBody = false;
716 boolean insideBody = false;
717
718 //Create our structure to store the parsed message content...
719 MailMessage message = new MailMessage();
720 message.header = new HeaderInfo();
721 LinkedList bodyLines = new LinkedList();
722
723 //The subject is the first non-whitespace line of the header...
724 String nextLine = "subject";
725 int bodyLine = 0;
726
727 while ((line = br.readLine()) != null) {
728 logger.finest("html: " + line);
729 //Strip all html tags...
730 StringBuffer lineBuff = new StringBuffer(line.length());
731
732 if ((line.indexOf("<title>") > -1) || (line.indexOf("<TITLE>") > -1)) {
733 continue; //throw away the title!
734 }
735
736 //If we have already processed the header and are waiting for the start of the body...
737 if (waitingForStartOfBody) {
738 //The body starts at the firsr <p> tag!
739 if ((line.indexOf("<p>") > -1) || (line.indexOf("<P>") > -1)) {
740 insideBody = true;
741 waitingForStartOfBody = false;
742 //Output a HTML header info so the body can be interpretted as HTML...
743 bodyLines.add("<html><body><p>");
744 bodyLines.add("\r\n");
745 }
746 } else {
747 //If we are looking for the header fields, we wil strip the HTML tags and convert to text...
748 if (!insideBody) {
749 //Inside header...
750 for (int c = 0; c < line.length(); c++) {
751 char chr = line.charAt(c);
752
753 if (chr == '<') {
754 insideTag = true;
755 }
756
757 if (!insideTag) {
758 lineBuff.append(chr);
759 }
760
761 if (chr == '>') {
762 insideTag = false;
763 }
764 }
765
766 //convert any html encodings that might be present in the line...
767 String line2 = lineBuff.toString();
768
769 if (line2.length() > 0) {
770 line2 = line2.replaceAll("\\&", "\\&");
771 line2 = line2.replaceAll("\\<", "<");
772 line2 = line2.replaceAll("\\>", ">");
773 line2 = line2.replaceAll("\\"", "\"");
774 }
775
776 //Process the header...
777 //Throw away blank lines (or this with only whitespace characters)....
778 Pattern whiteSpaceLine = Pattern.compile("^\\s*\\z");
779
780 if (whiteSpaceLine.matcher(line2).find()) {
781 continue; //line only contains white space!
782 }
783
784 //We are guaranteed here a non-whitespace line, which is in the header...
785 //We must replace any '[' with '<' and ']' with '>' but not if we are in the subject field....
786 if ((nextLine != null) && !nextLine.equals("subject")) {
787 line2 = line2.replace('[', '<');
788 line2 = line2.replace(']', '>');
789 }
790
791 if (nextLine != null) {
792 //We are expecting some input...
793 if (nextLine.equals("subject")) {
794 message.header.subject = line2;
795 nextLine = null;
796 } else if (nextLine.equals("to")) {
797 message.header.to = line2;
798 nextLine = null;
799 } else if (nextLine.equals("from")) {
800 message.header.from = line2;
801 nextLine = null;
802 } else if (nextLine.equals("date")) {
803 message.header.sentDate = line2;
804 nextLine = null;
805
806 //The Date field is the last item in the header
807 //So we are waiting for the start of the body, this will come at the next <p> tag
808 waitingForStartOfBody = true;
809 }
810 } else {
811 //We do not know what the next line represents yet....
812 if (line2.indexOf("To:") > -1) {
813 //The next line is guaranteed to be the To: fields....
814 nextLine = "to";
815 } else if (line2.indexOf("From:") > -1) {
816 //The next line is guaranteed to be the From: fields....
817 nextLine = "from";
818 } else if (line2.indexOf("Date:") > -1) {
819 //The next line is guaranteed to be the Date: fields....
820 nextLine = "date";
821 }
822 }
823 } else { //end header processing
824 //inside the body (we have already processed the header) then just echo the line
825 bodyLines.add(line);
826 bodyLines.add("\r\n"); //just incase the system we are running on has a different line terminator
827
828 if (numLines > -1) { //Are we executing a TOP command?
829
830 if (++bodyLine > numLines) { //it has to be 1 more as we have counted the blank line
831 break; //read enough of the body (for TOP commands)
832 }
833 }
834 }
835 }
836
837 //end waitingForBody
838 }
839
840 //end while
841 String[] tmpArray = new String[0];
842 message.bodyLines = (String[]) bodyLines.toArray(tmpArray);
843 return message;
844 } catch (Exception e) {
845 logger.log(Level.SEVERE, "should not happen", e);
846 throw new MailSessionException("parseMessageHeaderAndBody(" + num + "," + numLines + ") failed : "
847 + e.getMessage());
848 } finally {
849 try {
850 if (br != null) {
851 br.close();
852 }
853 } catch (IOException ie) {
854 logger.log(Level.SEVERE, "should not happen", ie);
855 }
856 }
857 }
858
859 /**
860 * Output the email header
861 */
862 private void outputMessageHeader(MailMessage message, PrintWriter pw) {
863 pw.print("Subject: " + message.header.subject);
864 pw.print("\r\n");
865 pw.print("To: " + message.header.to);
866 pw.print("\r\n");
867 pw.print("From: " + message.header.from);
868 pw.print("\r\n");
869 pw.print("Date: " + message.header.sentDate);
870 pw.print("\r\n");
871
872 if ((message.attachments == null) || (message.attachments.size() == 0)) {
873 //We'll need to add our header field to tell the email client that it is html body...
874 pw.print("Content-Type: text/html; charset=us-ascii");
875 pw.print("\r\n");
876 pw.print("\r\n"); //ouput the blank header-body separator line
877 } else {
878 //this is a multipart message...
879 pw.print("MIME-Version: 1.0");
880 pw.print("\r\n");
881 message.boundary = createUniqueBoundaryValue(message);
882 pw.print("Content-Type: multipart/mixed; boundary=\"" + message.boundary + "\"");
883 pw.print("\r\n");
884 pw.print("\r\n");
885 pw.print("This is a multi-part message in MIME format.");
886 pw.print("\r\n");
887 pw.print("--" + message.boundary);
888 pw.print("\r\n");
889 pw.print("Content-Type: text/html; charset=us-ascii");
890 pw.print("\r\n");
891 pw.print("Content-Transfer-Encoding: 7bit");
892 pw.print("\r\n");
893 pw.print("\r\n"); //ouput the blank header-body separator line
894 }
895 }
896
897 /**
898 * Create a unique boundary string
899 */
900 private String createUniqueBoundaryValue(MailMessage message) {
901 StringBuffer sBuffer = new StringBuffer();
902 sBuffer.append("----=_Part_").append(message.partCount++).append("_").append(sBuffer.hashCode()).append('.')
903 .append(System.currentTimeMillis());
904 message.boundary = sBuffer.toString();
905 return message.boundary;
906 }
907
908 /**
909 * Output the email body
910 */
911 private void outputMessageBody(MailMessage message, PrintWriter pw) {
912 for (int i = 0; i < message.bodyLines.length; i++) {
913 pw.print(message.bodyLines[i]);
914 }
915 }
916
917 /**
918 * Output an attachment
919 */
920 private void outputAttachment(MailMessage message, AttachmentIdentifier attachment, PrintWriter pw)
921 throws MailSessionException {
922 logger.fine("outputAttachment : " + attachment.filename);
923
924
925 //Retrieve the attachment...
926 InputStream is = null;
927
928 try {
929 //read it and base64 encode it
930 URL url = new URL(baseUrl + "servlet/downloadPart/" + attachment.filename + "?partID=" + attachment.partId);
931
932 logger.fine("Attachment URL:\n" + url);
933 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
934 conn.setRequestProperty("User-Agent", UserAgent);
935 conn.setRequestMethod("GET");
936 conn.setRequestProperty("Allowed", "GET HEAD PUT");
937 conn.setInstanceFollowRedirects(false);
938 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
939 hrh.setCookieJar(cj);
940 hrh.connect();
941 cj.addAll(hrh.getCookieJar());
942 String contentType = conn.getContentType();
943 logger.fine("Content-type:" + contentType);
944 is = conn.getInputStream();
945 int size = conn.getContentLength();
946 ByteArrayOutputStream bos = new ByteArrayOutputStream(size);
947
948 int numRead = -1;
949 byte[] bytes = new byte[128];
950
951 while ((numRead = is.read(bytes)) > -1) {
952 if (numRead > 0) {
953 bos.write(bytes, 0, numRead);
954 }
955 }
956
957 logger.fine("Attachment size: " + bos.toByteArray().length);
958 //We have the raw attachment bytes. Output the header with the correct content type....
959 pw.print("--" + message.boundary);
960 pw.print("\r\n");
961 pw.print("Content-Type: " + contentType + "; name=\"" + attachment.filename + "\"");
962 pw.print("\r\n");
963 pw.print("Content-Transfer-Encoding: base64");
964 pw.print("\r\n");
965 pw.print("Content-Disposition: attachment; filename=\"" + attachment.filename + "\"");
966 pw.print("\r\n");
967 pw.print("\r\n"); //ouput the blank header-body separator line
968
969 //Encode the bytes and output...
970 pw.print(Base64.encodeBytes(bos.toByteArray(), true));
971 pw.print("\r\n");
972 pw.flush();
973 } catch (IOException ie) {
974 throw new MailSessionException("outputAttachment(" + message + "," + attachment.filename + ") failed : "
975 + ie.getMessage());
976 } finally {
977 try {
978 if (is != null) {
979 is.close();
980 }
981 } catch (IOException ie) {
982 logger.log(Level.SEVERE, "should not happen", ie);
983 }
984 }
985 }
986
987 /**
988 * Return the size of the specified msg (indexed from 1!)
989 * This is a bit of a frig - we can't tell how big the message is without reading it.
990 * So we won't actually bother! We'll make up a number and hope the client doesn't mind!
991 * Note: We can't use the figure specified by another.com as that doesn't include attachments and
992 * isn't very accurate anyhow.
993 */
994 public int getSizeOfMessage(int num) {
995 logger.fine("getSizeOfMessage(" + num + ")");
996 return 1;
997 }
998
999 /**
1000 * Return the module name to the MrPostman controller
1001 */
1002 public String getModuleName() {
1003 return MODULE_INFO.getModuleID();
1004 }
1005
1006 /**
1007 * Return the list of email extensions supported by this module
1008 */
1009 public String[] getRecognizedExtensions() {
1010 String[] extensions = {"@another.com"};
1011 return extensions;
1012 }
1013
1014 /**
1015 * Delete the message with the supplied index number.
1016 */
1017
1018 //public void deleteMessage(int num) throws MailSessionException {
1019 public boolean deleteMessage(int num) {
1020 logger.fine("DEL Email #:" + num);
1021
1022 //We don't do anything about this!
1023 // Add the summary information for this command...
1024 if (CREATE_SUMMARY_INFO) {
1025 summaryInfo.deleSuccess(num);
1026 }
1027
1028 return true;
1029 }
1030
1031 /**
1032 * Handle the QUIT command.
1033 * We need to move all our read messages to our READ folder
1034 * and logout.
1035 * NOTE: We can't do this after each RETR as that would adjust all the
1036 * message ids!
1037 */
1038 public void quitSession() {
1039 logger.fine("quitSession()");
1040
1041 try {
1042 if (FLAG_MSG_AS_READ) {
1043 moveMessagesToRead();
1044 }
1045 } catch (MailSessionException me) {
1046 //Add this to our summary info...
1047 if (CREATE_SUMMARY_INFO) {
1048 summaryInfo.addGeneralError(me.getMessage());
1049 }
1050 }
1051
1052 //Logout from another.com
1053 try {
1054 logout();
1055 } catch (MailSessionException me) {
1056 //Add this to our summary info...
1057 if (CREATE_SUMMARY_INFO) {
1058 summaryInfo.addGeneralError(me.getMessage());
1059 }
1060 }
1061
1062 //Now output all our summary information...
1063 if (CREATE_SUMMARY_INFO) {
1064 logger.fine(summaryInfo.toString());
1065 }
1066 }
1067
1068 /**
1069 * Move all the messages that we have read into the 'READ' folder
1070 */
1071 private void moveMessagesToRead() throws MailSessionException {
1072 BufferedReader br = null;
1073
1074 try {
1075 //We may need to flag our messages as read -we'll do this by moving them into the READ folder
1076 if (readMessages != null) {
1077 Iterator it = readMessages.iterator();
1078
1079 while (it.hasNext()) {
1080 MailIdentifier mi = (MailIdentifier) it.next();
1081
1082 logger.fine("Flag message as read: id=" + mi.mailID + " hashId=" + mi.hashId + " hashCode="
1083 + mi.hashCode);
1084
1085 URL url = new URL(moveToReadUrl + "&cb=" + cb + "&remove=" + mi.hashId + "&MessageHash" + mi.hashId
1086 + "=" + mi.hashCode);
1087 logger.fine("url: " + url);
1088 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
1089 conn.setRequestProperty("User-Agent", UserAgent);
1090 conn.setRequestProperty("Allowed", "GET HEAD PUT");
1091 conn.setInstanceFollowRedirects(false);
1092 HTTPRedirectHandler hrh = new HTTPRedirectHandler(conn);
1093 hrh.setCookieJar(cj);
1094 hrh.connect();
1095 cj.addAll(hrh.getCookieJar());
1096 br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
1097 String line = null;
1098
1099 br.close(); //we're not interested in the response data!
1100
1101 //Add this move to our summary info...
1102 if (CREATE_SUMMARY_INFO) {
1103 summaryInfo.moveMsgSuccess();
1104 }
1105 }
1106 }
1107 } catch (Exception e) {
1108 logger.log(Level.SEVERE, "should not happen", e);
1109 throw new MailSessionException("Exception whilst moving msg to READ folder: " + e.getMessage());
1110 } finally {
1111 try {
1112 if (br != null) {
1113 br.close();
1114 }
1115 } catch (IOException ie) {
1116 logger.log(Level.SEVERE, "should not happen", ie);
1117 }
1118 }
1119 }
1120
1121 /**
1122 * Return the unique id for this message. This is the another.com hashcode value
1123 */
1124 public String getUniqueMessageId(int num) throws MailSessionException {
1125 //We return the hashcode for this message...
1126 return ((MailIdentifier) msgHashCodes.get(num - 1)).hashCode;
1127 }
1128
1129 /**
1130 * Return the module info.
1131 */
1132 public ModuleInfo getModuleInfo() {
1133 return MODULE_INFO;
1134 }
1135
1136 /**
1137 * Set a module option with a value from the preferences or from the GUI
1138 * Currently understood options are listed in the MODULE_INFO specification above.
1139 */
1140 public void setOption(String optionName, String value) {
1141 logger.log(Level.FINEST, "setOption(" + optionName + "," + value + ")");
1142 }
1143
1144 /**
1145 * A debug main method
1146 */
1147 public static void main(String[] args) {
1148 if (args.length != 2) {
1149 System.err.println("Syntax AnotherMailSession username password");
1150 System.exit(1);
1151 }
1152
1153 AnotherMailSession ams = new AnotherMailSession();
1154
1155 if (ams.login(args[0], args[1], true) == LOGIN_OK) {
1156 try {
1157 int numMsgs = ams.numMessages();
1158 System.out.println("Number of messages: " + numMsgs);
1159
1160 if (numMsgs > 0) {
1161 ams.outputMessage(1, new PrintWriter(System.out));
1162 }
1163
1164 ams.logout();
1165 } catch (Exception e) {
1166 logger.log(Level.SEVERE, "should not happen", e);
1167 }
1168 }
1169 }
1170
1171/**
1172* Helper class used to store message info.
1173*/
1174 class MailIdentifier {
1175 int mailID = -1;
1176 String hashCode = null;
1177 String hashId = null;
1178
1179 public String toString() {
1180 return new String("MailID=" + mailID + ", hashId=" + hashId + ", hashCode=" + hashCode);
1181 }
1182 }
1183
1184/**
1185* Helper class to store attachement info.
1186*/
1187 class AttachmentIdentifier {
1188 String partId = null;
1189 String filename = null;
1190 }
1191
1192/**
1193* Helper class to store Message content
1194*/
1195 class MailMessage {
1196 public HeaderInfo header = null;
1197 public LinkedList attachments = null;
1198 public String[] bodyLines;
1199 public String boundary;
1200 public int partCount = 0;
1201 }
1202}