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 }