001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package javax.mail;
019    
020    import java.net.InetAddress;
021    import java.net.UnknownHostException;
022    import java.util.Vector;
023    
024    import javax.mail.event.ConnectionEvent;
025    import javax.mail.event.ConnectionListener;
026    import javax.mail.event.MailEvent;
027    
028    /**
029     * @version $Rev: 390529 $ $Date: 2006-03-31 14:40:02 -0800 (Fri, 31 Mar 2006) $
030     */
031    public abstract class Service {
032        /**
033         * The session from which this service was created.
034         */
035        protected Session session;
036        /**
037         * The URLName of this service
038         */
039        protected URLName url;
040        /**
041         * Debug flag for this service, set from the Session's debug flag.
042         */
043        protected boolean debug;
044    
045        private boolean connected;
046        private final Vector connectionListeners = new Vector(2);
047        private final EventQueue queue = new EventQueue();
048    
049        /**
050         * Construct a new Service.
051         * @param session the session from which this service was created
052         * @param url the URLName of this service
053         */
054        protected Service(Session session, URLName url) {
055            this.session = session;
056            this.url = url;
057            this.debug = session.getDebug();
058        }
059    
060        /**
061         * A generic connect method that takes no parameters allowing subclasses
062         * to implement an appropriate authentication scheme.
063         * The default implementation calls <code>connect(null, null, null)</code>
064         * @throws AuthenticationFailedException if authentication fails
065         * @throws MessagingException for other failures
066         */
067        public void connect() throws MessagingException {
068            connect(null, null, null);
069        }
070    
071        /**
072         * Connect to the specified host using a simple username/password authenticaion scheme
073         * and the default port.
074         * The default implementation calls <code>connect(host, -1, user, password)</code>
075         *
076         * @param host the host to connect to
077         * @param user the user name
078         * @param password the user's password
079         * @throws AuthenticationFailedException if authentication fails
080         * @throws MessagingException for other failures
081         */
082        public void connect(String host, String user, String password) throws MessagingException {
083            connect(host, -1, user, password);
084        }
085    
086        /**
087         * Connect to the specified host at the specified port using a simple username/password authenticaion scheme.
088         *
089         * If this Service is already connected, an IllegalStateException is thrown.
090         *
091         * @param host the host to connect to
092         * @param port the port to connect to; pass -1 to use the default for the protocol
093         * @param user the user name
094         * @param password the user's password
095         * @throws AuthenticationFailedException if authentication fails
096         * @throws MessagingException for other failures
097         * @throws IllegalStateException if this service is already connected
098         */
099        public void connect(String host, int port, String user, String password) throws MessagingException {
100    
101            if (isConnected()) {
102                throw new IllegalStateException("Already connected");
103            }
104    
105            // before we try to connect, we need to derive values for some parameters that may not have
106            // been explicitly specified.  For example, the normal connect() method leaves us to derive all
107            // of these from other sources.  Some of the values are derived from our URLName value, others
108            // from session parameters.  We need to go through all of these to develop a set of values we
109            // can connect with.
110    
111            // this is the protocol we're connecting with.  We use this largely to derive configured values from
112            // session properties.
113            String protocol = null;
114    
115            // if we're working with the URL form, then we can retrieve the protocol from the URL.
116            if (url != null) {
117                protocol = url.getProtocol();
118            }
119    
120            // now try to derive values for any of the arguments we've been given as defaults
121            if (host == null) {
122                // first choice is from the url, if we have
123                if (url != null) {
124                    host = url.getHost();
125                    // it is possible that this could return null (rare).  If it does, try to get a
126                    // value from a protocol specific session variable.
127                    if (host == null) {
128                        host = session.getProperty("mail." + protocol + ".host");
129                    }
130                }
131                // this may still be null...get the global mail property
132                if (host == null) {
133                    host = session.getProperty("mail.host");
134                }
135            }
136    
137            // ok, go after userid information next.
138            if (user == null) {
139                // first choice is from the url, if we have
140                if (url != null) {
141                    user = url.getUsername();
142                    // make sure we get the password from the url, if we can.
143                    if (password == null) {
144                        password = url.getPassword();
145                    }
146                    // user still null?  We have several levels of properties to try yet
147                    if (user == null) {
148                        user = session.getProperty("mail." + protocol + ".user");
149                    }
150                }
151    
152                // this may still be null...get the global mail property
153                if (user == null) {
154                    user = session.getProperty("mail.user");
155                }
156    
157                // finally, we try getting the system defined user name
158                try {
159                    user = System.getProperty("user.name");
160                } catch (SecurityException e) {
161                    // we ignore this, and just us a null username.
162                }
163            }
164            // if we have an explicitly given user name, we need to see if this matches the url one and
165            // grab the password from there.
166            else {
167                if (url != null && user.equals(url.getUsername())) {
168                    password = url.getPassword();
169                }
170            }
171    
172            // we need to update the URLName associated with this connection once we have all of the information,
173            // which means we also need to propogate the file portion of the URLName if we have this form when
174            // we start.
175            String file = null;
176            if (url != null) {
177                file = url.getFile();
178            }
179    
180            // see if we have cached security information to use.  If this is not cached, we'll save it
181            // after we successfully connect.
182            boolean cachePassword = false;
183    
184    
185            // still have a null password to this point, and using a url form?
186            if (password == null && url != null) {
187                // construct a new URL, filling in any pieces that may have been explicitly specified.
188                setURLName(new URLName(protocol, host, port, file, user, password));
189                // now see if we have a saved password from a previous request.
190                PasswordAuthentication cachedPassword = session.getPasswordAuthentication(getURLName());
191    
192                // if we found a saved one, see if we need to get any the pieces from here.
193                if (cachedPassword != null) {
194                    // not even a resolved userid?  Then use both bits.
195                    if (user == null) {
196                        user = cachedPassword.getUserName();
197                        password = cachedPassword.getPassword();
198                    }
199                    // our user name must match the cached name to be valid.
200                    else if (user.equals(cachedPassword.getUserName())) {
201                        password = cachedPassword.getPassword();
202                    }
203                }
204                else
205                {
206                    // nothing found in the cache, so we need to save this if we can connect successfully.
207                    cachePassword = true;
208                }
209            }
210    
211            // we've done our best up to this point to obtain all of the information needed to make the
212            // connection.  Now we pass this off to the protocol handler to see if it works.  If we get a
213            // connection failure, we may need to prompt for a password before continuing.
214            try {
215                connected = protocolConnect(host, port, user, password);
216            }
217            catch (AuthenticationFailedException e) {
218            }
219    
220            if (!connected) {
221                InetAddress ipAddress = null;
222    
223                try {
224                    ipAddress = InetAddress.getByName(host);
225                } catch (UnknownHostException e) {
226                }
227    
228                // now ask the session to try prompting for a password.
229                PasswordAuthentication promptPassword = session.requestPasswordAuthentication(ipAddress, port, protocol, null, user);
230    
231                // if we were able to obtain new information from the session, then try again using the
232                // provided information .
233                if (promptPassword != null) {
234                    user = promptPassword.getUserName();
235                    password = promptPassword.getPassword();
236                }
237    
238                connected = protocolConnect(host, port, user, password);
239            }
240    
241    
242            // if we're still not connected, then this is an exception.
243            if (!connected) {
244                throw new AuthenticationFailedException();
245            }
246    
247            // the URL name needs to reflect the most recent information.
248            setURLName(new URLName(protocol, host, port, file, user, password));
249    
250            // we need to update the global password cache with this information.
251            if (cachePassword) {
252                session.setPasswordAuthentication(getURLName(), new PasswordAuthentication(user, password));
253            }
254    
255            // we're now connected....broadcast this to any interested parties.
256            setConnected(connected);
257            notifyConnectionListeners(ConnectionEvent.OPENED);
258        }
259    
260        /**
261         * Attempt the protocol-specific connection; subclasses should override this to establish
262         * a connection in the appropriate manner.
263         *
264         * This method should return true if the connection was established.
265         * It may return false to cause the {@link #connect(String, int, String, String)} method to
266         * reattempt the connection after trying to obtain user and password information from the user.
267         * Alternatively it may throw a AuthenticatedFailedException to abandon the conection attempt.
268         *
269         * @param host
270         * @param port
271         * @param user
272         * @param password
273         * @return
274         * @throws AuthenticationFailedException if authentication fails
275         * @throws MessagingException for other failures
276         */
277        protected boolean protocolConnect(String host, int port, String user, String password) throws MessagingException {
278            return false;
279        }
280    
281        /**
282         * Check if this service is currently connected.
283         * The default implementation simply returns the value of a private boolean field;
284         * subclasses may wish to override this method to verify the physical connection.
285         *
286         * @return true if this service is connected
287         */
288        public boolean isConnected() {
289            return connected;
290        }
291    
292        /**
293         * Notification to subclasses that the connection state has changed.
294         * This method is called by the connect() and close() methods to indicate state change;
295         * subclasses should also call this method if the connection is automatically closed
296         * for some reason.
297         *
298         * @param connected the connection state
299         */
300        protected void setConnected(boolean connected) {
301            this.connected = connected;
302        }
303    
304        /**
305         * Close this service and terminate its physical connection.
306         * The default implementation simply calls setConnected(false) and then
307         * sends a CLOSED event to all registered ConnectionListeners.
308         * Subclasses overriding this method should still ensure it is closed; they should
309         * also ensure that it is called if the connection is closed automatically, for
310         * for example in a finalizer.
311         *
312         *@throws MessagingException if there were errors closing; the connection is still closed
313         */
314        public void close() throws MessagingException {
315            setConnected(false);
316            notifyConnectionListeners(ConnectionEvent.CLOSED);
317        }
318    
319        /**
320         * Return a copy of the URLName representing this service with the password and file information removed.
321         *
322         * @return the URLName for this service
323         */
324        public URLName getURLName() {
325    
326            return url == null ? null : new URLName(url.getProtocol(), url.getHost(), url.getPort(), null, url.getUsername(), null);
327        }
328    
329        /**
330         * Set the url field.
331         * @param url the new value
332         */
333        protected void setURLName(URLName url) {
334            this.url = url;
335        }
336    
337        public void addConnectionListener(ConnectionListener listener) {
338            connectionListeners.add(listener);
339        }
340    
341        public void removeConnectionListener(ConnectionListener listener) {
342            connectionListeners.remove(listener);
343        }
344    
345        protected void notifyConnectionListeners(int type) {
346            queue.queueEvent(new ConnectionEvent(this, type), connectionListeners);
347        }
348    
349        public String toString() {
350            return url == null ? super.toString() : url.toString();
351        }
352    
353        protected void queueEvent(MailEvent event, Vector listeners) {
354            queue.queueEvent(event, listeners);
355        }
356    
357        protected void finalize() throws Throwable {
358            queue.stop();
359            connectionListeners.clear();
360            super.finalize();
361        }
362    
363    
364        /**
365         * Package scope utility method to allow Message instances
366         * access to the Service's session.
367         *
368         * @return The Session the service is associated with.
369         */
370        Session getSession() {
371            return session;
372        }
373    }