001: // The contents of this file are subject to the Mozilla Public License Version
002: // 1.1
003: //(the "License"); you may not use this file except in compliance with the
004: //License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
005: //
006: //Software distributed under the License is distributed on an "AS IS" basis,
007: //WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
008: //for the specific language governing rights and
009: //limitations under the License.
010: //
011: //The Original Code is "The Columba Project"
012: //
013: //The Initial Developers of the Original Code are Frederik Dietz and Timo
014: // Stich.
015: //Portions created by Frederik Dietz and Timo Stich are Copyright (C) 2003.
016: //
017: //All Rights Reserved.
018:
019: package org.columba.mail.smtp;
020:
021: import java.io.IOException;
022: import java.net.InetAddress;
023: import java.text.MessageFormat;
024: import java.util.ArrayList;
025: import java.util.Collections;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Observable;
029: import java.util.Observer;
030:
031: import javax.swing.JOptionPane;
032:
033: import org.columba.api.command.IWorkerStatusController;
034: import org.columba.core.base.Blowfish;
035: import org.columba.core.command.CommandCancelledException;
036: import org.columba.core.command.ProgressObservedInputStream;
037: import org.columba.core.gui.base.MultiLineLabel;
038: import org.columba.core.gui.frame.FrameManager;
039: import org.columba.mail.composer.SendableMessage;
040: import org.columba.mail.config.AccountItem;
041: import org.columba.mail.config.Identity;
042: import org.columba.mail.config.ImapItem;
043: import org.columba.mail.config.OutgoingItem;
044: import org.columba.mail.config.PopItem;
045: import org.columba.mail.gui.util.PasswordDialog;
046: import org.columba.mail.pop3.POP3Store;
047: import org.columba.mail.util.AuthenticationManager;
048: import org.columba.mail.util.AuthenticationSecurityComparator;
049: import org.columba.mail.util.MailResourceLoader;
050: import org.columba.ristretto.auth.AuthenticationException;
051: import org.columba.ristretto.auth.AuthenticationFactory;
052: import org.columba.ristretto.message.Address;
053: import org.columba.ristretto.parser.ParserException;
054: import org.columba.ristretto.pop3.POP3Exception;
055: import org.columba.ristretto.smtp.SMTPException;
056: import org.columba.ristretto.smtp.SMTPProtocol;
057:
058: /**
059: *
060: * SMTPServer makes use of <class>SMTPProtocol </class> to add a higher
061: * abstraction layer for sending messages.
062: *
063: * It takes care of authentication all the details.
064: *
065: * To send a message just create a <class>SendableMessage </class> object and
066: * use <method>sendMessage </method>.
067: *
068: * @author fdietz, Timo Stich <tstich@users.sourceforge.net>
069: *
070: */
071: public class SMTPServer implements Observer {
072:
073: private String[] capas;
074:
075: protected SMTPProtocol protocol;
076:
077: protected OutgoingItem smtpItem;
078:
079: protected Identity identity;
080:
081: protected String fromAddress;
082:
083: private boolean usingSSL;
084:
085: private AccountItem accountItem;
086:
087: /**
088: * Constructor for SMTPServer.
089: */
090: public SMTPServer(AccountItem accountItem) {
091: super ();
092:
093: this .accountItem = accountItem;
094: identity = accountItem.getIdentity();
095:
096: // initialise protocol layer
097: smtpItem = accountItem.getSmtpItem();
098:
099: smtpItem.getRoot().addObserver(this );
100: protocol = new SMTPProtocol(smtpItem.get("host"), smtpItem
101: .getInteger("port"));
102: }
103:
104: private void ensureConnected() throws IOException, SMTPException,
105: CommandCancelledException {
106: if (protocol.getState() == SMTPProtocol.NOT_CONNECTED) {
107: // Start login procedure
108: protocol.openPort();
109:
110: initialize();
111:
112: doSSL();
113: }
114: }
115:
116: /**
117: * Open connection to SMTP server and login if needed.
118: *
119: * @return true if connection was successful, false otherwise
120: */
121: private void ensureAuthenticated() throws IOException,
122: SMTPException, CommandCancelledException {
123: String username;
124: char[] password = new char[0];
125: boolean savePassword;
126:
127: // Init Values
128: // user's email address
129: fromAddress = identity.getAddress().getMailAddress();
130:
131: usingSSL = smtpItem.getBoolean("enable_ssl");
132: int authMethod = getLoginMethod();
133:
134: boolean authenticated = (authMethod == AuthenticationManager.NONE);
135:
136: if (authMethod == AuthenticationManager.POP_BEFORE_SMTP) {
137: // no esmtp - use POP3-before-SMTP instead
138: try {
139: pop3Authentification();
140: } catch (POP3Exception e) {
141: throw new SMTPException(e);
142: }
143:
144: authenticated = true;
145: }
146:
147: ensureConnected();
148:
149: if (!authenticated) {
150: username = smtpItem.get("user");
151: password = Blowfish.decrypt(smtpItem.getRoot()
152: .getAttribute("password", ""));
153: savePassword = smtpItem.getBoolean("save_password");
154:
155: if (username.length() == 0) {
156: // there seems to be no username set in the smtp-options
157: // -> use username from pop3 or imap options
158: if (accountItem.isPopAccount()) {
159: PopItem pop3Item = accountItem.getPopItem();
160: username = pop3Item.get("user");
161: } else {
162: ImapItem imapItem = accountItem.getImapItem();
163: username = imapItem.get("user");
164: }
165: }
166:
167: PasswordDialog passDialog = new PasswordDialog();
168:
169: // ask password from user
170: if (password.length == 0) {
171: passDialog.showDialog(MessageFormat
172: .format(MailResourceLoader.getString("dialog",
173: "password", "enter_password"),
174: new Object[] { username,
175: smtpItem.get("host") }),
176: new String(password), savePassword);
177:
178: if (passDialog.success()) {
179: password = passDialog.getPassword();
180: savePassword = passDialog.getSave();
181: } else {
182: throw new CommandCancelledException();
183: }
184: }
185:
186: // try to authenticate
187: while (!authenticated) {
188: try {
189: try {
190: protocol.auth(AuthenticationManager
191: .getSaslName(authMethod), username,
192: password);
193: authenticated = true;
194: } catch (AuthenticationException e) {
195: // If the cause is a SMTPExcpetion then only password
196: // wrong
197: // else bogus authentication mechanism
198: if (e.getCause() instanceof SMTPException) {
199: int errorCode = ((SMTPException) e
200: .getCause()).getCode();
201:
202: // Authentication is not supported
203: if (errorCode == 504) {
204: //TODO: Add dialog to inform user that the smtp server
205: // does not support authentication
206: JOptionPane
207: .showMessageDialog(
208: FrameManager
209: .getInstance()
210: .getActiveFrame(),
211: new MultiLineLabel(
212: MailResourceLoader
213: .getString(
214: "dialog",
215: "error",
216: "authentication_not_supported")),
217: MailResourceLoader
218: .getString(
219: "dialog",
220: "error",
221: "authentication_process_error"),
222: JOptionPane.INFORMATION_MESSAGE);
223:
224: //Turn off authentication for the future
225: smtpItem
226: .setString(
227: "login_method",
228: Integer
229: .toString(AuthenticationManager.NONE));
230:
231: return;
232: }
233:
234: } else {
235: throw (SMTPException) e.getCause();
236: }
237:
238: // Some error in the client/server communication
239: // --> fall back to default login process
240: int result = JOptionPane
241: .showConfirmDialog(
242: FrameManager.getInstance()
243: .getActiveFrame(),
244: new MultiLineLabel(
245: e.getMessage()
246: + "\n"
247: + MailResourceLoader
248: .getString(
249: "dialog",
250: "error",
251: "authentication_fallback_to_default")),
252: MailResourceLoader
253: .getString("dialog",
254: "error",
255: "authentication_process_error"),
256: JOptionPane.OK_CANCEL_OPTION);
257:
258: if (result == JOptionPane.OK_OPTION) {
259: authMethod = AuthenticationManager.SASL_PLAIN;
260: smtpItem.setString("login_method", Integer
261: .toString(authMethod));
262: } else {
263: throw new CommandCancelledException();
264: }
265:
266: }
267: } catch (SMTPException e) {
268: passDialog.showDialog(MessageFormat.format(
269: MailResourceLoader.getString("dialog",
270: "password", "enter_password"),
271: new Object[] { username,
272: smtpItem.get("host") }),
273: new String(password), savePassword);
274:
275: if (!passDialog.success()) {
276: throw new CommandCancelledException();
277: } else {
278: password = passDialog.getPassword();
279: savePassword = passDialog.getSave();
280: }
281: }
282: }
283:
284: // authentication was successful
285: // -> save name/password
286: smtpItem.setString("user", username);
287: smtpItem.setBoolean("save_password", savePassword);
288: if (savePassword) {
289: smtpItem.setString("password", Blowfish
290: .encrypt(password));
291: }
292: }
293: }
294:
295: /**
296: * @param smtpItem
297: * @throws CommandCancelledException
298: * @throws IOException
299: * @throws SMTPException
300: */
301: private void doSSL() throws CommandCancelledException, IOException,
302: SMTPException {
303: if (smtpItem.getBoolean("enable_ssl")) {
304: if (isSupported("STARTTLS")) {
305: try {
306: protocol.startTLS();
307: usingSSL = true;
308: } catch (Exception e) {
309: Object[] options = new String[] {
310: MailResourceLoader.getString("", "global",
311: "ok").replaceAll("&", ""),
312: MailResourceLoader.getString("", "global",
313: "cancel").replaceAll("&", "") };
314:
315: int result = JOptionPane
316: .showOptionDialog(
317: FrameManager.getInstance()
318: .getActiveFrame(),
319: MailResourceLoader.getString(
320: "dialog", "error",
321: "ssl_handshake_error")
322: + ": "
323: + e.getLocalizedMessage()
324: + "\n"
325: + MailResourceLoader
326: .getString(
327: "dialog",
328: "error",
329: "ssl_turn_off"),
330: "Warning",
331: JOptionPane.DEFAULT_OPTION,
332: JOptionPane.WARNING_MESSAGE, null,
333: options, options[0]);
334:
335: if (result == 1) {
336: throw new CommandCancelledException();
337: }
338:
339: // turn off SSL for the future
340: smtpItem.setBoolean("enable_ssl", false);
341:
342: protocol.openPort();
343:
344: initialize();
345: }
346: } else {
347: Object[] options = new String[] {
348: MailResourceLoader
349: .getString("", "global", "ok")
350: .replaceAll("&", ""),
351: MailResourceLoader.getString("", "global",
352: "cancel").replaceAll("&", "") };
353: int result = JOptionPane.showOptionDialog(null,
354: MailResourceLoader.getString("dialog", "error",
355: "ssl_not_supported")
356: + "\n"
357: + MailResourceLoader.getString(
358: "dialog", "error",
359: "ssl_turn_off"), "Warning",
360: JOptionPane.DEFAULT_OPTION,
361: JOptionPane.WARNING_MESSAGE, null, options,
362: options[0]);
363:
364: if (result == 1) {
365: throw new CommandCancelledException();
366: }
367:
368: // turn off SSL for the future
369: smtpItem.setBoolean("enable_ssl", false);
370: }
371: }
372: }
373:
374: /**
375: * @param string
376: * @return
377: */
378: private boolean isSupported(String string) {
379: for (int i = 0; i < capas.length; i++) {
380: if (capas[i].startsWith(string)) {
381: return true;
382: }
383: }
384:
385: return false;
386: }
387:
388: /**
389: * @return
390: * @throws CommandCancelledException
391: */
392: public List checkSupportedAuthenticationMethods()
393: throws IOException, SMTPException,
394: CommandCancelledException {
395: ensureConnected();
396:
397: List supportedMechanisms = new ArrayList();
398:
399: for (int i = 0; i < capas.length; i++) {
400: if (capas[i].startsWith("AUTH")) {
401: List authMechanisms = AuthenticationFactory
402: .getInstance().getSupportedMechanisms(capas[i]);
403: Iterator it = authMechanisms.iterator();
404: while (it.hasNext()) {
405: supportedMechanisms.add(new Integer(
406: AuthenticationManager
407: .getSaslCode((String) it.next())));
408: }
409:
410: break;
411: }
412: }
413:
414: if (supportedMechanisms.size() == 0) {
415: // Add a default PLAIN login as fallback
416: supportedMechanisms.add(new Integer(
417: AuthenticationManager.SASL_PLAIN));
418: }
419:
420: return supportedMechanisms;
421: }
422:
423: private void initialize() throws IOException, SMTPException {
424: try {
425: capas = protocol.ehlo(InetAddress.getLocalHost());
426: } catch (SMTPException e1) {
427: // EHLO not supported -> AUTH not supported
428: if (protocol.getState() < SMTPProtocol.PLAIN) {
429: protocol.openPort();
430: }
431: protocol.helo(InetAddress.getLocalHost());
432: capas = new String[] {};
433: }
434: }
435:
436: /**
437: * @param authType
438: * @return
439: * @throws CommandCancelledException
440: */
441: private int getLoginMethod() throws IOException, SMTPException,
442: CommandCancelledException {
443: String authType = accountItem.getSmtpItem().get("login_method");
444: int method = 0;
445:
446: try {
447: method = Integer.parseInt(authType);
448: } catch (NumberFormatException e) {
449: //Fallback to Securest Login method
450: }
451:
452: if (method == 0) {
453: List supported = checkSupportedAuthenticationMethods();
454:
455: if (accountItem.isPopAccount()) {
456: supported.add(new Integer(
457: AuthenticationManager.POP_BEFORE_SMTP));
458: }
459:
460: if (supported.size() == 0) {
461: // No Authentication available
462: return AuthenticationManager.NONE;
463: }
464:
465: if (usingSSL) {
466: // NOTE if SSL is possible we just need the plain login
467: // since SSL does the encryption for us.
468: method = ((Integer) supported.get(0)).intValue();
469: } else {
470: Collections.sort(supported,
471: new AuthenticationSecurityComparator());
472: method = ((Integer) supported.get(supported.size() - 1))
473: .intValue();
474: }
475: }
476:
477: return method;
478: }
479:
480: /**
481: *
482: * close the connection to the SMTP server
483: *
484: */
485: public void closeConnection() {
486: // Close Port
487: try {
488: protocol.quit();
489: } catch (Exception e) {
490: e.printStackTrace();
491: }
492: }
493:
494: /**
495: *
496: * POP-before-SMTP authentication makes use of the POP3 authentication
497: * mechanism, before sending mail.
498: *
499: * Basically you authenticate with the POP3 server, which allows you to use
500: * the SMTP server for sending mail for a specific amount of time.
501: *
502: * @throws Exception
503: */
504: protected void pop3Authentification() throws IOException,
505: POP3Exception, CommandCancelledException {
506: POP3Store.doPOPbeforeSMTP(accountItem.getPopItem());
507: }
508:
509: /**
510: * Send a message
511: *
512: * For an complete example of creating a <class>SendableMessage </class>
513: * object see <class>MessageComposer </class>
514: *
515: * @param message
516: * @param workerStatusController
517: * @throws Exception
518: */
519: public void sendMessage(SendableMessage message,
520: IWorkerStatusController workerStatusController)
521: throws SMTPException, IOException,
522: CommandCancelledException {
523: ensureAuthenticated();
524:
525: // send from address and recipient list to SMTP server
526: // ->all addresses have to be normalized
527: protocol.mail(identity.getAddress());
528:
529: Iterator recipients = message.getRecipients().iterator();
530:
531: while (recipients.hasNext()) {
532: try {
533: protocol
534: .rcpt(Address.parse((String) recipients.next()));
535: } catch (ParserException e1) {
536: throw new SMTPException(e1);
537: }
538: }
539:
540: // now send message source
541: protocol.data(new ProgressObservedInputStream(message
542: .getSourceStream(), workerStatusController));
543: }
544:
545: public String getName() {
546: OutgoingItem smtpItem = accountItem.getSmtpItem();
547: String host = smtpItem.get("host");
548:
549: return host;
550: }
551:
552: public void dropConnection() throws IOException {
553: protocol.dropConnection();
554: }
555:
556: public void update(Observable o, Object arg) {
557: protocol = new SMTPProtocol(smtpItem.get("host"), smtpItem
558: .getInteger("port"));
559: }
560: }
|