1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 20 package javax.mail; 21 22 import java.net.InetAddress; 23 import java.net.UnknownHostException; 24 import java.util.List; 25 import java.util.Vector; 26 27 import javax.mail.event.ConnectionEvent; 28 import javax.mail.event.ConnectionListener; 29 import javax.mail.event.MailEvent; 30 31 /** 32 * @version $Rev: 597092 $ $Date: 2007-11-21 09:02:38 -0500 (Wed, 21 Nov 2007) $ 33 */ 34 public abstract class Service { 35 /** 36 * The session from which this service was created. 37 */ 38 protected Session session; 39 /** 40 * The URLName of this service 41 */ 42 protected URLName url; 43 /** 44 * Debug flag for this service, set from the Session's debug flag. 45 */ 46 protected boolean debug; 47 48 private boolean connected; 49 private final Vector connectionListeners = new Vector(2); 50 // the EventQueue spins off a new thread, so we only create this 51 // if we have actual listeners to dispatch an event to. 52 private EventQueue queue = null; 53 // when returning the URL, we need to ensure that the password and file information is 54 // stripped out. 55 private URLName exposedUrl; 56 57 /** 58 * Construct a new Service. 59 * @param session the session from which this service was created 60 * @param url the URLName of this service 61 */ 62 protected Service(Session session, URLName url) { 63 this.session = session; 64 this.url = url; 65 this.debug = session.getDebug(); 66 } 67 68 /** 69 * A generic connect method that takes no parameters allowing subclasses 70 * to implement an appropriate authentication scheme. 71 * The default implementation calls <code>connect(null, null, null)</code> 72 * @throws AuthenticationFailedException if authentication fails 73 * @throws MessagingException for other failures 74 */ 75 public void connect() throws MessagingException { 76 connect(null, null, null); 77 } 78 79 /** 80 * Connect to the specified host using a simple username/password authenticaion scheme 81 * and the default port. 82 * The default implementation calls <code>connect(host, -1, user, password)</code> 83 * 84 * @param host the host to connect to 85 * @param user the user name 86 * @param password the user's password 87 * @throws AuthenticationFailedException if authentication fails 88 * @throws MessagingException for other failures 89 */ 90 public void connect(String host, String user, String password) throws MessagingException { 91 connect(host, -1, user, password); 92 } 93 94 /** 95 * Connect to the specified host using a simple username/password authenticaion scheme 96 * and the default host and port. 97 * The default implementation calls <code>connect(host, -1, user, password)</code> 98 * 99 * @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 // user still null? We have several levels of properties to try yet 178 if (user == null) { 179 if (protocol != null) { 180 user = session.getProperty("mail." + protocol + ".user"); 181 } 182 } 183 } 184 185 // this may still be null...get the global mail property 186 if (user == null) { 187 user = session.getProperty("mail.user"); 188 } 189 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 // if we have an explicitly given user name, we need to see if this matches the url one and 198 // grab the password from there. 199 else { 200 if (url != null && user.equals(url.getUsername())) { 201 password = url.getPassword(); 202 } 203 } 204 205 // we need to update the URLName associated with this connection once we have all of the information, 206 // which means we also need to propogate the file portion of the URLName if we have this form when 207 // we start. 208 String file = null; 209 if (url != null) { 210 file = url.getFile(); 211 } 212 213 // see if we have cached security information to use. If this is not cached, we'll save it 214 // after we successfully connect. 215 boolean cachePassword = false; 216 217 218 // still have a null password to this point, and using a url form? 219 if (password == null && url != null) { 220 // construct a new URL, filling in any pieces that may have been explicitly specified. 221 setURLName(new URLName(protocol, host, port, file, user, password)); 222 // now see if we have a saved password from a previous request. 223 PasswordAuthentication cachedPassword = session.getPasswordAuthentication(getURLName()); 224 225 // if we found a saved one, see if we need to get any the pieces from here. 226 if (cachedPassword != null) { 227 // not even a resolved userid? Then use both bits. 228 if (user == null) { 229 user = cachedPassword.getUserName(); 230 password = cachedPassword.getPassword(); 231 } 232 // our user name must match the cached name to be valid. 233 else if (user.equals(cachedPassword.getUserName())) { 234 password = cachedPassword.getPassword(); 235 } 236 } 237 else 238 { 239 // nothing found in the cache, so we need to save this if we can connect successfully. 240 cachePassword = true; 241 } 242 } 243 244 // we've done our best up to this point to obtain all of the information needed to make the 245 // connection. Now we pass this off to the protocol handler to see if it works. If we get a 246 // connection failure, we may need to prompt for a password before continuing. 247 try { 248 connected = protocolConnect(host, port, user, password); 249 } 250 catch (AuthenticationFailedException e) { 251 } 252 253 if (!connected) { 254 InetAddress ipAddress = null; 255 256 try { 257 ipAddress = InetAddress.getByName(host); 258 } catch (UnknownHostException e) { 259 } 260 261 // now ask the session to try prompting for a password. 262 PasswordAuthentication promptPassword = session.requestPasswordAuthentication(ipAddress, port, protocol, null, user); 263 264 // if we were able to obtain new information from the session, then try again using the 265 // provided information . 266 if (promptPassword != null) { 267 user = promptPassword.getUserName(); 268 password = promptPassword.getPassword(); 269 } 270 271 connected = protocolConnect(host, port, user, password); 272 } 273 274 275 // if we're still not connected, then this is an exception. 276 if (!connected) { 277 throw new AuthenticationFailedException(); 278 } 279 280 // the URL name needs to reflect the most recent information. 281 setURLName(new URLName(protocol, host, port, file, user, password)); 282 283 // we need to update the global password cache with this information. 284 if (cachePassword) { 285 session.setPasswordAuthentication(getURLName(), new PasswordAuthentication(user, password)); 286 } 287 288 // we're now connected....broadcast this to any interested parties. 289 setConnected(connected); 290 notifyConnectionListeners(ConnectionEvent.OPENED); 291 } 292 293 /** 294 * Attempt the protocol-specific connection; subclasses should override this to establish 295 * a connection in the appropriate manner. 296 * 297 * This method should return true if the connection was established. 298 * It may return false to cause the {@link #connect(String, int, String, String)} method to 299 * reattempt the connection after trying to obtain user and password information from the user. 300 * Alternatively it may throw a AuthenticatedFailedException to abandon the conection attempt. 301 * 302 * @param host The target host name of the service. 303 * @param port The connection port for the service. 304 * @param user The user name used for the connection. 305 * @param password The password used for the connection. 306 * 307 * @return true if a connection was established, false if there was authentication 308 * error with the connection. 309 * @throws AuthenticationFailedException 310 * if authentication fails 311 * @throws MessagingException 312 * for other failures 313 */ 314 protected boolean protocolConnect(String host, int port, String user, String password) throws MessagingException { 315 return false; 316 } 317 318 /** 319 * Check if this service is currently connected. 320 * The default implementation simply returns the value of a private boolean field; 321 * subclasses may wish to override this method to verify the physical connection. 322 * 323 * @return true if this service is connected 324 */ 325 public boolean isConnected() { 326 return connected; 327 } 328 329 /** 330 * Notification to subclasses that the connection state has changed. 331 * This method is called by the connect() and close() methods to indicate state change; 332 * subclasses should also call this method if the connection is automatically closed 333 * for some reason. 334 * 335 * @param connected the connection state 336 */ 337 protected void setConnected(boolean connected) { 338 this.connected = connected; 339 } 340 341 /** 342 * Close this service and terminate its physical connection. 343 * The default implementation simply calls setConnected(false) and then 344 * sends a CLOSED event to all registered ConnectionListeners. 345 * Subclasses overriding this method should still ensure it is closed; they should 346 * also ensure that it is called if the connection is closed automatically, for 347 * for example in a finalizer. 348 * 349 *@throws MessagingException if there were errors closing; the connection is still closed 350 */ 351 public void close() throws MessagingException { 352 setConnected(false); 353 notifyConnectionListeners(ConnectionEvent.CLOSED); 354 } 355 356 /** 357 * Return a copy of the URLName representing this service with the password and file information removed. 358 * 359 * @return the URLName for this service 360 */ 361 public URLName getURLName() { 362 // if we haven't composed the URL version we hand out, create it now. But only if we really 363 // have a URL. 364 if (exposedUrl == null) { 365 if (url != null) { 366 exposedUrl = new URLName(url.getProtocol(), url.getHost(), url.getPort(), null, url.getUsername(), null); 367 } 368 } 369 return exposedUrl; 370 } 371 372 /** 373 * Set the url field. 374 * @param url the new value 375 */ 376 protected void setURLName(URLName url) { 377 this.url = url; 378 } 379 380 public void addConnectionListener(ConnectionListener listener) { 381 connectionListeners.add(listener); 382 } 383 384 public void removeConnectionListener(ConnectionListener listener) { 385 connectionListeners.remove(listener); 386 } 387 388 protected void notifyConnectionListeners(int type) { 389 queueEvent(new ConnectionEvent(this, type), connectionListeners); 390 } 391 392 public String toString() { 393 // NOTE: We call getURLName() rather than use the URL directly 394 // because the get method strips out the password information. 395 URLName url = getURLName(); 396 397 return url == null ? super.toString() : url.toString(); 398 } 399 400 protected void queueEvent(MailEvent event, Vector listeners) { 401 // if there are no listeners to dispatch this to, don't put it on the queue. 402 // This allows us to delay creating the queue (and its new thread) until 403 // we 404 if (listeners.isEmpty()) { 405 return; 406 } 407 // first real event? Time to get the queue kicked off. 408 if (queue == null) { 409 queue = new EventQueue(); 410 } 411 // tee it up and let it rip. 412 queue.queueEvent(event, (List)listeners.clone()); 413 } 414 415 protected void finalize() throws Throwable { 416 // stop our event queue if we had to create one 417 if (queue != null) { 418 queue.stop(); 419 } 420 connectionListeners.clear(); 421 super.finalize(); 422 } 423 424 425 /** 426 * Package scope utility method to allow Message instances 427 * access to the Service's session. 428 * 429 * @return The Session the service is associated with. 430 */ 431 Session getSession() { 432 return session; 433 } 434 }