View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package javax.mail;
21  
22  import java.net.InetAddress;
23  import java.net.UnknownHostException;
24  import java.util.List;   
25  import java.util.Vector;
26  
27  import javax.mail.event.ConnectionEvent;
28  import javax.mail.event.ConnectionListener;
29  import javax.mail.event.MailEvent;
30  
31  /**
32   * @version $Rev: 597092 $ $Date: 2007-11-21 09:02:38 -0500 (Wed, 21 Nov 2007) $
33   */
34  public abstract class Service {
35      /**
36       * The session from which this service was created.
37       */
38      protected Session session;
39      /**
40       * The URLName of this service
41       */
42      protected URLName url;
43      /**
44       * Debug flag for this service, set from the Session's debug flag.
45       */
46      protected boolean debug;
47  
48      private boolean connected;
49      private final Vector connectionListeners = new Vector(2);
50      // the EventQueue spins off a new thread, so we only create this 
51      // if we have actual listeners to dispatch an event to. 
52      private EventQueue queue = null;
53      // when returning the URL, we need to ensure that the password and file information is 
54      // stripped out. 
55      private URLName exposedUrl; 
56  
57      /**
58       * Construct a new Service.
59       * @param session the session from which this service was created
60       * @param url the URLName of this service
61       */
62      protected Service(Session session, URLName url) {
63          this.session = session;
64          this.url = url;
65          this.debug = session.getDebug();
66      }
67  
68      /**
69       * A generic connect method that takes no parameters allowing subclasses
70       * to implement an appropriate authentication scheme.
71       * The default implementation calls <code>connect(null, null, null)</code>
72       * @throws AuthenticationFailedException if authentication fails
73       * @throws MessagingException for other failures
74       */
75      public void connect() throws MessagingException {
76          connect(null, null, null);
77      }
78  
79      /**
80       * Connect to the specified host using a simple username/password authenticaion scheme
81       * and the default port.
82       * The default implementation calls <code>connect(host, -1, user, password)</code>
83       *
84       * @param host the host to connect to
85       * @param user the user name
86       * @param password the user's password
87       * @throws AuthenticationFailedException if authentication fails
88       * @throws MessagingException for other failures
89       */
90      public void connect(String host, String user, String password) throws MessagingException {
91          connect(host, -1, user, password);
92      }
93  
94      /**
95       * Connect to the specified host using a simple username/password authenticaion scheme
96       * and the default host and port.
97       * The default implementation calls <code>connect(host, -1, user, password)</code>
98       *
99       * @param user the user name
100      * @param password the user's password
101      * @throws AuthenticationFailedException if authentication fails
102      * @throws MessagingException for other failures
103      */
104     public void connect(String user, String password) throws MessagingException {
105         connect(null, -1, user, password);
106     }
107 
108     /**
109      * Connect to the specified host at the specified port using a simple username/password authenticaion scheme.
110      *
111      * If this Service is already connected, an IllegalStateException is thrown.
112      *
113      * @param host the host to connect to
114      * @param port the port to connect to; pass -1 to use the default for the protocol
115      * @param user the user name
116      * @param password the user's password
117      * @throws AuthenticationFailedException if authentication fails
118      * @throws MessagingException for other failures
119      * @throws IllegalStateException if this service is already connected
120      */
121     public void connect(String host, int port, String user, String password) throws MessagingException {
122 
123         if (isConnected()) {
124             throw new IllegalStateException("Already connected");
125         }
126 
127         // before we try to connect, we need to derive values for some parameters that may not have
128         // been explicitly specified.  For example, the normal connect() method leaves us to derive all
129         // of these from other sources.  Some of the values are derived from our URLName value, others
130         // from session parameters.  We need to go through all of these to develop a set of values we
131         // can connect with.
132 
133         // this is the protocol we're connecting with.  We use this largely to derive configured values from
134         // session properties.
135         String protocol = null;
136 
137         // if we're working with the URL form, then we can retrieve the protocol from the URL.
138         if (url != null) {
139             protocol = url.getProtocol();
140         }
141         
142         // if the port is -1, see if we have an override from url. 
143         if (port == -1) {
144             if (protocol != null) {
145                 port = url.getPort();
146             }
147         }
148 
149         // now try to derive values for any of the arguments we've been given as defaults
150         if (host == null) {
151             // first choice is from the url, if we have
152             if (url != null) {
153                 host = url.getHost();
154                 // it is possible that this could return null (rare).  If it does, try to get a
155                 // value from a protocol specific session variable.
156                 if (host == null) {
157                 	if (protocol != null) {
158                 		host = session.getProperty("mail." + protocol + ".host");
159                 	}
160                 }
161             }
162             // this may still be null...get the global mail property
163             if (host == null) {
164                 host = session.getProperty("mail.host");
165             }
166         }
167 
168         // ok, go after userid information next.
169         if (user == null) {
170             // first choice is from the url, if we have
171             if (url != null) {
172                 user = url.getUsername();
173                 // make sure we get the password from the url, if we can.
174                 if (password == null) {
175                     password = url.getPassword();
176                 }
177                 // user still null?  We have several levels of properties to try yet
178                 if (user == null) {
179                 	if (protocol != null) {
180                 		user = session.getProperty("mail." + protocol + ".user");
181                 	}
182                 }
183             }
184 
185             // this may still be null...get the global mail property
186             if (user == null) {
187                 user = session.getProperty("mail.user");
188             }
189 
190             // finally, we try getting the system defined user name
191             try {
192                 user = System.getProperty("user.name");
193             } catch (SecurityException e) {
194                 // we ignore this, and just us a null username.
195             }
196         }
197         // if we have an explicitly given user name, we need to see if this matches the url one and
198         // grab the password from there.
199         else {
200             if (url != null && user.equals(url.getUsername())) {
201                 password = url.getPassword();
202             }
203         }
204 
205         // we need to update the URLName associated with this connection once we have all of the information,
206         // which means we also need to propogate the file portion of the URLName if we have this form when
207         // we start.
208         String file = null;
209         if (url != null) {
210             file = url.getFile();
211         }
212 
213         // see if we have cached security information to use.  If this is not cached, we'll save it
214         // after we successfully connect.
215         boolean cachePassword = false;
216 
217 
218         // still have a null password to this point, and using a url form?
219         if (password == null && url != null) {
220             // construct a new URL, filling in any pieces that may have been explicitly specified.
221             setURLName(new URLName(protocol, host, port, file, user, password));
222             // now see if we have a saved password from a previous request.
223             PasswordAuthentication cachedPassword = session.getPasswordAuthentication(getURLName());
224 
225             // if we found a saved one, see if we need to get any the pieces from here.
226             if (cachedPassword != null) {
227                 // not even a resolved userid?  Then use both bits.
228                 if (user == null) {
229                     user = cachedPassword.getUserName();
230                     password = cachedPassword.getPassword();
231                 }
232                 // our user name must match the cached name to be valid.
233                 else if (user.equals(cachedPassword.getUserName())) {
234                     password = cachedPassword.getPassword();
235                 }
236             }
237             else
238             {
239                 // nothing found in the cache, so we need to save this if we can connect successfully.
240                 cachePassword = true;
241             }
242         }
243 
244         // we've done our best up to this point to obtain all of the information needed to make the
245         // connection.  Now we pass this off to the protocol handler to see if it works.  If we get a
246         // connection failure, we may need to prompt for a password before continuing.
247         try {
248             connected = protocolConnect(host, port, user, password);
249         }
250         catch (AuthenticationFailedException e) {
251         }
252 
253         if (!connected) {
254             InetAddress ipAddress = null;
255 
256             try {
257                 ipAddress = InetAddress.getByName(host);
258             } catch (UnknownHostException e) {
259             }
260 
261             // now ask the session to try prompting for a password.
262             PasswordAuthentication promptPassword = session.requestPasswordAuthentication(ipAddress, port, protocol, null, user);
263 
264             // if we were able to obtain new information from the session, then try again using the
265             // provided information .
266             if (promptPassword != null) {
267                 user = promptPassword.getUserName();
268                 password = promptPassword.getPassword();
269             }
270 
271             connected = protocolConnect(host, port, user, password);
272         }
273 
274 
275         // if we're still not connected, then this is an exception.
276         if (!connected) {
277             throw new AuthenticationFailedException();
278         }
279 
280         // the URL name needs to reflect the most recent information.
281         setURLName(new URLName(protocol, host, port, file, user, password));
282 
283         // we need to update the global password cache with this information.
284         if (cachePassword) {
285             session.setPasswordAuthentication(getURLName(), new PasswordAuthentication(user, password));
286         }
287 
288         // we're now connected....broadcast this to any interested parties.
289         setConnected(connected);
290         notifyConnectionListeners(ConnectionEvent.OPENED);
291     }
292 
293     /**
294      * Attempt the protocol-specific connection; subclasses should override this to establish
295      * a connection in the appropriate manner.
296      * 
297      * This method should return true if the connection was established.
298      * It may return false to cause the {@link #connect(String, int, String, String)} method to
299      * reattempt the connection after trying to obtain user and password information from the user.
300      * Alternatively it may throw a AuthenticatedFailedException to abandon the conection attempt.
301      * 
302      * @param host     The target host name of the service.
303      * @param port     The connection port for the service.
304      * @param user     The user name used for the connection.
305      * @param password The password used for the connection.
306      * 
307      * @return true if a connection was established, false if there was authentication 
308      *         error with the connection.
309      * @throws AuthenticationFailedException
310      *                if authentication fails
311      * @throws MessagingException
312      *                for other failures
313      */
314     protected boolean protocolConnect(String host, int port, String user, String password) throws MessagingException {
315         return false;
316     }
317 
318     /**
319      * Check if this service is currently connected.
320      * The default implementation simply returns the value of a private boolean field;
321      * subclasses may wish to override this method to verify the physical connection.
322      *
323      * @return true if this service is connected
324      */
325     public boolean isConnected() {
326         return connected;
327     }
328 
329     /**
330      * Notification to subclasses that the connection state has changed.
331      * This method is called by the connect() and close() methods to indicate state change;
332      * subclasses should also call this method if the connection is automatically closed
333      * for some reason.
334      *
335      * @param connected the connection state
336      */
337     protected void setConnected(boolean connected) {
338         this.connected = connected;
339     }
340 
341     /**
342      * Close this service and terminate its physical connection.
343      * The default implementation simply calls setConnected(false) and then
344      * sends a CLOSED event to all registered ConnectionListeners.
345      * Subclasses overriding this method should still ensure it is closed; they should
346      * also ensure that it is called if the connection is closed automatically, for
347      * for example in a finalizer.
348      *
349      *@throws MessagingException if there were errors closing; the connection is still closed
350      */
351     public void close() throws MessagingException {
352         setConnected(false);
353         notifyConnectionListeners(ConnectionEvent.CLOSED);
354     }
355 
356     /**
357      * Return a copy of the URLName representing this service with the password and file information removed.
358      *
359      * @return the URLName for this service
360      */
361     public URLName getURLName() {
362         // if we haven't composed the URL version we hand out, create it now.  But only if we really 
363         // have a URL. 
364         if (exposedUrl == null) {
365             if (url != null) {
366                 exposedUrl = new URLName(url.getProtocol(), url.getHost(), url.getPort(), null, url.getUsername(), null);
367             }
368         }
369         return exposedUrl; 
370     }
371 
372     /**
373      * Set the url field.
374      * @param url the new value
375      */
376     protected void setURLName(URLName url) {
377         this.url = url;
378     }
379 
380     public void addConnectionListener(ConnectionListener listener) {
381         connectionListeners.add(listener);
382     }
383 
384     public void removeConnectionListener(ConnectionListener listener) {
385         connectionListeners.remove(listener);
386     }
387 
388     protected void notifyConnectionListeners(int type) {
389         queueEvent(new ConnectionEvent(this, type), connectionListeners);
390     }
391 
392     public String toString() {
393         // NOTE:  We call getURLName() rather than use the URL directly 
394         // because the get method strips out the password information. 
395         URLName url = getURLName(); 
396         
397         return url == null ? super.toString() : url.toString();
398     }
399 
400     protected void queueEvent(MailEvent event, Vector listeners) {
401         // if there are no listeners to dispatch this to, don't put it on the queue. 
402         // This allows us to delay creating the queue (and its new thread) until 
403         // we 
404         if (listeners.isEmpty()) {
405             return; 
406         }
407         // first real event?  Time to get the queue kicked off. 
408         if (queue == null) {
409             queue = new EventQueue(); 
410         }
411         // tee it up and let it rip. 
412         queue.queueEvent(event, (List)listeners.clone()); 
413     }
414 
415     protected void finalize() throws Throwable {
416         // stop our event queue if we had to create one 
417         if (queue != null) {
418             queue.stop();
419         }
420         connectionListeners.clear();
421         super.finalize();
422     }
423 
424 
425     /**
426      * Package scope utility method to allow Message instances
427      * access to the Service's session.
428      *
429      * @return The Session the service is associated with.
430      */
431     Session getSession() {
432         return session;
433     }
434 }