View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.util;
19  
20  import java.io.IOException; 
21  import java.io.InputStream; 
22  import java.io.OutputStream; 
23  import java.io.PrintStream; 
24  import java.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.net.InetAddress;
27  import java.net.Socket; 
28  import java.net.UnknownHostException;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.StringTokenizer; 
33  
34  import javax.mail.MessagingException;
35  import javax.mail.Session;
36  import javax.net.ssl.SSLSocket;
37  
38  import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 
39  import org.apache.geronimo.javamail.authentication.CramMD5Authenticator; 
40  import org.apache.geronimo.javamail.authentication.DigestMD5Authenticator; 
41  import org.apache.geronimo.javamail.authentication.LoginAuthenticator; 
42  import org.apache.geronimo.javamail.authentication.PlainAuthenticator; 
43  import org.apache.geronimo.javamail.authentication.SASLAuthenticator; 
44  import org.apache.geronimo.javamail.util.CommandFailedException;      
45  import org.apache.geronimo.javamail.util.InvalidCommandException;      
46  
47  /**
48   * Base class for all mail Store/Transport connection.  Centralizes management
49   * of a lot of common connection handling.  Actual protcol-specific 
50   * functions are handled at the subclass level. 
51   */
52  public class MailConnection {
53      /**
54       * constants for EOL termination
55       */
56      protected static final char CR = '\r';
57      protected static final char LF = '\n';
58  
59      /**
60       * property keys for protocol properties.
61       */
62      protected static final String MAIL_AUTH = "auth";
63      protected static final String MAIL_PORT = "port";
64      protected static final String MAIL_LOCALHOST = "localhost";
65      protected static final String MAIL_STARTTLS_ENABLE = "starttls.enable";
66      protected static final String MAIL_SSL_ENABLE = "ssl.enable";
67      protected static final String MAIL_TIMEOUT = "timeout";
68      protected static final String MAIL_SASL_ENABLE = "sasl.enable";
69      protected static final String MAIL_SASL_REALM = "sasl.realm";
70      protected static final String MAIL_AUTHORIZATIONID = "sasl.authorizationid"; 
71      protected static final String MAIL_SASL_MECHANISMS = "sasl.mechanisms";
72      protected static final String MAIL_PLAIN_DISABLE = "auth.plain.disable";
73      protected static final String MAIL_LOGIN_DISABLE = "auth.login.disable";
74      protected static final String MAIL_FACTORY_CLASS = "socketFactory.class";
75      protected static final String MAIL_FACTORY_FALLBACK = "socketFactory.fallback";
76      protected static final String MAIL_FACTORY_PORT = "socketFactory.port";
77      protected static final String MAIL_SSL_PROTOCOLS = "ssl.protocols";
78      protected static final String MAIL_SSL_CIPHERSUITES = "ssl.ciphersuites";
79      protected static final String MAIL_LOCALADDRESS = "localaddress";
80      protected static final String MAIL_LOCALPORT = "localport";
81      protected static final String MAIL_ENCODE_TRACE = "encodetrace";
82  
83      protected static final int MIN_MILLIS = 1000 * 60;
84      protected static final int TIMEOUT = MIN_MILLIS * 5;
85      protected static final String DEFAULT_MAIL_HOST = "localhost";
86  
87      protected static final String CAPABILITY_STARTTLS = "STARTTLS";
88  
89      protected static final String AUTHENTICATION_PLAIN = "PLAIN";
90      protected static final String AUTHENTICATION_LOGIN = "LOGIN";
91      protected static final String AUTHENTICATION_CRAMMD5 = "CRAM-MD5";
92      protected static final String AUTHENTICATION_DIGESTMD5 = "DIGEST-MD5";
93      
94      // The mail Session we're associated with
95      protected Session session; 
96      // The protocol we're implementing 
97      protected String protocol; 
98      // There are usually SSL and non-SSL versions of these protocols.  This 
99      // indicates which version we're using.
100     protected boolean sslConnection; 
101     // This is the default port we should be using for making a connection.  Each 
102     // protocol (and each ssl version of the protocol) normally has a different default that 
103     // should be used. 
104     protected int defaultPort; 
105     
106     // a wrapper around our session to provide easier lookup of protocol 
107     // specific property values 
108     protected ProtocolProperties props; 
109     
110     // The target server host 
111     protected String serverHost;
112     // The target server port 
113     protected int serverPort; 
114     
115     // the connection socket...can be a plain socket or SSLSocket, if TLS is being used.
116     protected Socket socket;
117     
118     // our local host name
119     protected InetAddress localAddress;
120     // our local port value 
121     protected int localPort; 
122     // our local host name
123     protected String localHost;
124     
125     // our timeout value 
126     protected int timeout; 
127     
128     // our login username 
129     protected String username; 
130     // our login password 
131     protected String password; 
132     // our SASL security realm 
133     protected String realm; 
134     // our authorization id 
135     protected String authid; 
136     
137     // input stream used to read data.  If Sasl is in use, this might be other than the
138     // direct access to the socket input stream.
139     protected InputStream inputStream;
140     // the other end of the connection pipeline.
141     protected OutputStream outputStream;
142 
143     // our session provided debug output stream.
144     protected PrintStream debugStream;
145     // our debug flag (passed from the hosting transport)
146     protected boolean debug;
147 
148     // list of authentication mechanisms supported by the server
149     protected List authentications;
150     // map of server extension arguments
151     protected Map capabilities;        
152     // property list of authentication mechanisms
153     protected List mechanisms; 
154     
155     protected MailConnection(ProtocolProperties props) 
156     {
157         // this is our properties retriever utility, which will look up 
158         // properties based on the appropriate "mail.protocol." prefix. 
159         // this also holds other information we might need for access, such as 
160         // the protocol name and the Session; 
161         this.props = props; 
162         this.protocol = props.getProtocol(); 
163         this.session = props.getSession(); 
164         this.sslConnection = props.getSSLConnection(); 
165         this.defaultPort = props.getDefaultPort(); 
166         
167         // initialize our debug settings from the session 
168         debug = session.getDebug(); 
169         debugStream = session.getDebugOut();
170     }
171     
172     
173     /**
174      * Connect to the server and do the initial handshaking.
175      * 
176      * @param host     The target host name.
177      * @param port     The target port
178      * @param username The connection username (can be null)
179      * @param password The authentication password (can be null).
180      * 
181      * @return true if we were able to obtain a connection and 
182      *         authenticate.
183      * @exception MessagingException
184      */
185     public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
186         // NOTE:  We don't check for the username/password being null at this point.  It's possible that 
187         // the server will send back a PREAUTH response, which means we don't need to go through login 
188         // processing.  We'll need to check the capabilities response after we make the connection to decide 
189         // if logging in is necesssary. 
190         
191         // save this for subsequent connections.  All pool connections will use this info.
192         // if the port is defaulted, then see if we have something configured in the session.
193         // if not configured, we just use the default default.
194         if (port == -1) {
195             // check for a property and fall back on the default if it's not set.
196             port = props.getIntProperty(MAIL_PORT, props.getDefaultPort());
197             // it's possible that -1 might have been explicitly set, so one last check. 
198             if (port == -1) {
199                 port = props.getDefaultPort(); 
200             }
201         }
202     	
203     	// Before we do anything, let's make sure that we succesfully received a host
204     	if ( host == null ) {
205     		host = DEFAULT_MAIL_HOST;
206     	}
207         
208         this.serverHost = host;
209         this.serverPort = port;
210         this.username = username;
211         this.password = password;
212         
213         // make sure we have the realm information 
214         realm = props.getProperty(MAIL_SASL_REALM); 
215         // get an authzid value, if we have one.  The default is to use the username.
216         authid = props.getProperty(MAIL_AUTHORIZATIONID, username);
217         return true; 
218     }
219     
220     
221     /**
222      * Establish a connection using an existing socket. 
223      * 
224      * @param s      The socket to use.
225      */
226     public void connect(Socket s) {
227         // just save the socket connection 
228         this.socket = s; 
229     }
230     
231     
232     /**
233      * Create a transport connection object and connect it to the
234      * target server.
235      *
236      * @exception MessagingException
237      */
238     protected void getConnection() throws IOException, MessagingException
239     {
240         // We might have been passed a socket to connect with...if not, we need to create one of the correct type.
241         if (socket == null) {
242             // get the connection properties that control how we set this up. 
243             getConnectionProperties(); 
244             // if this is the SSL version of the protocol, we start with an SSLSocket
245             if (sslConnection) {
246                 getConnectedSSLSocket();
247             }
248             else
249             {
250                 getConnectedSocket();
251             }
252         }
253         // if we already have a socket, get some information from it and override what we've been passed.
254         else {
255             localPort = socket.getPort();
256             localAddress = socket.getInetAddress();
257         }
258         
259         // now set up the input/output streams.
260         getConnectionStreams(); 
261     }
262 
263     /**
264      * Get common connection properties before creating a connection socket. 
265      */
266     protected void getConnectionProperties() {
267 
268         // there are several protocol properties that can be set to tune the created socket.  We need to
269         // retrieve those bits before creating the socket.
270         timeout = props.getIntProperty(MAIL_TIMEOUT, -1);
271         localAddress = null;
272         // see if we have a local address override.
273         String localAddrProp = props.getProperty(MAIL_LOCALADDRESS);
274         if (localAddrProp != null) {
275             try {
276                 localAddress = InetAddress.getByName(localAddrProp);
277             } catch (UnknownHostException e) {
278                 // not much we can do if this fails. 
279             }
280         }
281 
282         // check for a local port...default is to allow socket to choose.
283         localPort = props.getIntProperty(MAIL_LOCALPORT, 0);
284     }
285     
286 
287     /**
288      * Creates a connected socket
289      *
290      * @exception MessagingException
291      */
292     protected void getConnectedSocket() throws IOException {
293         debugOut("Attempting plain socket connection to server " + serverHost + ":" + serverPort);
294 
295         // check the properties that control how we connect. 
296         getConnectionProperties(); 
297 
298         // the socket factory can be specified via a session property.  By default, we just directly
299         // instantiate a socket without using a factory.
300         String socketFactory = props.getProperty(MAIL_FACTORY_CLASS);
301 
302         // make sure the socket is nulled out to start 
303         socket = null;
304 
305         // if there is no socket factory defined (normal), we just create a socket directly.
306         if (socketFactory == null) {
307             socket = new Socket(serverHost, serverPort, localAddress, localPort);
308         }
309 
310         else {
311             try {
312                 int socketFactoryPort = props.getIntProperty(MAIL_FACTORY_PORT, -1);
313 
314                 // we choose the port used by the socket based on overrides.
315                 Integer portArg = new Integer(socketFactoryPort == -1 ? serverPort : socketFactoryPort);
316 
317                 // use the current context loader to resolve this.
318                 ClassLoader loader = Thread.currentThread().getContextClassLoader();
319                 Class factoryClass = loader.loadClass(socketFactory);
320 
321                 // done indirectly, we need to invoke the method using reflection.
322                 // This retrieves a factory instance.
323                 Method getDefault = factoryClass.getMethod("getDefault", new Class[0]);
324                 Object defFactory = getDefault.invoke(new Object(), new Object[0]);
325 
326                 // now that we have the factory, there are two different createSocket() calls we use,
327                 // depending on whether we have a localAddress override.
328 
329                 if (localAddress != null) {
330                     // retrieve the createSocket(String, int, InetAddress, int) method.
331                     Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE };
332                     Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
333 
334                     Object[] createSocketArgs = new Object[] { serverHost, portArg, localAddress, new Integer(localPort) };
335                     socket = (Socket)createSocket.invoke(defFactory, createSocketArgs);
336                 }
337                 else {
338                     // retrieve the createSocket(String, int) method.
339                     Class[] createSocketSig = new Class[] { String.class, Integer.TYPE };
340                     Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
341 
342                     Object[] createSocketArgs = new Object[] { serverHost, portArg };
343                     socket = (Socket)createSocket.invoke(defFactory, createSocketArgs);
344                 }
345             } catch (Throwable e) {
346                 // if a socket factor is specified, then we may need to fall back to a default.  This behavior
347                 // is controlled by (surprise) more session properties.
348                 if (props.getBooleanProperty(MAIL_FACTORY_FALLBACK, false)) {
349                     debugOut("First plain socket attempt failed, falling back to default factory", e);
350                     socket = new Socket(serverHost, serverPort, localAddress, localPort);
351                 }
352                 // we have an exception.  We're going to throw an IOException, which may require unwrapping
353                 // or rewrapping the exception.
354                 else {
355                     // we have an exception from the reflection, so unwrap the base exception
356                     if (e instanceof InvocationTargetException) {
357                         e = ((InvocationTargetException)e).getTargetException();
358                     }
359 
360                     debugOut("Plain socket creation failure", e);
361 
362                     // throw this as an IOException, with the original exception attached.
363                     IOException ioe = new IOException("Error connecting to " + serverHost + ", " + serverPort);
364                     ioe.initCause(e);
365                     throw ioe;
366                 }
367             }
368         }
369         // if we have a timeout value, set that before returning 
370         if (timeout >= 0) {
371             socket.setSoTimeout(timeout);
372         }
373     }
374 
375 
376     /**
377      * Creates a connected SSL socket for an initial SSL connection.
378      *
379      * @exception MessagingException
380      */
381     protected void getConnectedSSLSocket() throws IOException {
382         debugOut("Attempting SSL socket connection to server " + serverHost + ":" + serverPort);
383         // the socket factory can be specified via a protocol property, a session property, and if all else
384         // fails (which it usually does), we fall back to the standard factory class.
385         String socketFactory = props.getProperty(MAIL_FACTORY_CLASS, "javax.net.ssl.SSLSocketFactory");
386 
387         // make sure this is null 
388         socket = null;
389 
390         // we'll try this with potentially two different factories if we're allowed to fall back.
391         boolean fallback = props.getBooleanProperty(MAIL_FACTORY_FALLBACK, false);
392 
393         while (true) {
394             try {
395                 debugOut("Creating SSL socket using factory " + socketFactory);
396 
397                 int socketFactoryPort = props.getIntProperty(MAIL_FACTORY_PORT, -1);
398 
399                 // we choose the port used by the socket based on overrides.
400                 Integer portArg = new Integer(socketFactoryPort == -1 ? serverPort : socketFactoryPort);
401 
402                 // use the current context loader to resolve this.
403                 ClassLoader loader = Thread.currentThread().getContextClassLoader();
404                 Class factoryClass = loader.loadClass(socketFactory);
405 
406                 // done indirectly, we need to invoke the method using reflection.
407                 // This retrieves a factory instance.
408                 Method getDefault = factoryClass.getMethod("getDefault", new Class[0]);
409                 Object defFactory = getDefault.invoke(new Object(), new Object[0]);
410 
411                 // now that we have the factory, there are two different createSocket() calls we use,
412                 // depending on whether we have a localAddress override.
413 
414                 if (localAddress != null) {
415                     // retrieve the createSocket(String, int, InetAddress, int) method.
416                     Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE };
417                     Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
418 
419                     Object[] createSocketArgs = new Object[] { serverHost, portArg, localAddress, new Integer(localPort) };
420                     socket = (Socket)createSocket.invoke(defFactory, createSocketArgs);
421                     break; 
422                 }
423                 else {
424                     // retrieve the createSocket(String, int) method.
425                     Class[] createSocketSig = new Class[] { String.class, Integer.TYPE };
426                     Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
427 
428                     Object[] createSocketArgs = new Object[] { serverHost, portArg };
429                     socket = (Socket)createSocket.invoke(defFactory, createSocketArgs);
430                     break; 
431                 }
432             } catch (Throwable e) {
433                 // if we're allowed to fallback, then use the default factory and try this again.  We only
434                 // allow this to happen once.
435                 if (fallback) {
436                     debugOut("First attempt at creating SSL socket failed, falling back to default factory");
437                     socketFactory = "javax.net.ssl.SSLSocketFactory";
438                     fallback = false;
439                     continue;
440                 }
441                 // we have an exception.  We're going to throw an IOException, which may require unwrapping
442                 // or rewrapping the exception.
443                 else {
444                     // we have an exception from the reflection, so unwrap the base exception
445                     if (e instanceof InvocationTargetException) {
446                         e = ((InvocationTargetException)e).getTargetException();
447                     }
448 
449                     debugOut("Failure creating SSL socket", e);
450                     // throw this as an IOException, with the original exception attached.
451                     IOException ioe = new IOException("Error connecting to " + serverHost + ", " + serverPort);
452                     ioe.initCause(e);
453                     throw ioe;
454                 }
455             }
456         }
457         // and set the timeout value 
458         if (timeout >= 0) {
459             socket.setSoTimeout(timeout);
460         }
461         
462         // if there is a list of protocols specified, we need to break this down into 
463         // the individual names 
464         String protocols = props.getProperty(MAIL_SSL_PROTOCOLS); 
465         if (protocols != null) {
466             ArrayList<String> list = new ArrayList<String>(); 
467             StringTokenizer t = new StringTokenizer(protocols); 
468             
469             while (t.hasMoreTokens()) {
470                 list.add(t.nextToken()); 
471             }
472             
473             ((SSLSocket)socket).setEnabledProtocols(list.toArray(new String[list.size()])); 
474         }
475         
476         // and do the same for any cipher suites 
477         String suites = props.getProperty(MAIL_SSL_CIPHERSUITES); 
478         if (suites != null) {
479             ArrayList<String> list = new ArrayList<String>(); 
480             StringTokenizer t = new StringTokenizer(suites); 
481             
482             while (t.hasMoreTokens()) {
483                 list.add(t.nextToken()); 
484             }
485             
486             ((SSLSocket)socket).setEnabledCipherSuites(list.toArray(new String[list.size()])); 
487         }
488     }
489 
490 
491     /**
492      * Switch the connection to using TLS level security,
493      * switching to an SSL socket.
494      */
495     protected void getConnectedTLSSocket() throws MessagingException {
496      	// it worked, now switch the socket into TLS mode
497      	try {
498 
499             // we use the same target and port as the current connection.
500             String host = socket.getInetAddress().getHostName();
501             int port = socket.getPort();
502 
503             // the socket factory can be specified via a session property.  By default, we use
504             // the native SSL factory.
505             String socketFactory = props.getProperty(MAIL_FACTORY_CLASS, "javax.net.ssl.SSLSocketFactory");
506 
507             // use the current context loader to resolve this.
508             ClassLoader loader = Thread.currentThread().getContextClassLoader();
509             Class factoryClass = loader.loadClass(socketFactory);
510 
511             // done indirectly, we need to invoke the method using reflection.
512             // This retrieves a factory instance.
513             Method getDefault = factoryClass.getMethod("getDefault", new Class[0]);
514             Object defFactory = getDefault.invoke(new Object(), new Object[0]);
515 
516             // now we need to invoke createSocket()
517             Class[] createSocketSig = new Class[] { Socket.class, String.class, Integer.TYPE, Boolean.TYPE };
518             Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
519 
520             Object[] createSocketArgs = new Object[] { socket, host, new Integer(port), Boolean.TRUE };
521 
522             // and finally create the socket
523             Socket sslSocket = (Socket)createSocket.invoke(defFactory, createSocketArgs);
524 
525             // if this is an instance of SSLSocket (very common), try setting the protocol to be
526             // "TLSv1".  If this is some other class because of a factory override, we'll just have to
527             // accept that things will work.
528             if (sslSocket instanceof SSLSocket) {
529                 ((SSLSocket)sslSocket).setEnabledProtocols(new String[] {"TLSv1"} );
530                 ((SSLSocket)sslSocket).setUseClientMode(true);
531                 debugOut("Initiating STARTTLS handshake"); 
532                 ((SSLSocket)sslSocket).startHandshake();
533             }
534 
535 
536             // this is our active socket now
537             socket = sslSocket;
538             getConnectionStreams(); 
539             debugOut("TLS connection established"); 
540      	}
541         catch (Exception e) {
542             debugOut("Failure attempting to convert connection to TLS", e);
543      	    throw new MessagingException("Unable to convert connection to SSL", e);
544      	}
545     }
546     
547     
548     /**
549      * Set up the input and output streams for server communications once the 
550      * socket connection has been made. 
551      * 
552      * @exception MessagingException
553      */
554     protected void getConnectionStreams() throws MessagingException, IOException {
555         // and finally, as a last step, replace our input streams with the secure ones.
556         // now set up the input/output streams.
557         inputStream = new TraceInputStream(socket.getInputStream(), debugStream, debug, props.getBooleanProperty(
558                 MAIL_ENCODE_TRACE, false));
559         outputStream = new TraceOutputStream(socket.getOutputStream(), debugStream, debug, props.getBooleanProperty(
560                 MAIL_ENCODE_TRACE, false));
561     }
562     
563 
564     /**
565      * Close the server connection at termination.
566      */
567     public void closeServerConnection()
568     {
569         try {
570             socket.close();
571         } catch (IOException ignored) {
572         }
573 
574         socket = null;
575         inputStream = null;
576         outputStream = null;
577     }
578     
579     
580     /**
581      * Verify that we have a good connection before 
582      * attempting to send a command. 
583      * 
584      * @exception MessagingException
585      */
586     protected void checkConnected() throws MessagingException {
587         if (socket == null || !socket.isConnected()) {
588             throw new MessagingException("no connection");
589         }
590     }
591 
592 
593     /**
594      * Retrieve the SASL realm used for DIGEST-MD5 authentication.
595      * This will either be explicitly set, or retrieved using the
596      * mail.imap.sasl.realm session property.
597      *
598      * @return The current realm information (which can be null).
599      */
600     public String getSASLRealm() {
601         // if the realm is null, retrieve it using the realm session property.
602         if (realm == null) {
603             realm = props.getProperty(MAIL_SASL_REALM);
604         }
605         return realm;
606     }
607 
608 
609     /**
610      * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton.
611      *
612      * @param name   The new realm name.
613      */
614     public void setSASLRealm(String name) {
615         realm = name;
616     }
617 
618 
619     /**
620      * Get a list of the SASL mechanisms we're configured to accept.
621      *
622      * @return A list of mechanisms we're allowed to use.
623      */
624     protected List getSaslMechanisms() {
625         if (mechanisms == null) {
626             mechanisms = new ArrayList();
627             String mechList = props.getProperty(MAIL_SASL_MECHANISMS);
628             if (mechList != null) {
629                 // the mechanisms are a blank or comma-separated list
630                 StringTokenizer tokenizer = new StringTokenizer(mechList, " ,");
631 
632                 while (tokenizer.hasMoreTokens()) {
633                     String mech = tokenizer.nextToken().toUpperCase();
634                     mechanisms.add(mech);
635                 }
636             }
637         }
638         return mechanisms;
639     }
640     
641     
642     /**
643      * Get the list of authentication mechanisms the server
644      * is supposed to support. 
645      * 
646      * @return A list of the server supported authentication 
647      *         mechanisms.
648      */
649     protected List getServerMechanisms() {
650         return authentications; 
651     }
652     
653     
654     /**
655      * Merge the configured SASL mechanisms with the capabilities that the 
656      * server has indicated it supports, returning a merged list that can 
657      * be used for selecting a mechanism. 
658      * 
659      * @return A List representing the intersection of the configured list and the 
660      *         capabilities list.
661      */
662     protected List selectSaslMechanisms() {
663         List configured = getSaslMechanisms(); 
664         List supported = getServerMechanisms(); 
665         
666         // if not restricted, then we'll select from anything supported. 
667         if (configured.isEmpty()) {
668             return supported; 
669         }
670         
671         List merged = new ArrayList(); 
672         
673         // we might need a subset of the supported ones 
674         for (int i = 0; i < configured.size(); i++) {
675             // if this is in both lists, add to the merged one. 
676             String mech = (String)configured.get(i); 
677             if (supported.contains(mech)) {
678                 merged.add(mech); 
679             }
680         }
681         return merged; 
682     }
683 
684 
685     /**
686      * Process SASL-type authentication.
687      *
688      * @return An authenticator to process the login challenge/response handling.    
689      * @exception MessagingException
690      */
691     protected ClientAuthenticator getLoginAuthenticator() throws MessagingException {
692         
693         // get the list of mechanisms we're allowed to use. 
694         List mechs = selectSaslMechanisms(); 
695 
696         try {
697             String[] mechArray = (String[])mechs.toArray(new String[0]); 
698             // create a SASLAuthenticator, if we can.  A failure likely indicates we're not 
699             // running on a Java 5 VM, and the Sasl API isn't available. 
700             return new SASLAuthenticator(mechArray, session.getProperties(), protocol, serverHost, getSASLRealm(), authid, username, password); 
701         } catch (Throwable e) {
702         }
703         
704 
705         // now go through the progression of mechanisms we support, from the most secure to the
706         // least secure.
707 
708         if (mechs.contains(AUTHENTICATION_DIGESTMD5)) {
709             return new DigestMD5Authenticator(serverHost, username, password, getSASLRealm());
710         }
711         else if (mechs.contains(AUTHENTICATION_CRAMMD5)) {
712             return new CramMD5Authenticator(username, password);
713         }
714         else if (mechs.contains(AUTHENTICATION_LOGIN)) {
715             return new LoginAuthenticator(username, password);
716         }
717         else if (mechs.contains(AUTHENTICATION_PLAIN)) {
718             return new PlainAuthenticator(username, password);
719         }
720         else {
721             // can't find a mechanism we support in common
722             return null;  
723         }
724     }
725     
726     
727     /**
728      * Internal debug output routine.
729      *
730      * @param value  The string value to output.
731      */
732     protected void debugOut(String message) {
733         if (debug) {
734             debugStream.println(protocol + " DEBUG: " + message);
735         }
736     }
737 
738     /**
739      * Internal debugging routine for reporting exceptions.
740      *
741      * @param message A message associated with the exception context.
742      * @param e       The received exception.
743      */
744     protected void debugOut(String message, Throwable e) {
745         if (debug) {
746             debugOut("Received exception -> " + message);
747             debugOut("Exception message -> " + e.getMessage());
748             e.printStackTrace(debugStream);
749         }
750     }
751     
752     
753     /**
754      * Test if this connection has a given capability. 
755      * 
756      * @param capability The capability name.
757      * 
758      * @return true if this capability is in the list, false for a mismatch. 
759      */
760     public boolean hasCapability(String capability) {
761         return capabilities.containsKey(capability); 
762     }
763     
764     /**
765      * Get the capabilities map. 
766      * 
767      * @return The capabilities map for the connection. 
768      */
769     public Map getCapabilities() {
770         return capabilities; 
771     }
772     
773     
774     /**
775      * Test if the server supports a given mechanism. 
776      * 
777      * @param mech   The mechanism name.
778      * 
779      * @return true if the server has asserted support for the named 
780      *         mechanism.
781      */
782     public boolean supportsMechanism(String mech) {
783         return authentications.contains(mech); 
784     }
785     
786     
787     /**
788      * Retrieve the connection host. 
789      * 
790      * @return The host name. 
791      */
792     public String getHost() {
793         return serverHost; 
794     }
795     
796 
797     /**
798      * Retrieve the local client host name.
799      *
800      * @return The string version of the local host name.
801      * @exception SMTPTransportException
802      */
803     public String getLocalHost() throws MessagingException {
804         if (localHost == null) {
805 
806             try {
807                 localHost = InetAddress.getLocalHost().getHostName();
808             } catch (UnknownHostException e) {
809                 // fine, we're misconfigured - ignore
810             }
811 
812             if (localHost == null) {
813                 localHost = props.getProperty(MAIL_LOCALHOST);
814             }
815 
816             if (localHost == null) {
817                 localHost = props.getSessionProperty(MAIL_LOCALHOST);
818             }
819 
820             if (localHost == null) {
821                 throw new MessagingException("Can't get local hostname. "
822                         + " Please correctly configure JDK/DNS or set mail.smtp.localhost");
823             }
824         }
825 
826         return localHost;
827     }
828 
829     
830     /**
831      * Explicitly set the local host information.
832      *
833      * @param localHost
834      *            The new localHost name.
835      */
836     public void setLocalHost(String localHost) {
837         this.localHost = localHost;
838     }
839 }