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