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 }