View Javadoc

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