001: //The contents of this file are subject to the Mozilla Public License Version 1.1
002: //(the "License"); you may not use this file except in compliance with the
003: //License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
004: //
005: //Software distributed under the License is distributed on an "AS IS" basis,
006: //WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
007: //for the specific language governing rights and
008: //limitations under the License.
009: //
010: //The Original Code is "The Columba Project"
011: //
012: //The Initial Developers of the Original Code are Frederik Dietz and Timo Stich.
013: //Portions created by Frederik Dietz and Timo Stich are Copyright (C) 2003.
014: //
015: //All Rights Reserved.
016:
017: package org.columba.mail.pop3;
018:
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.text.MessageFormat;
022: import java.util.ArrayList;
023: import java.util.Collections;
024: import java.util.Hashtable;
025: import java.util.Iterator;
026: import java.util.List;
027: import java.util.Map;
028: import java.util.Observable;
029: import java.util.Observer;
030: import java.util.logging.Logger;
031:
032: import javax.net.ssl.SSLException;
033: import javax.swing.JOptionPane;
034:
035: import org.columba.api.command.IStatusObservable;
036: import org.columba.core.base.Blowfish;
037: import org.columba.core.command.CommandCancelledException;
038: import org.columba.core.command.StatusObservableImpl;
039: import org.columba.core.gui.base.MultiLineLabel;
040: import org.columba.core.gui.frame.FrameManager;
041: import org.columba.mail.config.IncomingItem;
042: import org.columba.mail.config.PopItem;
043: import org.columba.mail.gui.util.PasswordDialog;
044: import org.columba.mail.util.AuthenticationManager;
045: import org.columba.mail.util.AuthenticationSecurityComparator;
046: import org.columba.mail.util.MailResourceLoader;
047: import org.columba.ristretto.auth.AuthenticationException;
048: import org.columba.ristretto.auth.AuthenticationFactory;
049: import org.columba.ristretto.pop3.MessageNotOnServerException;
050: import org.columba.ristretto.pop3.POP3Exception;
051: import org.columba.ristretto.pop3.POP3Protocol;
052: import org.columba.ristretto.pop3.ScanListEntry;
053: import org.columba.ristretto.pop3.UidListEntry;
054:
055: /**
056: * First abstractionlayer of the POP3 Protocol. Its task is to manage the state
057: * of the server and handle the low level connection stuff. This means open the
058: * connection using SSL or not, login via the most secure or selected
059: * authentication method. It also provides the capabilites to convert an POP3
060: * uid to the index.
061: *
062: * @see POP3Server
063: *
064: * @author freddy, tstich
065: */
066: public class POP3Store implements Observer {
067:
068: /** JDK 1.4+ logging framework logger, used for logging. */
069: private static final Logger LOG = Logger
070: .getLogger("org.columba.mail.pop3");
071:
072: private POP3Protocol protocol;
073:
074: private PopItem popItem;
075:
076: private StatusObservableImpl observable;
077:
078: private Map uidMap;
079:
080: private List sizeList;
081:
082: private String[] capas;
083:
084: private int messageCount = -1;
085:
086: private boolean usingSSL = false;
087:
088: /**
089: * Constructor for POP3Store.
090: */
091: public POP3Store(PopItem popItem) {
092: super ();
093: this .popItem = popItem;
094:
095: popItem.getRoot().addObserver(this );
096:
097: protocol = new POP3Protocol(popItem.get("host"), popItem
098: .getInteger("port"));
099:
100: // add status information observable
101: observable = new StatusObservableImpl();
102: }
103:
104: public List getUIDList() throws Exception {
105: return new ArrayList(getUidMap().keySet());
106: }
107:
108: /**
109: * Returns the size of the message with the given index.
110: */
111: public int getSize(int index) throws IOException, POP3Exception,
112: CommandCancelledException {
113: try {
114: return ((Integer) getSizeList().get(index)).intValue();
115: } catch (IndexOutOfBoundsException e) {
116: throw new MessageNotOnServerException(new Integer(index));
117: } catch (NullPointerException e) {
118: throw new MessageNotOnServerException(new Integer(index));
119: }
120: }
121:
122: /**
123: * @return
124: */
125: private List getSizeList() throws IOException, POP3Exception,
126: CommandCancelledException {
127: if (sizeList == null) {
128: ensureTransaction();
129: ScanListEntry[] sizes = protocol.list();
130:
131: sizeList = new ArrayList(sizes.length + 1);
132: // since the indices on the pop server start with 1 we add
133: // a dummy null for the 0 element in the list
134: sizeList.add(null);
135:
136: for (int i = 0; i < sizes.length; i++) {
137: if (sizes[i].getIndex() > sizeList.size() - 1) {
138: // fill with nulls
139: for (int nextIndex = sizeList.size() - 1; nextIndex < sizes[i]
140: .getIndex(); nextIndex++) {
141: sizeList.add(null);
142: }
143: }
144:
145: // put size at the specified place
146: sizeList.set(sizes[i].getIndex(), new Integer(sizes[i]
147: .getSize()));
148: }
149: }
150:
151: return sizeList;
152: }
153:
154: /**
155: * Returns the number of messages on the server.
156: */
157: public int getMessageCount() throws IOException, POP3Exception,
158: CommandCancelledException {
159: if (messageCount == -1) {
160: ensureTransaction();
161: int[] stat = protocol.stat();
162: messageCount = stat[0];
163: }
164:
165: return messageCount;
166: }
167:
168: /**
169: * Deletes the message with the given UID from the server.
170: */
171: public boolean deleteMessage(Object uid)
172: throws CommandCancelledException, IOException,
173: POP3Exception {
174: ensureTransaction();
175: return protocol.dele(getIndex(uid));
176: }
177:
178: protected int getIndex(Object uid) throws IOException,
179: POP3Exception, CommandCancelledException {
180:
181: if (getUidMap().containsKey(uid)) {
182: return ((Integer) getUidMap().get(uid)).intValue();
183: } else {
184: throw new MessageNotOnServerException(uid);
185: }
186: }
187:
188: public InputStream fetchMessage(int index) throws IOException,
189: POP3Exception, CommandCancelledException {
190: ensureTransaction();
191: return protocol.retr(index, getSize(index));
192: }
193:
194: public void logout() throws IOException, POP3Exception {
195: if (protocol.getState() != POP3Protocol.NOT_CONNECTED) {
196: protocol.quit();
197: }
198: uidMap = null;
199: sizeList = null;
200: messageCount = -1;
201: }
202:
203: /**
204: * Returns whether a given POP3 command is supported by the server.
205: */
206: protected boolean isSupported(String command) throws IOException {
207: fetchCapas();
208: for (int i = 0; i < capas.length; i++) {
209: if (capas[i].startsWith(command)) {
210: return true;
211: }
212: }
213: return false;
214: }
215:
216: protected String getCapa(String command) throws IOException {
217: fetchCapas();
218: for (int i = 0; i < capas.length; i++) {
219: if (capas[i].startsWith(command)) {
220: return capas[i];
221: }
222: }
223: return null;
224: }
225:
226: private void fetchCapas() throws IOException {
227: if (capas == null) {
228: try {
229: capas = protocol.capa();
230: } catch (POP3Exception e) {
231: // CAPA not supported
232: capas = new String[] {};
233: }
234: }
235: }
236:
237: /**
238: * Try to login a user to a given pop3 server. While the login is not
239: * succeed, the connection to the server is opened, then a dialog box is
240: * coming up, then the user should fill in his password. The username and
241: * password is sended to the pop3 server. Then we recive a answer. Is the
242: * answer is false, we try to logout from the server and closing the
243: * connection. The we begin again with open connection, showing dialog and
244: * so on.
245: *
246: * @param worker
247: * used for cancel button
248: * @throws Exception
249: *
250: * Bug number 619290 fixed.
251: */
252: protected void login() throws IOException, POP3Exception,
253: CommandCancelledException {
254: PasswordDialog dialog;
255: boolean login = false;
256:
257: char[] password = new char[0];
258:
259: boolean save = false;
260:
261: int loginMethod = getLoginMethod();
262:
263: while (!login) {
264: if ((password = Blowfish.decrypt(popItem.getRoot()
265: .getAttribute("password", ""))).length == 0) {
266: dialog = new PasswordDialog();
267:
268: dialog.showDialog(MessageFormat.format(
269: MailResourceLoader.getString("dialog",
270: "password", "enter_password"),
271: new Object[] { popItem.get("user"),
272: popItem.get("host") }), popItem
273: .get("password"), popItem
274: .getBoolean("save_password"));
275:
276: if (dialog.success()) {
277: // ok pressed
278: password = dialog.getPassword();
279: save = dialog.getSave();
280: } else {
281: throw new CommandCancelledException();
282: }
283: } else {
284: save = popItem.getBoolean("save_password");
285: }
286:
287: try {
288: if (loginMethod == AuthenticationManager.USER) {
289: protocol.userPass(popItem.get("user"), password);
290: login = true;
291: } else if (loginMethod == AuthenticationManager.APOP) {
292: try {
293: protocol.apop(popItem.get("user"), password);
294: } catch (POP3Exception e1) {
295: // some server have a bogus apop
296: // try user/pass to check if the password is
297: // correct
298: protocol
299: .userPass(popItem.get("user"), password);
300:
301: LOG
302: .warning(popItem.get("host")
303: + " : bogus APOP implementation -> falling back to USER/PASS.");
304:
305: // user/pass worked -> this is indeed
306: // a bogus server.
307: }
308: login = true;
309: } else {
310: try {
311: // AUTH
312: protocol.auth(AuthenticationManager
313: .getSaslName(loginMethod), popItem
314: .get("user"), password);
315: login = true;
316: } catch (AuthenticationException e) {
317: // If the cause is a IMAPExcpetion then only password
318: // wrong
319: // else bogus authentication mechanism
320: if (e.getCause() instanceof POP3Exception)
321: throw (POP3Exception) e.getCause();
322:
323: // Some error in the client/server communication
324: // --> fall back to default login process
325: int result = JOptionPane
326: .showConfirmDialog(
327: FrameManager.getInstance()
328: .getActiveFrame(),
329: new MultiLineLabel(
330: e.getMessage()
331: + "\n"
332: + MailResourceLoader
333: .getString(
334: "dialog",
335: "error",
336: "authentication_fallback_to_default")),
337: MailResourceLoader
338: .getString("dialog",
339: "error",
340: "authentication_process_error"),
341: JOptionPane.OK_CANCEL_OPTION);
342: if (result == JOptionPane.OK_OPTION) {
343: loginMethod = AuthenticationManager.USER;
344: popItem.setString("login_method", Integer
345: .toString(loginMethod));
346: } else {
347: throw new CommandCancelledException();
348: }
349: }
350: }
351: } catch (POP3Exception e) {
352: JOptionPane.showMessageDialog(null, e.getMessage(),
353: "Authorization failed!",
354: JOptionPane.ERROR_MESSAGE);
355:
356: popItem.setString("password", "");
357: }
358:
359: LOG.info("login=" + login);
360: }
361:
362: popItem.setBoolean("save_password", save);
363:
364: if (save) {
365: popItem.setString("password", Blowfish.encrypt(password));
366: }
367: }
368:
369: public List checkSupportedAuthenticationMethods()
370: throws CommandCancelledException, IOException {
371: ArrayList supportedMechanisms = new ArrayList();
372: // USER/PASS is always supported
373: supportedMechanisms
374: .add(new Integer(AuthenticationManager.USER));
375:
376: try {
377: ensureAuthorization();
378:
379: // APOP?
380: if (protocol.isApopSupported()) {
381: supportedMechanisms.add(new Integer(
382: AuthenticationManager.APOP));
383: }
384: } catch (POP3Exception e) {
385: // APOP not supported
386: }
387:
388: try {
389: String serverSaslMechansims = getCapa("SASL");
390:
391: // AUTH?
392: if (serverSaslMechansims != null) {
393: List authMechanisms = AuthenticationFactory
394: .getInstance().getSupportedMechanisms(
395: serverSaslMechansims);
396: Iterator it = authMechanisms.iterator();
397: while (it.hasNext()) {
398: supportedMechanisms.add(new Integer(
399: AuthenticationManager
400: .getSaslCode((String) it.next())));
401: }
402: }
403: } catch (IOException e) {
404: e.printStackTrace();
405: }
406:
407: return supportedMechanisms;
408: }
409:
410: /**
411: * Gets the selected Authentication method or else the most secure.
412: *
413: * @return the authentication method
414: */
415: private int getLoginMethod() throws CommandCancelledException,
416: IOException {
417: String loginMethod = popItem.get("login_method");
418: int result = 0;
419:
420: try {
421: result = Integer.parseInt(loginMethod);
422: } catch (NumberFormatException e) {
423: // Just use the default as fallback
424: }
425:
426: if (result == 0) {
427: List supported = checkSupportedAuthenticationMethods();
428:
429: if (usingSSL) {
430: // NOTE if SSL is possible we just need the plain login
431: // since SSL does the encryption for us.
432: result = ((Integer) supported.get(0)).intValue();
433: } else {
434: Collections.sort(supported,
435: new AuthenticationSecurityComparator());
436: result = ((Integer) supported.get(supported.size() - 1))
437: .intValue();
438: }
439:
440: }
441:
442: return result;
443: }
444:
445: public static void doPOPbeforeSMTP(PopItem popItem)
446: throws IOException, POP3Exception,
447: CommandCancelledException {
448: POP3Store store = new POP3Store(popItem);
449: store.ensureTransaction();
450: store.logout();
451: }
452:
453: protected void ensureTransaction() throws IOException,
454: POP3Exception, CommandCancelledException {
455: ensureAuthorization();
456: if (protocol.getState() < POP3Protocol.TRANSACTION) {
457: login();
458: }
459: }
460:
461: protected void ensureAuthorization() throws IOException,
462: POP3Exception, CommandCancelledException {
463: if (protocol.getState() < POP3Protocol.AUTHORIZATION) {
464: openConnection();
465: }
466: }
467:
468: /**
469: *
470: */
471: private void openConnection() throws IOException, POP3Exception,
472: CommandCancelledException {
473: int sslType = popItem.getIntegerWithDefault("ssl_type",
474: IncomingItem.TLS);
475: boolean sslEnabled = popItem.getBoolean("enable_ssl");
476:
477: // open a port to the server
478: if (sslEnabled && sslType == IncomingItem.IMAPS_POP3S) {
479: try {
480: protocol.openSSLPort();
481: usingSSL = true;
482: } catch (SSLException e) {
483: int result = showErrorDialog(MailResourceLoader
484: .getString("dialog", "error",
485: "ssl_handshake_error")
486: + ": "
487: + e.getLocalizedMessage()
488: + "\n"
489: + MailResourceLoader.getString("dialog",
490: "error", "ssl_turn_off"));
491:
492: if (result == JOptionPane.CANCEL_OPTION) {
493: throw new CommandCancelledException();
494: }
495:
496: // turn off SSL for the future
497: popItem.setBoolean("enable_ssl", false);
498: popItem.setInteger("port", POP3Protocol.DEFAULT_PORT);
499:
500: // reopen the port
501: protocol.openPort();
502: }
503: } else {
504: protocol.openPort();
505: }
506:
507: // shall we switch to SSL?
508: if (!usingSSL && sslEnabled && sslType == IncomingItem.TLS) {
509: // if CAPA was not support just give it a try...
510: if (isSupported("STLS") || capas.length == 0) {
511: try {
512: protocol.startTLS();
513:
514: usingSSL = true;
515: LOG.info("Switched to SSL");
516: } catch (IOException e) {
517: int result = showErrorDialog(MailResourceLoader
518: .getString("dialog", "error",
519: "ssl_handshake_error")
520: + ": "
521: + e.getLocalizedMessage()
522: + "\n"
523: + MailResourceLoader.getString("dialog",
524: "error", "ssl_turn_off"));
525:
526: if (result == JOptionPane.CANCEL_OPTION) {
527: throw new CommandCancelledException();
528: }
529:
530: // turn off SSL for the future
531: popItem.setBoolean("enable_ssl", false);
532:
533: // reopen the port
534: protocol.openPort();
535: } catch (POP3Exception e) {
536: int result = showErrorDialog(MailResourceLoader
537: .getString("dialog", "error",
538: "ssl_not_supported")
539: + "\n"
540: + MailResourceLoader.getString("dialog",
541: "error", "ssl_turn_off"));
542:
543: if (result == JOptionPane.CANCEL_OPTION) {
544: throw new CommandCancelledException();
545: }
546:
547: // turn off SSL for the future
548: popItem.setBoolean("enable_ssl", false);
549: }
550: } else {
551: // CAPAs say that SSL is not supported
552: int result = showErrorDialog(MailResourceLoader
553: .getString("dialog", "error",
554: "ssl_not_supported")
555: + "\n"
556: + MailResourceLoader.getString("dialog",
557: "error", "ssl_turn_off"));
558:
559: if (result == JOptionPane.CANCEL_OPTION) {
560: throw new CommandCancelledException();
561: }
562:
563: // turn off SSL for the future
564: popItem.setBoolean("enable_ssl", false);
565: }
566: }
567: }
568:
569: private int showErrorDialog(String message) {
570: int result = JOptionPane.showConfirmDialog(FrameManager
571: .getInstance().getActiveFrame(), message, "Warning",
572: JOptionPane.WARNING_MESSAGE,
573: JOptionPane.OK_CANCEL_OPTION);
574: return result;
575: }
576:
577: public IStatusObservable getObservable() {
578: return observable;
579: }
580:
581: /**
582: * @return Returns the uidMap.
583: */
584: private Map getUidMap() throws CommandCancelledException,
585: IOException, POP3Exception {
586: if (uidMap == null) {
587: if (getMessageCount() != 0) {
588: ensureTransaction();
589:
590: UidListEntry[] uidList = protocol.uidl();
591: uidMap = new Hashtable();
592:
593: for (int i = 0; i < uidList.length; i++) {
594: uidMap.put(uidList[i].getUid(), new Integer(
595: uidList[i].getIndex()));
596: }
597: } else {
598: uidMap = new Hashtable(0);
599: }
600: }
601: return uidMap;
602: }
603:
604: /**
605: * @throws IOException
606: *
607: */
608: public void dropConnection() throws IOException {
609: protocol.dropConnection();
610: }
611:
612: public void update(Observable o, Object arg) {
613: protocol = new POP3Protocol(popItem.get("host"), popItem
614: .getInteger("port"));
615: }
616:
617: }
|