1 |
/* |
2 |
* Service.java |
3 |
* Copyright(C) 2002 The Free Software Foundation |
4 |
* |
5 |
* This file is part of GNU JavaMail, a library. |
6 |
* |
7 |
* GNU JavaMail is free software; you can redistribute it and/or modify |
8 |
* it under the terms of the GNU General Public License as published by |
9 |
* the Free Software Foundation; either version 2 of the License, or |
10 |
*(at your option) any later version. |
11 |
* |
12 |
* GNU JavaMail is distributed in the hope that it will be useful, |
13 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 |
* GNU General Public License for more details. |
16 |
* |
17 |
* You should have received a copy of the GNU General Public License |
18 |
* along with this library; if not, write to the Free Software |
19 |
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
20 |
* |
21 |
* As a special exception, if you link this library with other files to |
22 |
* produce an executable, this library does not by itself cause the |
23 |
* resulting executable to be covered by the GNU General Public License. |
24 |
* This exception does not however invalidate any other reasons why the |
25 |
* executable file might be covered by the GNU General Public License. |
26 |
*/ |
27 |
|
28 |
package javax.mail; |
29 |
|
30 |
import java.net.InetAddress; |
31 |
import java.net.UnknownHostException; |
32 |
import java.util.ArrayList; |
33 |
import javax.mail.event.ConnectionEvent; |
34 |
import javax.mail.event.ConnectionListener; |
35 |
import javax.mail.event.MailEvent; |
36 |
|
37 |
/** |
38 |
* An abstract class that contains the functionality common to messaging |
39 |
* services, such as stores and transports. |
40 |
* <p> |
41 |
* A messaging service is created from a Session and is named using a URLName. |
42 |
* A service must be connected before it can be used. |
43 |
* Connection events are sent to reflect its connection status. |
44 |
* |
45 |
* @author <a href="mailto:dog@gnu.org">Chris Burdess</a> |
46 |
* @version 1.3 |
47 |
*/ |
48 |
public abstract class Service |
49 |
{ |
50 |
|
51 |
/** |
52 |
* The session from which this service was created. |
53 |
*/ |
54 |
protected Session session; |
55 |
|
56 |
/** |
57 |
* The URLName of this service. |
58 |
*/ |
59 |
protected URLName url; |
60 |
|
61 |
/** |
62 |
* Debug flag for this service. |
63 |
* Set from the session's debug flag when this service is created. |
64 |
*/ |
65 |
protected boolean debug; |
66 |
|
67 |
/* |
68 |
* @see #isConnected |
69 |
*/ |
70 |
private boolean connected = false; |
71 |
|
72 |
private ArrayList connectionListeners = null; |
73 |
|
74 |
/** |
75 |
* Constructor. |
76 |
* @param session Session object for this service |
77 |
* @param url URLName object to be used for this service |
78 |
*/ |
79 |
protected Service(Session session, URLName url) |
80 |
{ |
81 |
this.session = session; |
82 |
this.url = url; |
83 |
debug = session.getDebug(); |
84 |
} |
85 |
|
86 |
/** |
87 |
* A generic connect method that takes no parameters. |
88 |
* Subclasses can implement the appropriate authentication schemes. |
89 |
* Subclasses that need additional information might want to use |
90 |
* some properties or might get it interactively using a popup window. |
91 |
* <p> |
92 |
* If the connection is successful, an "open" ConnectionEvent is delivered |
93 |
* to any ConnectionListeners on this service. |
94 |
* <p> |
95 |
* Most clients should just call this method to connect to the service. |
96 |
* <p> |
97 |
* It is an error to connect to an already connected service. |
98 |
* <p> |
99 |
* The implementation provided here simply calls the following |
100 |
* <code>connect(String, String, String)</code> method with nulls. |
101 |
* @exception AuthenticationFailedException for authentication failures |
102 |
* @exception MessagingException for other failures |
103 |
* @exception IllegalStateException if the service is already connected |
104 |
*/ |
105 |
public void connect() |
106 |
throws MessagingException |
107 |
{ |
108 |
connect(null, null, null); |
109 |
} |
110 |
|
111 |
/** |
112 |
* Connect to the specified address. |
113 |
* This method provides a simple authentication scheme |
114 |
* that requires a username and password. |
115 |
* <p> |
116 |
* If the connection is successful, an "open" ConnectionEvent is delivered |
117 |
* to any ConnectionListeners on this service. |
118 |
* <p> |
119 |
* It is an error to connect to an already connected service. |
120 |
* <p> |
121 |
* The implementation in the Service class will collect defaults for the |
122 |
* host, user, and password from the session, from the URLName for this |
123 |
* service, and from the supplied parameters and then call the |
124 |
* <code>protocolConnect</code> method. If the <code>protocolConnect</code> |
125 |
* method returns false, the user will be prompted for any missing |
126 |
* information and the <code>protocolConnect</code> method will be called |
127 |
* again. The subclass should override the <code>protocolConnect</code> |
128 |
* method. The subclass should also implement the <code>getURLName</code> |
129 |
* method, or use the implementation in this class. |
130 |
* <p> |
131 |
* On a successful connection, the <code>setURLName</code> method is called |
132 |
* with a URLName that includes the information used to make the connection, |
133 |
* including the password. |
134 |
* <p> |
135 |
* If the password passed in is null and this is the first successful |
136 |
* connection to this service, the user name and the password collected from |
137 |
* the user will be saved as defaults for subsequent connection attempts to |
138 |
* this same service when using other Service object instances(the |
139 |
* connection information is typically always saved within a particular |
140 |
* Service object instance). The password is saved using the Session method |
141 |
* <code>setPasswordAuthenticaiton</code>. If the password passed in is not |
142 |
* null, it is not saved, on the assumption that the application is managing |
143 |
* passwords explicitly. |
144 |
* @param host the host to connect to |
145 |
* @param user the user name |
146 |
* @param password this user's password |
147 |
* @exception AuthenticationFailedException for authentication failures |
148 |
* @exception MessagingException for other failures |
149 |
* @exception IllegalStateException if the service is already connected |
150 |
*/ |
151 |
public void connect(String host, String user, String password) |
152 |
throws MessagingException |
153 |
{ |
154 |
connect(host, -1, user, password); |
155 |
} |
156 |
|
157 |
/** |
158 |
* Similar to connect(host, user, password) except a specific port can be |
159 |
* specified. |
160 |
* @param host the host to connect to |
161 |
* @param port the port to use(-1 means use default port) |
162 |
* @param user the user name |
163 |
* @param password this user's password |
164 |
* @exception AuthenticationFailedException for authentication failures |
165 |
* @exception MessagingException for other failures |
166 |
* @exception IllegalStateException if the service is already connected |
167 |
*/ |
168 |
public void connect(String host, int port, String user, String password) |
169 |
throws MessagingException |
170 |
{ |
171 |
if (isConnected()) |
172 |
{ |
173 |
throw new MessagingException("already connected"); |
174 |
} |
175 |
|
176 |
boolean success = false; |
177 |
boolean authenticated = false; |
178 |
String protocol = null; |
179 |
String file = null; |
180 |
if (url != null) |
181 |
{ |
182 |
protocol = url.getProtocol(); |
183 |
if (host == null) |
184 |
{ |
185 |
host = url.getHost(); |
186 |
} |
187 |
if (port == -1) |
188 |
{ |
189 |
port = url.getPort(); |
190 |
} |
191 |
if (user == null) |
192 |
{ |
193 |
user = url.getUsername(); |
194 |
if (password == null) |
195 |
{ |
196 |
password = url.getPassword(); |
197 |
} |
198 |
} |
199 |
else if (password == null && user.equals(url.getUsername())) |
200 |
{ |
201 |
password = url.getPassword(); |
202 |
} |
203 |
file = url.getFile(); |
204 |
} |
205 |
if (protocol != null) |
206 |
{ |
207 |
if (host == null) |
208 |
{ |
209 |
host = session.getProperty("mail." + protocol + ".host"); |
210 |
} |
211 |
if (user == null) |
212 |
{ |
213 |
user = session.getProperty("mail." + protocol + ".user"); |
214 |
} |
215 |
} |
216 |
if (host == null) |
217 |
{ |
218 |
host = session.getProperty("mail.host"); |
219 |
} |
220 |
if (user == null) |
221 |
{ |
222 |
user = session.getProperty("mail.user"); |
223 |
} |
224 |
if (user == null) |
225 |
{ |
226 |
try |
227 |
{ |
228 |
user = System.getProperty("user.name"); |
229 |
} |
230 |
catch (SecurityException e) |
231 |
{ |
232 |
if (debug) |
233 |
{ |
234 |
e.printStackTrace(); |
235 |
} |
236 |
} |
237 |
} |
238 |
if (password == null && url != null) |
239 |
{ |
240 |
setURLName(new URLName(protocol, host, port, file, user, password)); |
241 |
PasswordAuthentication auth = |
242 |
session.getPasswordAuthentication(getURLName()); |
243 |
if (auth != null) |
244 |
{ |
245 |
if (user == null) |
246 |
{ |
247 |
user = auth.getUserName(); |
248 |
password = auth.getPassword(); |
249 |
} |
250 |
else if (user.equals(auth.getUserName())) |
251 |
{ |
252 |
password = auth.getPassword(); |
253 |
} |
254 |
} |
255 |
else |
256 |
{ |
257 |
authenticated = true; |
258 |
} |
259 |
} |
260 |
AuthenticationFailedException afex = null; |
261 |
try |
262 |
{ |
263 |
success = protocolConnect(host, port, user, password); |
264 |
} |
265 |
catch (AuthenticationFailedException afex2) |
266 |
{ |
267 |
afex = afex2; |
268 |
} |
269 |
if (!success) |
270 |
{ |
271 |
InetAddress address = null; |
272 |
try |
273 |
{ |
274 |
address = InetAddress.getByName(host); |
275 |
} |
276 |
catch (UnknownHostException e) |
277 |
{ |
278 |
} |
279 |
PasswordAuthentication auth = |
280 |
session.requestPasswordAuthentication(address, port, protocol, |
281 |
null, user); |
282 |
if (auth != null) |
283 |
{ |
284 |
user = auth.getUserName(); |
285 |
password = auth.getPassword(); |
286 |
success = protocolConnect(host, port, user, password); |
287 |
} |
288 |
} |
289 |
if (!success) |
290 |
{ |
291 |
if (afex!=null) |
292 |
{ |
293 |
throw afex; |
294 |
} |
295 |
throw new AuthenticationFailedException(); |
296 |
} |
297 |
setURLName(new URLName(protocol, host, port, file, user, password)); |
298 |
if (authenticated) |
299 |
{ |
300 |
PasswordAuthentication auth = |
301 |
new PasswordAuthentication(user, password); |
302 |
session.setPasswordAuthentication(getURLName(), auth); |
303 |
} |
304 |
setConnected(true); |
305 |
notifyConnectionListeners(ConnectionEvent.OPENED); |
306 |
} |
307 |
|
308 |
/** |
309 |
* The service implementation should override this method to perform the |
310 |
* actual protocol-specific connection attempt. |
311 |
* The default implementation of the <code>connect</code> method calls |
312 |
* this method as needed. |
313 |
* <p> |
314 |
* The <code>protocolConnect</code> method should return false if a user |
315 |
* name or password is required for authentication but the corresponding |
316 |
* parameter is null; the connect method will prompt the user when needed |
317 |
* to supply missing information. This method may also return false if |
318 |
* authentication fails for the supplied user name or password. |
319 |
* Alternatively, this method may throw an AuthenticationFailedException |
320 |
* when authentication fails. This exception may include a String message |
321 |
* with more detail about the failure. |
322 |
* <p> |
323 |
* The <code>protocolConnect</code> method should throw an exception to |
324 |
* report failures not related to authentication, such as an invalid host |
325 |
* name or port number, loss of a connection during the authentication |
326 |
* process, unavailability of the server, etc. |
327 |
* @param host the name of the host to connect to |
328 |
* @param port the port to use(-1 means use default port) |
329 |
* @param user the name of the user to login as |
330 |
* @param password the user's password |
331 |
* @return true if connection successful, false if authentication failed |
332 |
* @exception AuthenticationFailedException for authentication failures |
333 |
* @exception MessagingException for non-authentication failures |
334 |
*/ |
335 |
protected boolean protocolConnect(String host, int port, |
336 |
String user, String password) |
337 |
throws MessagingException |
338 |
{ |
339 |
return false; |
340 |
} |
341 |
|
342 |
/** |
343 |
* Is this service currently connected? |
344 |
* <p> |
345 |
* This implementation uses a private boolean field to store the connection |
346 |
* state. This method returns the value of that field. |
347 |
* <p> |
348 |
* Subclasses may want to override this method to verify that any connection |
349 |
* to the message store is still alive. |
350 |
*/ |
351 |
public boolean isConnected() |
352 |
{ |
353 |
return connected; |
354 |
} |
355 |
|
356 |
/** |
357 |
* Set the connection state of this service. |
358 |
* The connection state will automatically be set by the service |
359 |
* implementation during the connect and close methods. |
360 |
* Subclasses will need to call this method to set the state if |
361 |
* the service was automatically disconnected. |
362 |
* <p> |
363 |
* The implementation in this class merely sets the private field |
364 |
* returned by the <code>isConnected</code> method. |
365 |
*/ |
366 |
protected void setConnected(boolean connected) |
367 |
{ |
368 |
this.connected = connected; |
369 |
} |
370 |
|
371 |
/** |
372 |
* Close this service and terminate its connection. |
373 |
* A close ConnectionEvent is delivered to any ConnectionListeners. |
374 |
* Any Messaging components(Folders, Messages, etc.) belonging to |
375 |
* this service are invalid after this service is closed. Note that |
376 |
* the service is closed even if this method terminates abnormally |
377 |
* by throwing a MessagingException. |
378 |
* <p> |
379 |
* This implementation uses <code>setConnected(false)</code> to set |
380 |
* this service's connected state to false. It will then send a close |
381 |
* ConnectionEvent to any registered ConnectionListeners. Subclasses |
382 |
* overriding this method to do implementation specific cleanup should |
383 |
* call this method as a last step to insure event notification, |
384 |
* probably by including a call to <code>super.close()</code> in |
385 |
* a finally clause. |
386 |
*/ |
387 |
public synchronized void close() |
388 |
throws MessagingException |
389 |
{ |
390 |
setConnected(false); |
391 |
notifyConnectionListeners(ConnectionEvent.CLOSED); |
392 |
} |
393 |
|
394 |
/** |
395 |
* Return a URLName representing this service. |
396 |
* The returned URLName does not include the password field. |
397 |
* <p> |
398 |
* Subclasses should only override this method if their URLName does not |
399 |
* follow the standard format. |
400 |
* <p> |
401 |
* The implementation in the Service class returns(usually a copy of) |
402 |
* the url field with the password and file information stripped out. |
403 |
*/ |
404 |
public URLName getURLName() |
405 |
{ |
406 |
if (url != null &&(url.getPassword() != null || url.getFile() != null)) |
407 |
{ |
408 |
return new URLName(url.getProtocol(), url.getHost(), url.getPort(), |
409 |
null, url.getUsername(), null); |
410 |
} |
411 |
return url; |
412 |
} |
413 |
|
414 |
/** |
415 |
* Set the URLName representing this service. |
416 |
* Normally used to update the url field after a service has |
417 |
* successfully connected. |
418 |
* <p> |
419 |
* Subclasses should only override this method if their URL does not |
420 |
* follow the standard format. In particular, subclasses should override |
421 |
* this method if their URL does not require all the possible fields |
422 |
* supported by URLName; a new URLName should be constructed with any |
423 |
* unneeded fields removed. |
424 |
* <p> |
425 |
* The implementation in the Service class simply sets the url field. |
426 |
*/ |
427 |
protected void setURLName(URLName url) |
428 |
{ |
429 |
this.url = url; |
430 |
} |
431 |
|
432 |
// -- Event management -- |
433 |
|
434 |
/* |
435 |
* Because the propagation of events of different kinds in the JavaMail |
436 |
* API is so haphazard, I have here sacrificed a small time advantage for |
437 |
* readability and consistency. |
438 |
* |
439 |
* All the various propagation methods now call a method with a name based |
440 |
* on the eventual listener method name prefixed by 'fire', as is the |
441 |
* preferred pattern for usage of the EventListenerList in Swing. |
442 |
* |
443 |
* Note that all events are currently delivered synchronously, where in |
444 |
* Sun's implementation a different thread is used for event delivery. |
445 |
* |
446 |
* TODO Examine the impact of this. |
447 |
*/ |
448 |
|
449 |
// -- Connection events -- |
450 |
|
451 |
/** |
452 |
* Add a listener for Connection events on this service. |
453 |
*/ |
454 |
public void addConnectionListener(ConnectionListener l) |
455 |
{ |
456 |
if (connectionListeners == null) |
457 |
{ |
458 |
connectionListeners = new ArrayList(); |
459 |
} |
460 |
synchronized (connectionListeners) |
461 |
{ |
462 |
connectionListeners.add(l); |
463 |
} |
464 |
} |
465 |
|
466 |
/** |
467 |
* Remove a Connection event listener. |
468 |
*/ |
469 |
public void removeConnectionListener(ConnectionListener l) |
470 |
{ |
471 |
if (connectionListeners != null) |
472 |
{ |
473 |
synchronized (connectionListeners) |
474 |
{ |
475 |
connectionListeners.remove(l); |
476 |
} |
477 |
} |
478 |
} |
479 |
|
480 |
/** |
481 |
* Notify all ConnectionListeners. |
482 |
* Service implementations are expected to use this method |
483 |
* to broadcast connection events. |
484 |
*/ |
485 |
protected void notifyConnectionListeners(int type) |
486 |
{ |
487 |
ConnectionEvent event = new ConnectionEvent(this, type); |
488 |
switch (type) |
489 |
{ |
490 |
case ConnectionEvent.OPENED: |
491 |
fireOpened(event); |
492 |
break; |
493 |
case ConnectionEvent.DISCONNECTED: |
494 |
fireDisconnected(event); |
495 |
break; |
496 |
case ConnectionEvent.CLOSED: |
497 |
fireClosed(event); |
498 |
break; |
499 |
} |
500 |
} |
501 |
|
502 |
/* |
503 |
* Propagates an OPENED ConnectionEvent to all registered listeners. |
504 |
*/ |
505 |
void fireOpened(ConnectionEvent event) |
506 |
{ |
507 |
if (connectionListeners != null) |
508 |
{ |
509 |
ConnectionListener[] l = null; |
510 |
synchronized (connectionListeners) |
511 |
{ |
512 |
l = new ConnectionListener[connectionListeners.size()]; |
513 |
connectionListeners.toArray(l); |
514 |
} |
515 |
for (int i = 0; i < l.length; i++) |
516 |
{ |
517 |
l[i].opened(event); |
518 |
} |
519 |
} |
520 |
} |
521 |
|
522 |
/* |
523 |
* Propagates a DISCONNECTED ConnectionEvent to all registered listeners. |
524 |
*/ |
525 |
void fireDisconnected(ConnectionEvent event) |
526 |
{ |
527 |
if (connectionListeners != null) |
528 |
{ |
529 |
ConnectionListener[] l = null; |
530 |
synchronized (connectionListeners) |
531 |
{ |
532 |
l = new ConnectionListener[connectionListeners.size()]; |
533 |
connectionListeners.toArray(l); |
534 |
} |
535 |
for (int i = 0; i < l.length; i++) |
536 |
{ |
537 |
l[i].disconnected(event); |
538 |
} |
539 |
} |
540 |
} |
541 |
|
542 |
/* |
543 |
* Propagates a CLOSED ConnectionEvent to all registered listeners. |
544 |
*/ |
545 |
void fireClosed(ConnectionEvent event) |
546 |
{ |
547 |
if (connectionListeners != null) |
548 |
{ |
549 |
ConnectionListener[] l = null; |
550 |
synchronized (connectionListeners) |
551 |
{ |
552 |
l = new ConnectionListener[connectionListeners.size()]; |
553 |
connectionListeners.toArray(l); |
554 |
} |
555 |
for (int i = 0; i < l.length; i++) |
556 |
{ |
557 |
l[i].closed(event); |
558 |
} |
559 |
} |
560 |
} |
561 |
|
562 |
/** |
563 |
* Return getURLName.toString() if this service has a URLName, |
564 |
* otherwise it will return the default toString. |
565 |
*/ |
566 |
public String toString() |
567 |
{ |
568 |
URLName urlName = getURLName(); |
569 |
return (urlName != null) ? urlName.toString() : super.toString(); |
570 |
} |
571 |
|
572 |
} |