001    /**
002     *
003     * Copyright 2003-2006 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: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 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 using a simple username/password authenticaion scheme
088         * and the default host and port.
089         * The default implementation calls <code>connect(host, -1, user, password)</code>
090         *
091         * @param user the user name
092         * @param password the user's password
093         * @throws AuthenticationFailedException if authentication fails
094         * @throws MessagingException for other failures
095         */
096        public void connect(String user, String password) throws MessagingException {
097            connect(null, -1, user, password);
098        }
099    
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    }