001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package javax.mail;
021    
022    import java.net.InetAddress;
023    import java.net.UnknownHostException;
024    import java.util.List;
025    import java.util.Vector;
026    
027    import javax.mail.event.ConnectionEvent;
028    import javax.mail.event.ConnectionListener;
029    import javax.mail.event.MailEvent;
030    
031    /**
032     * @version $Rev: 826623 $ $Date: 2009-10-19 05:56:31 -0400 (Mon, 19 Oct 2009) $
033     */
034    public abstract class Service {
035        /**
036         * The session from which this service was created.
037         */
038        protected Session session;
039        /**
040         * The URLName of this service
041         */
042        protected URLName url;
043        /**
044         * Debug flag for this service, set from the Session's debug flag.
045         */
046        protected boolean debug;
047    
048        private boolean connected;
049        private final Vector connectionListeners = new Vector(2);
050        // the EventQueue spins off a new thread, so we only create this
051        // if we have actual listeners to dispatch an event to.
052        private EventQueue queue = null;
053        // when returning the URL, we need to ensure that the password and file information is
054        // stripped out.
055        private URLName exposedUrl;
056    
057        /**
058         * Construct a new Service.
059         * @param session the session from which this service was created
060         * @param url the URLName of this service
061         */
062        protected Service(Session session, URLName url) {
063            this.session = session;
064            this.url = url;
065            this.debug = session.getDebug();
066        }
067    
068        /**
069         * A generic connect method that takes no parameters allowing subclasses
070         * to implement an appropriate authentication scheme.
071         * The default implementation calls <code>connect(null, null, null)</code>
072         * @throws AuthenticationFailedException if authentication fails
073         * @throws MessagingException for other failures
074         */
075        public void connect() throws MessagingException {
076            connect(null, null, null);
077        }
078    
079        /**
080         * Connect to the specified host using a simple username/password authenticaion scheme
081         * and the default port.
082         * The default implementation calls <code>connect(host, -1, user, password)</code>
083         *
084         * @param host the host to connect to
085         * @param user the user name
086         * @param password the user's password
087         * @throws AuthenticationFailedException if authentication fails
088         * @throws MessagingException for other failures
089         */
090        public void connect(String host, String user, String password) throws MessagingException {
091            connect(host, -1, user, password);
092        }
093    
094        /**
095         * Connect to the specified host using a simple username/password authenticaion scheme
096         * and the default host and port.
097         * The default implementation calls <code>connect(host, -1, user, password)</code>
098         *
099         * @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                }
178    
179                // user still null?  We have several levels of properties to try yet
180                if (user == null) {
181                    if (protocol != null) {
182                        user = session.getProperty("mail." + protocol + ".user");
183                    }
184    
185                    // this may still be null...get the global mail property
186                    if (user == null) {
187                        user = session.getProperty("mail.user");
188                        // still null, try using the user.name system property
189                        if (user == null) {
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                    }
198                }
199            }
200            // if we have an explicitly given user name, we need to see if this matches the url one and
201            // grab the password from there.
202            else {
203                if (url != null && user.equals(url.getUsername())) {
204                    password = url.getPassword();
205                }
206            }
207    
208            // we need to update the URLName associated with this connection once we have all of the information,
209            // which means we also need to propogate the file portion of the URLName if we have this form when
210            // we start.
211            String file = null;
212            if (url != null) {
213                file = url.getFile();
214            }
215    
216            // see if we have cached security information to use.  If this is not cached, we'll save it
217            // after we successfully connect.
218            boolean cachePassword = false;
219    
220    
221            // still have a null password to this point, and using a url form?
222            if (password == null && url != null) {
223                // construct a new URL, filling in any pieces that may have been explicitly specified.
224                setURLName(new URLName(protocol, host, port, file, user, password));
225                // now see if we have a saved password from a previous request.
226                PasswordAuthentication cachedPassword = session.getPasswordAuthentication(getURLName());
227    
228                // if we found a saved one, see if we need to get any the pieces from here.
229                if (cachedPassword != null) {
230                    // not even a resolved userid?  Then use both bits.
231                    if (user == null) {
232                        user = cachedPassword.getUserName();
233                        password = cachedPassword.getPassword();
234                    }
235                    // our user name must match the cached name to be valid.
236                    else if (user.equals(cachedPassword.getUserName())) {
237                        password = cachedPassword.getPassword();
238                    }
239                }
240                else
241                {
242                    // nothing found in the cache, so we need to save this if we can connect successfully.
243                    cachePassword = true;
244                }
245            }
246    
247            // we've done our best up to this point to obtain all of the information needed to make the
248            // connection.  Now we pass this off to the protocol handler to see if it works.  If we get a
249            // connection failure, we may need to prompt for a password before continuing.
250            try {
251                connected = protocolConnect(host, port, user, password);
252            }
253            catch (AuthenticationFailedException e) {
254            }
255    
256            if (!connected) {
257                InetAddress ipAddress = null;
258    
259                try {
260                    ipAddress = InetAddress.getByName(host);
261                } catch (UnknownHostException e) {
262                }
263    
264                // now ask the session to try prompting for a password.
265                PasswordAuthentication promptPassword = session.requestPasswordAuthentication(ipAddress, port, protocol, null, user);
266    
267                // if we were able to obtain new information from the session, then try again using the
268                // provided information .
269                if (promptPassword != null) {
270                    user = promptPassword.getUserName();
271                    password = promptPassword.getPassword();
272                }
273    
274                connected = protocolConnect(host, port, user, password);
275            }
276    
277    
278            // if we're still not connected, then this is an exception.
279            if (!connected) {
280                throw new AuthenticationFailedException();
281            }
282    
283            // the URL name needs to reflect the most recent information.
284            setURLName(new URLName(protocol, host, port, file, user, password));
285    
286            // we need to update the global password cache with this information.
287            if (cachePassword) {
288                session.setPasswordAuthentication(getURLName(), new PasswordAuthentication(user, password));
289            }
290    
291            // we're now connected....broadcast this to any interested parties.
292            setConnected(connected);
293            notifyConnectionListeners(ConnectionEvent.OPENED);
294        }
295    
296        /**
297         * Attempt the protocol-specific connection; subclasses should override this to establish
298         * a connection in the appropriate manner.
299         *
300         * This method should return true if the connection was established.
301         * It may return false to cause the {@link #connect(String, int, String, String)} method to
302         * reattempt the connection after trying to obtain user and password information from the user.
303         * Alternatively it may throw a AuthenticatedFailedException to abandon the conection attempt.
304         *
305         * @param host     The target host name of the service.
306         * @param port     The connection port for the service.
307         * @param user     The user name used for the connection.
308         * @param password The password used for the connection.
309         *
310         * @return true if a connection was established, false if there was authentication
311         *         error with the connection.
312         * @throws AuthenticationFailedException
313         *                if authentication fails
314         * @throws MessagingException
315         *                for other failures
316         */
317        protected boolean protocolConnect(String host, int port, String user, String password) throws MessagingException {
318            return false;
319        }
320    
321        /**
322         * Check if this service is currently connected.
323         * The default implementation simply returns the value of a private boolean field;
324         * subclasses may wish to override this method to verify the physical connection.
325         *
326         * @return true if this service is connected
327         */
328        public boolean isConnected() {
329            return connected;
330        }
331    
332        /**
333         * Notification to subclasses that the connection state has changed.
334         * This method is called by the connect() and close() methods to indicate state change;
335         * subclasses should also call this method if the connection is automatically closed
336         * for some reason.
337         *
338         * @param connected the connection state
339         */
340        protected void setConnected(boolean connected) {
341            this.connected = connected;
342        }
343    
344        /**
345         * Close this service and terminate its physical connection.
346         * The default implementation simply calls setConnected(false) and then
347         * sends a CLOSED event to all registered ConnectionListeners.
348         * Subclasses overriding this method should still ensure it is closed; they should
349         * also ensure that it is called if the connection is closed automatically, for
350         * for example in a finalizer.
351         *
352         *@throws MessagingException if there were errors closing; the connection is still closed
353         */
354        public void close() throws MessagingException {
355            setConnected(false);
356            notifyConnectionListeners(ConnectionEvent.CLOSED);
357        }
358    
359        /**
360         * Return a copy of the URLName representing this service with the password and file information removed.
361         *
362         * @return the URLName for this service
363         */
364        public URLName getURLName() {
365            // if we haven't composed the URL version we hand out, create it now.  But only if we really
366            // have a URL.
367            if (exposedUrl == null) {
368                if (url != null) {
369                    exposedUrl = new URLName(url.getProtocol(), url.getHost(), url.getPort(), null, url.getUsername(), null);
370                }
371            }
372            return exposedUrl;
373        }
374    
375        /**
376         * Set the url field.
377         * @param url the new value
378         */
379        protected void setURLName(URLName url) {
380            this.url = url;
381        }
382    
383        public void addConnectionListener(ConnectionListener listener) {
384            connectionListeners.add(listener);
385        }
386    
387        public void removeConnectionListener(ConnectionListener listener) {
388            connectionListeners.remove(listener);
389        }
390    
391        protected void notifyConnectionListeners(int type) {
392            queueEvent(new ConnectionEvent(this, type), connectionListeners);
393        }
394    
395        public String toString() {
396            // NOTE:  We call getURLName() rather than use the URL directly
397            // because the get method strips out the password information.
398            URLName url = getURLName();
399    
400            return url == null ? super.toString() : url.toString();
401        }
402    
403        protected void queueEvent(MailEvent event, Vector listeners) {
404            // if there are no listeners to dispatch this to, don't put it on the queue.
405            // This allows us to delay creating the queue (and its new thread) until
406            // we
407            if (listeners.isEmpty()) {
408                return;
409            }
410            // first real event?  Time to get the queue kicked off.
411            if (queue == null) {
412                queue = new EventQueue();
413            }
414            // tee it up and let it rip.
415            queue.queueEvent(event, (List)listeners.clone());
416        }
417    
418        protected void finalize() throws Throwable {
419            // stop our event queue if we had to create one
420            if (queue != null) {
421                queue.stop();
422            }
423            connectionListeners.clear();
424            super.finalize();
425        }
426    
427    
428        /**
429         * Package scope utility method to allow Message instances
430         * access to the Service's session.
431         *
432         * @return The Session the service is associated with.
433         */
434        Session getSession() {
435            return session;
436        }
437    }