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 }