001: /*
002: * ImapSession.java
003: *
004: * Copyright (C) 2000-2002 Peter Graves
005: * $Id: ImapSession.java,v 1.2 2002/09/25 13:58:11 piso Exp $
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: */
021:
022: package org.armedbear.j.mail;
023:
024: import java.io.IOException;
025: import java.io.InterruptedIOException;
026: import java.io.OutputStreamWriter;
027: import java.net.Socket;
028: import java.net.SocketException;
029: import org.armedbear.j.Debug;
030: import org.armedbear.j.Editor;
031: import org.armedbear.j.FastStringBuffer;
032: import org.armedbear.j.Log;
033: import org.armedbear.j.Netrc;
034: import org.armedbear.j.SocketConnection;
035: import org.armedbear.j.Utilities;
036:
037: public final class ImapSession {
038: private static final int DEFAULT_PORT = 143;
039:
040: // States.
041: private static final int DISCONNECTED = 0;
042: private static final int NONAUTHENTICATED = 1;
043: private static final int AUTHENTICATED = 2;
044: private static final int SELECTED = 3;
045:
046: // Responses.
047: public static final int OK = 0;
048: public static final int NO = 1;
049: public static final int BAD = 2;
050: public static final int PREAUTH = 3;
051: public static final int BYE = 4;
052:
053: public static final int UNKNOWN = -1;
054:
055: private final String host;
056: private final String user;
057: private final String password;
058: private final int port;
059:
060: private String tunnelHost;
061: private int tunnelPort = -1;
062: private ImapMailbox mailbox;
063: private int state;
064: private boolean echo;
065: private Socket socket;
066: private MailReader reader;
067: private OutputStreamWriter writer;
068: private String folderName;
069: private boolean readOnly;
070: private int messageCount;
071: private int recent;
072: private int uidValidity;
073: private int uidNext;
074: private String errorText;
075: private long lastErrorMillis;
076:
077: private ImapSession(ImapURL url, String user, String password) {
078: this .host = url.getHost();
079: this .folderName = url.getFolderName();
080: this .port = url.getPort();
081: this .user = user;
082: this .password = password;
083: }
084:
085: public final void setMailbox(ImapMailbox mb) {
086: if (mailbox != null)
087: Debug.bug();
088: mailbox = mb;
089: }
090:
091: public final boolean isReadOnly() {
092: return readOnly;
093: }
094:
095: public final String getHost() {
096: return host;
097: }
098:
099: public final int getPort() {
100: return port;
101: }
102:
103: public final String getUser() {
104: return user;
105: }
106:
107: public final String getFolderName() {
108: return folderName;
109: }
110:
111: public final int getMessageCount() {
112: return messageCount;
113: }
114:
115: public final int getRecent() {
116: return recent;
117: }
118:
119: public final int getUidNext() {
120: return uidNext;
121: }
122:
123: public final int getUidValidity() {
124: return uidValidity;
125: }
126:
127: public final String getErrorText() {
128: return errorText;
129: }
130:
131: public void setTunnel(String tunnel) {
132: if (tunnel != null) {
133: tunnel = tunnel.trim();
134: int colon = tunnel.indexOf(':');
135: if (colon > 0) {
136: tunnelHost = tunnel.substring(0, colon);
137: try {
138: tunnelPort = Integer.parseInt(tunnel.substring(
139: colon + 1).trim());
140: } catch (NumberFormatException e) {
141: Log.error(e);
142: tunnelHost = null;
143: tunnelPort = -1;
144: }
145: }
146: }
147: Log.debug("setTunnel host = |" + tunnelHost + "| port = "
148: + tunnelPort);
149: }
150:
151: public synchronized final long getLastErrorMillis() {
152: return lastErrorMillis;
153: }
154:
155: private synchronized final void setLastErrorMillis(long millis) {
156: Log.debug("setLastErrorMillis");
157: lastErrorMillis = millis;
158: }
159:
160: public static ImapSession getSession(ImapURL url) {
161: if (url.getHost() == null || url.getFolderName() == null)
162: return null;
163: String user = url.getUser();
164: if (user == null)
165: user = System.getProperty("user.name");
166: return getSession(url, user);
167: }
168:
169: public static ImapSession getSession(ImapURL url, String user) {
170: String password = Netrc.getPassword(url.getHost(), user);
171: if (password == null)
172: return null;
173: return new ImapSession(url, user, password);
174: }
175:
176: public static ImapSession getSession(ImapURL url, String user,
177: String password) {
178: return new ImapSession(url, user, password);
179: }
180:
181: public boolean verifyConnected() {
182: if (state != DISCONNECTED) {
183: // We send a NOOP command here both to verify that we really are
184: // connected and to give the server a chance to report changes to
185: // the mailbox.
186: if (writeTagged("noop")) {
187: if (getResponse() == OK)
188: return true;
189: }
190: }
191: return connect();
192: }
193:
194: public boolean verifySelected(String folderName) {
195: if (state == SELECTED && this .folderName.equals(folderName))
196: return true;
197: return reselect(folderName);
198: }
199:
200: private boolean connect() {
201: socket = null;
202: errorText = null;
203: final String h; // Host.
204: final int p; // Port.
205: if (tunnelHost != null && tunnelPort > 0) {
206: h = tunnelHost;
207: p = tunnelPort;
208: Log.debug("connect using tunnel h = " + h + " p = " + p);
209: } else {
210: h = host;
211: p = port;
212: }
213: SocketConnection sc = new SocketConnection(h, p, 30000, 200,
214: null);
215: Log.debug("connecting to " + h + " on port " + p);
216: socket = sc.connect();
217: if (socket == null) {
218: errorText = sc.getErrorText();
219: Log.error(errorText);
220: return false;
221: }
222: Log.debug("connected to " + host);
223: boolean succeeded = false;
224: boolean oldEcho = echo;
225: if (Editor.isDebugEnabled())
226: echo = true;
227: try {
228: reader = new MailReader(socket.getInputStream());
229: writer = new OutputStreamWriter(socket.getOutputStream(),
230: "iso-8859-1");
231: if (readLine() != null) {
232: writeTagged("login " + user + " " + password);
233: if (getResponse() == OK) {
234: state = AUTHENTICATED;
235: succeeded = true;
236: }
237: }
238: } catch (IOException e) {
239: Log.error(e);
240: } finally {
241: echo = oldEcho;
242: }
243: return succeeded;
244: }
245:
246: private static final String UIDVALIDITY = "* OK [UIDVALIDITY ";
247: private static final String UIDNEXT = "* OK [UIDNEXT ";
248:
249: public boolean reselect(String folderName) {
250: long start = System.currentTimeMillis();
251: boolean oldEcho = echo;
252: if (Editor.isDebugEnabled())
253: echo = true;
254: try {
255: if (state < AUTHENTICATED
256: || !writeTagged("select \"" + folderName + "\"")) {
257: connect();
258: if (state < AUTHENTICATED)
259: return false;
260: if (!writeTagged("select \"" + folderName + "\""))
261: return false;
262: }
263: while (true) {
264: String s = readLine();
265: if (s == null) {
266: Log
267: .error("ImapSession.reselect readLine returned null");
268: this .folderName = null;
269: messageCount = 0;
270: recent = 0;
271: return false;
272: }
273: final String upper = s.toUpperCase();
274: if (upper.startsWith("* NO ")) {
275: mailbox.setStatusText(s.substring(5).trim());
276: continue;
277: }
278: if (upper.startsWith("* ")) {
279: if (upper.endsWith(" EXISTS")) {
280: processUntaggedResponse(s);
281: continue;
282: }
283: if (upper.endsWith(" RECENT")) {
284: processUntaggedResponse(s);
285: continue;
286: }
287: }
288: if (upper.startsWith(UIDVALIDITY)) {
289: uidValidity = Utilities.parseInt(s
290: .substring(UIDVALIDITY.length()));
291: continue;
292: }
293: if (upper.startsWith(UIDNEXT)) {
294: uidNext = Utilities.parseInt(s.substring(UIDNEXT
295: .length()));
296: continue;
297: }
298: if (upper.startsWith(lastTag + " ")) {
299: // Tagged response.
300: if (upper.startsWith(lastTag + " OK ")) {
301: // Success!
302: state = SELECTED;
303: this .folderName = folderName;
304: readOnly = upper.indexOf("[READ-ONLY]") >= 0;
305: if (readOnly) {
306: Log.warn("reselect mailbox " + folderName
307: + " is read-only!");
308: setLastErrorMillis(System
309: .currentTimeMillis());
310: } else {
311: Log.debug("reselect mailbox " + folderName
312: + " is read-write");
313: }
314: return true;
315: } else {
316: // Error!
317: Log.error("SELECT " + folderName + " failed");
318: // Don't assume old folder is still selected.
319: state = AUTHENTICATED;
320: this .folderName = null;
321: messageCount = 0;
322: recent = 0;
323: return false;
324: }
325: }
326: }
327: } catch (Exception e) {
328: Log.error(e);
329: disconnect();
330: this .folderName = null;
331: messageCount = 0;
332: recent = 0;
333: return false;
334: } finally {
335: echo = oldEcho;
336: long elapsed = System.currentTimeMillis() - start;
337: Log.debug("ImapSession.reselect " + folderName + " "
338: + elapsed + " ms");
339: }
340: }
341:
342: public boolean close() {
343: if (state != SELECTED) {
344: Log.debug("already closed");
345: return true;
346: }
347: // State may be set to DISCONNECTED if writeTagged() or getResponse()
348: // fails.
349: if (writeTagged("close") && getResponse() == OK)
350: state = AUTHENTICATED;
351: folderName = null;
352: messageCount = 0;
353: recent = 0;
354: return true;
355: }
356:
357: public void logout() {
358: Log.debug("ImapSession.logout " + host);
359: if (state > DISCONNECTED) {
360: if (writeTagged("logout"))
361: getResponse();
362: if (state > DISCONNECTED)
363: disconnect();
364: }
365: }
366:
367: public synchronized void disconnect() {
368: if (socket != null) {
369: try {
370: socket.close();
371: } catch (IOException e) {
372: Log.error(e);
373: }
374: socket = null;
375: reader = null;
376: writer = null;
377: }
378: state = DISCONNECTED;
379: }
380:
381: public String readLine() {
382: if (reader == null)
383: return null;
384: try {
385: String s = reader.readLine();
386: if (s != null) {
387: if (echo)
388: Log.debug("<== " + s);
389: if (s.startsWith(lastTag + " "))
390: errorText = getTaggedResponseText(s);
391: }
392: return s;
393: } catch (InterruptedIOException e) {
394: // Timed out.
395: Log.error(e);
396: setLastErrorMillis(System.currentTimeMillis());
397: disconnect();
398: return null;
399: } catch (SocketException e) {
400: // This is what happens when the user cancels and we close the
401: // socket.
402: disconnect();
403: return null;
404: } catch (IOException e) {
405: Log.error(e);
406: setLastErrorMillis(System.currentTimeMillis());
407: disconnect();
408: return null;
409: }
410: }
411:
412: public void uidStore(int uid, String arg) {
413: FastStringBuffer sb = new FastStringBuffer("uid store ");
414: sb.append(uid);
415: sb.append(' ');
416: sb.append(arg);
417: writeTagged(sb.toString());
418: }
419:
420: public void uidStore(String messageSet, String arg) {
421: FastStringBuffer sb = new FastStringBuffer("uid store ");
422: sb.append(messageSet);
423: sb.append(' ');
424: sb.append(arg);
425: writeTagged(sb.toString());
426: }
427:
428: public boolean writeTagged(String s) {
429: if (writer == null)
430: return false;
431: // Store command.
432: int index = s.indexOf(' ');
433: final String lastCommand = index >= 0 ? s.substring(0, index)
434: : s;
435: // Prepend tag.
436: s = nextTag() + " " + s;
437: if (echo) {
438: if (lastCommand.equalsIgnoreCase("login")) {
439: index = s.lastIndexOf(' ');
440: if (index >= 0)
441: Log.debug("==> " + s.substring(0, index));
442: else
443: Log.debug("==> " + s);
444: } else
445: Log.debug("==> " + s);
446: }
447: try {
448: writer.write(s.concat("\r\n"));
449: writer.flush();
450: return true;
451: } catch (IOException e) {
452: Log.error(e);
453: disconnect();
454: return false;
455: }
456: }
457:
458: public int getResponse() {
459: while (true) {
460: String s = readLine();
461: if (s == null)
462: return BYE;
463: String upper = s.toUpperCase();
464: int index = upper.indexOf("[ALERT]");
465: if (index >= 0)
466: mailbox.setAlertText(s.substring(index + 7).trim());
467: if (upper.startsWith("* BYE ")) {
468: Log.debug("getResponse |" + s + "|");
469: disconnect();
470: return BYE;
471: }
472: if (upper.startsWith(lastTag + " ")) {
473: upper = upper.substring(lastTag.length() + 1);
474: if (upper.startsWith("OK "))
475: return OK;
476: if (upper.startsWith("NO ")) {
477: mailbox.setStatusText(s.substring(3).trim());
478: return NO;
479: }
480: if (upper.startsWith("BAD "))
481: return BAD;
482: // According to Section 7.1 of RFC 2060, PREAUTH and BYE are
483: // always untagged, so we should never encounter the following
484: // cases.
485: if (upper.startsWith("PREAUTH"))
486: return PREAUTH;
487: if (upper.startsWith("BYE")) {
488: disconnect();
489: return BYE;
490: }
491: return UNKNOWN;
492: }
493: processUntaggedResponse(s);
494: }
495: }
496:
497: private void processUntaggedResponse(String s) {
498: Log.debug("processUntaggedResponse |" + s + "|");
499: if (s.startsWith("* ")) {
500: final String upper = s.toUpperCase();
501: if (upper.endsWith(" EXISTS")) {
502: try {
503: messageCount = Integer.parseInt(upper.substring(2,
504: upper.length() - 7));
505: Log.debug("messageCount = " + messageCount);
506: } catch (NumberFormatException e) {
507: Log.error(e);
508: }
509: } else if (upper.endsWith(" RECENT")) {
510: try {
511: recent = Integer.parseInt(upper.substring(2, upper
512: .length() - 7));
513: Log.debug("recent = " + recent);
514: } catch (NumberFormatException e) {
515: Log.error(e);
516: }
517: } else if (upper.endsWith(" EXPUNGE")) {
518: try {
519: int messageNumber = Integer.parseInt(upper
520: .substring(2, upper.length() - 8));
521: if (messageCount > 0) {
522: --messageCount;
523: Log.debug("EXPUNGE messageCount = "
524: + messageCount);
525: } else
526: Log
527: .error("received untagged EXPUNGE response with messageCount = "
528: + messageCount);
529: mailbox.messageExpunged(messageNumber);
530: } catch (NumberFormatException e) {
531: Log.error(e);
532: }
533: }
534: }
535: }
536:
537: private static String getTaggedResponseText(String taggedResponse) {
538: // Skip tag.
539: int index = taggedResponse.indexOf(' ');
540: if (index < 0)
541: return null;
542: // Skip status string ("OK", "NO", etc.).
543: index = taggedResponse.indexOf(' ', index + 1);
544: if (index < 0)
545: return null;
546: // Return rest of line.
547: return taggedResponse.substring(index + 1);
548: }
549:
550: private int tagNumber;
551: private String lastTag;
552:
553: public final String lastTag() {
554: return lastTag;
555: }
556:
557: // Upper case.
558: private final String nextTag() {
559: return lastTag = "A".concat(String.valueOf(++tagNumber));
560: }
561:
562: public final void setEcho(boolean b) {
563: echo = b;
564: }
565:
566: protected void finalize() throws Throwable {
567: Log.debug("ImapSession.finalize " + host);
568: super.finalize();
569: }
570: }
|