1 |
/* |
2 |
* SMTPConnection.java |
3 |
* Copyright (C) 2003 Chris Burdess <dog@gnu.org> |
4 |
* |
5 |
* This file is part of GNU inetlib, a library. |
6 |
* |
7 |
* GNU inetlib 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 inetlib 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
20 |
* |
21 |
* Linking this library statically or dynamically with other modules is |
22 |
* making a combined work based on this library. Thus, the terms and |
23 |
* conditions of the GNU General Public License cover the whole |
24 |
* combination. |
25 |
* |
26 |
* As a special exception, the copyright holders of this library give you |
27 |
* permission to link this library with independent modules to produce an |
28 |
* executable, regardless of the license terms of these independent |
29 |
* modules, and to copy and distribute the resulting executable under |
30 |
* terms of your choice, provided that you also meet, for each linked |
31 |
* independent module, the terms and conditions of the license of that |
32 |
* module. An independent module is a module which is not derived from |
33 |
* or based on this library. If you modify this library, you may extend |
34 |
* this exception to your version of the library, but you are not |
35 |
* obliged to do so. If you do not wish to do so, delete this |
36 |
* exception statement from your version. |
37 |
*/ |
38 |
|
39 |
package gnu.inet.smtp; |
40 |
|
41 |
import java.io.BufferedInputStream; |
42 |
import java.io.BufferedOutputStream; |
43 |
import java.io.InputStream; |
44 |
import java.io.IOException; |
45 |
import java.io.OutputStream; |
46 |
import java.net.InetSocketAddress; |
47 |
import java.net.ProtocolException; |
48 |
import java.net.Socket; |
49 |
import java.security.GeneralSecurityException; |
50 |
import java.util.ArrayList; |
51 |
import java.util.Collections; |
52 |
import java.util.HashMap; |
53 |
import java.util.List; |
54 |
import java.util.logging.Level; |
55 |
import java.util.logging.Logger; |
56 |
|
57 |
import javax.net.ssl.SSLContext; |
58 |
import javax.net.ssl.SSLSocket; |
59 |
import javax.net.ssl.SSLSocketFactory; |
60 |
import javax.net.ssl.TrustManager; |
61 |
|
62 |
import javax.security.auth.callback.CallbackHandler; |
63 |
import javax.security.sasl.Sasl; |
64 |
import javax.security.sasl.SaslClient; |
65 |
import javax.security.sasl.SaslException; |
66 |
|
67 |
import gnu.inet.util.BASE64; |
68 |
import gnu.inet.util.CRLFInputStream; |
69 |
import gnu.inet.util.EmptyX509TrustManager; |
70 |
import gnu.inet.util.LineInputStream; |
71 |
import gnu.inet.util.MessageOutputStream; |
72 |
import gnu.inet.util.SaslCallbackHandler; |
73 |
import gnu.inet.util.SaslCramMD5; |
74 |
import gnu.inet.util.SaslInputStream; |
75 |
import gnu.inet.util.SaslLogin; |
76 |
import gnu.inet.util.SaslOutputStream; |
77 |
import gnu.inet.util.SaslPlain; |
78 |
import gnu.inet.util.TraceLevel; |
79 |
|
80 |
/** |
81 |
* An SMTP client. |
82 |
* This implements RFC 2821. |
83 |
* |
84 |
* @author <a href="mailto:dog@gnu.org">Chris Burdess</a> |
85 |
*/ |
86 |
public class SMTPConnection |
87 |
{ |
88 |
|
89 |
/** |
90 |
* The network trace level. |
91 |
*/ |
92 |
public static final Level SMTP_TRACE = new TraceLevel("smtp"); |
93 |
|
94 |
/** |
95 |
* The default SMTP port. |
96 |
*/ |
97 |
public static final int DEFAULT_PORT = 25; |
98 |
|
99 |
protected static final String MAIL_FROM = "MAIL FROM:"; |
100 |
protected static final String RCPT_TO = "RCPT TO:"; |
101 |
protected static final String SP = " "; |
102 |
protected static final String DATA = "DATA"; |
103 |
protected static final String FINISH_DATA = "\n."; |
104 |
protected static final String RSET = "RSET"; |
105 |
protected static final String VRFY = "VRFY"; |
106 |
protected static final String EXPN = "EXPN"; |
107 |
protected static final String HELP = "HELP"; |
108 |
protected static final String NOOP = "NOOP"; |
109 |
protected static final String QUIT = "QUIT"; |
110 |
protected static final String HELO = "HELO"; |
111 |
protected static final String EHLO = "EHLO"; |
112 |
protected static final String AUTH = "AUTH"; |
113 |
protected static final String STARTTLS = "STARTTLS"; |
114 |
|
115 |
protected static final int INFO = 214; |
116 |
protected static final int READY = 220; |
117 |
protected static final int OK = 250; |
118 |
protected static final int OK_NOT_LOCAL = 251; |
119 |
protected static final int OK_UNVERIFIED = 252; |
120 |
protected static final int SEND_DATA = 354; |
121 |
protected static final int AMBIGUOUS = 553; |
122 |
|
123 |
/** |
124 |
* The logger used for SMTP protocol traces. |
125 |
*/ |
126 |
public final Logger logger = Logger.getLogger("gnu.inet.smtp"); |
127 |
|
128 |
/** |
129 |
* The underlying socket used for communicating with the server. |
130 |
*/ |
131 |
protected Socket socket; |
132 |
|
133 |
/** |
134 |
* The input stream used to read responses from the server. |
135 |
*/ |
136 |
protected LineInputStream in; |
137 |
|
138 |
/** |
139 |
* The output stream used to send commands to the server. |
140 |
*/ |
141 |
protected SMTPOutputStream out; |
142 |
|
143 |
/** |
144 |
* The last response message received from the server. |
145 |
*/ |
146 |
protected String response; |
147 |
|
148 |
/** |
149 |
* If true, there are more responses to read. |
150 |
*/ |
151 |
protected boolean continuation; |
152 |
|
153 |
/** |
154 |
* The greeting message given by the server. |
155 |
*/ |
156 |
protected String greeting; |
157 |
|
158 |
/** |
159 |
* Creates a new connection to the specified host, using the default SMTP |
160 |
* port. |
161 |
* @param host the server hostname |
162 |
*/ |
163 |
public SMTPConnection(String host) |
164 |
throws IOException |
165 |
{ |
166 |
this(host, DEFAULT_PORT, 0, 0, false, null); |
167 |
} |
168 |
|
169 |
/** |
170 |
* Creates a new connection to the specified host, using the specified |
171 |
* port. |
172 |
* @param host the server hostname |
173 |
* @param port the port to connect to |
174 |
*/ |
175 |
public SMTPConnection(String host, int port) |
176 |
throws IOException |
177 |
{ |
178 |
this(host, port, 0, 0, false, null); |
179 |
} |
180 |
|
181 |
/** |
182 |
* Creates a new connection to the specified host, using the specified |
183 |
* port. |
184 |
* @param host the server hostname |
185 |
* @param port the port to connect to |
186 |
* @param connectionTimeout the connection timeout in milliseconds |
187 |
* @param timeout the I/O timeout in milliseconds |
188 |
*/ |
189 |
public SMTPConnection(String host, int port, |
190 |
int connectionTimeout, int timeout) |
191 |
throws IOException |
192 |
{ |
193 |
this(host, port, connectionTimeout, timeout, false, null); |
194 |
} |
195 |
|
196 |
/** |
197 |
* Creates a new connection to the specified host, using the specified |
198 |
* port. |
199 |
* @param host the server hostname |
200 |
* @param port the port to connect to |
201 |
* @param connectionTimeout the connection timeout in milliseconds |
202 |
* @param timeout the I/O timeout in milliseconds |
203 |
* @param secure true to create an SMTPS connection |
204 |
* @param tm a trust manager used to check SSL certificates, or null to |
205 |
* use the default |
206 |
*/ |
207 |
public SMTPConnection(String host, int port, |
208 |
int connectionTimeout, int timeout, |
209 |
boolean secure, TrustManager tm) |
210 |
throws IOException |
211 |
{ |
212 |
this(host, port, connectionTimeout, timeout, secure, tm, true); |
213 |
} |
214 |
|
215 |
/** |
216 |
* Creates a new connection to the specified host, using the specified |
217 |
* port. |
218 |
* @param host the server hostname |
219 |
* @param port the port to connect to |
220 |
* @param connectionTimeout the connection timeout in milliseconds |
221 |
* @param timeout the I/O timeout in milliseconds |
222 |
* @param secure true to create an SMTPS connection |
223 |
* @param tm a trust manager used to check SSL certificates, or null to |
224 |
* use the default |
225 |
* @param init initialise the connection |
226 |
*/ |
227 |
public SMTPConnection(String host, int port, |
228 |
int connectionTimeout, int timeout, |
229 |
boolean secure, TrustManager tm, boolean init) |
230 |
throws IOException |
231 |
{ |
232 |
if (port <= 0) |
233 |
{ |
234 |
port = DEFAULT_PORT; |
235 |
} |
236 |
response = null; |
237 |
continuation = false; |
238 |
|
239 |
// Initialise socket |
240 |
try |
241 |
{ |
242 |
socket = new Socket(); |
243 |
InetSocketAddress address = new InetSocketAddress(host, port); |
244 |
if (connectionTimeout > 0) |
245 |
{ |
246 |
socket.connect(address, connectionTimeout); |
247 |
} |
248 |
else |
249 |
{ |
250 |
socket.connect(address); |
251 |
} |
252 |
if (timeout > 0) |
253 |
{ |
254 |
socket.setSoTimeout(timeout); |
255 |
} |
256 |
if (secure) |
257 |
{ |
258 |
SSLSocketFactory factory = getSSLSocketFactory(tm); |
259 |
SSLSocket ss = |
260 |
(SSLSocket) factory.createSocket(socket, host, port, true); |
261 |
String[] protocols = { "TLSv1", "SSLv3" }; |
262 |
ss.setEnabledProtocols(protocols); |
263 |
ss.setUseClientMode(true); |
264 |
ss.startHandshake(); |
265 |
socket = ss; |
266 |
} |
267 |
} |
268 |
catch (GeneralSecurityException e) |
269 |
{ |
270 |
IOException e2 = new IOException(); |
271 |
e2.initCause(e); |
272 |
throw e2; |
273 |
} |
274 |
|
275 |
// Initialise streams |
276 |
InputStream is = socket.getInputStream(); |
277 |
is = new BufferedInputStream(is); |
278 |
is = new CRLFInputStream(is); |
279 |
in = new LineInputStream(is); |
280 |
OutputStream os = socket.getOutputStream(); |
281 |
os = new BufferedOutputStream(os); |
282 |
out = new SMTPOutputStream(os); |
283 |
|
284 |
if (init) |
285 |
init(); |
286 |
} |
287 |
|
288 |
/** |
289 |
* Initialises the connection. |
290 |
* Unless the init parameter was provided with the value false, |
291 |
* do not call this method. Otherwise call it only once after e.g. |
292 |
* configuring logging. |
293 |
*/ |
294 |
public void init() |
295 |
throws IOException |
296 |
{ |
297 |
// Greeting |
298 |
StringBuffer greetingBuffer = new StringBuffer(); |
299 |
boolean notFirst = false; |
300 |
do |
301 |
{ |
302 |
if (getResponse() != READY) |
303 |
{ |
304 |
throw new ProtocolException(response); |
305 |
} |
306 |
if (notFirst) |
307 |
{ |
308 |
greetingBuffer.append('\n'); |
309 |
} |
310 |
else |
311 |
{ |
312 |
notFirst = true; |
313 |
} |
314 |
greetingBuffer.append(response); |
315 |
|
316 |
} |
317 |
while (continuation); |
318 |
greeting = greetingBuffer.toString(); |
319 |
} |
320 |
|
321 |
/** |
322 |
* Returns the server greeting message. |
323 |
*/ |
324 |
public String getGreeting() |
325 |
{ |
326 |
return greeting; |
327 |
} |
328 |
|
329 |
/** |
330 |
* Returns the text of the last response received from the server. |
331 |
*/ |
332 |
public String getLastResponse() |
333 |
{ |
334 |
return response; |
335 |
} |
336 |
|
337 |
// -- 3.3 Mail transactions -- |
338 |
|
339 |
/** |
340 |
* Execute a MAIL command. |
341 |
* @param reversePath the source mailbox(from address) |
342 |
* @param parameters optional ESMTP parameters |
343 |
* @return true if accepted, false otherwise |
344 |
*/ |
345 |
public boolean mailFrom(String reversePath, ParameterList parameters) |
346 |
throws IOException |
347 |
{ |
348 |
StringBuffer command = new StringBuffer(MAIL_FROM); |
349 |
command.append('<'); |
350 |
command.append(reversePath); |
351 |
command.append('>'); |
352 |
if (parameters != null) |
353 |
{ |
354 |
command.append(SP); |
355 |
command.append(parameters); |
356 |
} |
357 |
send(command.toString()); |
358 |
switch (getAllResponses()) |
359 |
{ |
360 |
case OK: |
361 |
case OK_NOT_LOCAL: |
362 |
case OK_UNVERIFIED: |
363 |
return true; |
364 |
default: |
365 |
return false; |
366 |
} |
367 |
} |
368 |
|
369 |
/** |
370 |
* Execute a RCPT command. |
371 |
* @param forwardPath the forward-path(recipient address) |
372 |
* @param parameters optional ESMTP parameters |
373 |
* @return true if successful, false otherwise |
374 |
*/ |
375 |
public boolean rcptTo(String forwardPath, ParameterList parameters) |
376 |
throws IOException |
377 |
{ |
378 |
StringBuffer command = new StringBuffer(RCPT_TO); |
379 |
command.append('<'); |
380 |
command.append(forwardPath); |
381 |
command.append('>'); |
382 |
if (parameters != null) |
383 |
{ |
384 |
command.append(SP); |
385 |
command.append(parameters); |
386 |
} |
387 |
send(command.toString()); |
388 |
switch (getAllResponses()) |
389 |
{ |
390 |
case OK: |
391 |
case OK_NOT_LOCAL: |
392 |
case OK_UNVERIFIED: |
393 |
return true; |
394 |
default: |
395 |
return false; |
396 |
} |
397 |
} |
398 |
|
399 |
/** |
400 |
* Requests an output stream to write message data to. |
401 |
* When the entire message has been written to the stream, the |
402 |
* <code>flush</code> method must be called on the stream. Until then no |
403 |
* further methods should be called on the connection. |
404 |
* Immediately after this procedure is complete, <code>finishData</code> |
405 |
* must be called to complete the transfer and determine its success. |
406 |
* @return a stream for writing messages to |
407 |
*/ |
408 |
public OutputStream data() |
409 |
throws IOException |
410 |
{ |
411 |
send(DATA); |
412 |
switch (getAllResponses()) |
413 |
{ |
414 |
case SEND_DATA: |
415 |
return new MessageOutputStream(out); |
416 |
default: |
417 |
throw new ProtocolException(response); |
418 |
} |
419 |
} |
420 |
|
421 |
/** |
422 |
* Completes the DATA procedure. |
423 |
* @see #data |
424 |
* @return true id transfer was successful, false otherwise |
425 |
*/ |
426 |
public boolean finishData() |
427 |
throws IOException |
428 |
{ |
429 |
send(FINISH_DATA); |
430 |
switch (getAllResponses()) |
431 |
{ |
432 |
case OK: |
433 |
return true; |
434 |
default: |
435 |
return false; |
436 |
} |
437 |
} |
438 |
|
439 |
/** |
440 |
* Aborts the current mail transaction. |
441 |
*/ |
442 |
public void rset() |
443 |
throws IOException |
444 |
{ |
445 |
send(RSET); |
446 |
if (getAllResponses() != OK) |
447 |
{ |
448 |
throw new ProtocolException(response); |
449 |
} |
450 |
} |
451 |
|
452 |
// -- 3.5 Commands for Debugging Addresses -- |
453 |
|
454 |
/** |
455 |
* Returns a list of valid possibilities for the specified address, or |
456 |
* null on failure. |
457 |
* @param address a mailbox, or real name and mailbox |
458 |
*/ |
459 |
public List vrfy(String address) |
460 |
throws IOException |
461 |
{ |
462 |
String command = VRFY + ' ' + address; |
463 |
send(command); |
464 |
List list = new ArrayList(); |
465 |
do |
466 |
{ |
467 |
switch (getResponse()) |
468 |
{ |
469 |
case OK: |
470 |
case AMBIGUOUS: |
471 |
response = response.trim(); |
472 |
if (response.indexOf('@') != -1) |
473 |
{ |
474 |
list.add(response); |
475 |
} |
476 |
else if (response.indexOf('<') != -1) |
477 |
{ |
478 |
list.add(response); |
479 |
} |
480 |
else if (response.indexOf(' ') == -1) |
481 |
{ |
482 |
list.add(response); |
483 |
} |
484 |
break; |
485 |
default: |
486 |
return null; |
487 |
} |
488 |
} |
489 |
while (continuation); |
490 |
return Collections.unmodifiableList(list); |
491 |
} |
492 |
|
493 |
/** |
494 |
* Returns a list of valid possibilities for the specified mailing list, |
495 |
* or null on failure. |
496 |
* @param address a mailing list name |
497 |
*/ |
498 |
public List expn(String address) |
499 |
throws IOException |
500 |
{ |
501 |
String command = EXPN + ' ' + address; |
502 |
send(command); |
503 |
List list = new ArrayList(); |
504 |
do |
505 |
{ |
506 |
switch (getResponse()) |
507 |
{ |
508 |
case OK: |
509 |
response = response.trim(); |
510 |
list.add(response); |
511 |
break; |
512 |
default: |
513 |
return null; |
514 |
} |
515 |
} |
516 |
while (continuation); |
517 |
return Collections.unmodifiableList(list); |
518 |
} |
519 |
|
520 |
/** |
521 |
* Returns some useful information about the specified parameter. |
522 |
* Typically this is a command. |
523 |
* @param arg the context of the query, or null for general information |
524 |
* @return a list of possibly useful information, or null if the command |
525 |
* failed. |
526 |
*/ |
527 |
public List help(String arg) |
528 |
throws IOException |
529 |
{ |
530 |
String command = (arg == null) ? HELP : |
531 |
HELP + ' ' + arg; |
532 |
send(command); |
533 |
List list = new ArrayList(); |
534 |
do |
535 |
{ |
536 |
switch (getResponse()) |
537 |
{ |
538 |
case INFO: |
539 |
list.add(response); |
540 |
break; |
541 |
default: |
542 |
return null; |
543 |
} |
544 |
} |
545 |
while (continuation); |
546 |
return Collections.unmodifiableList(list); |
547 |
} |
548 |
|
549 |
/** |
550 |
* Issues a NOOP command. |
551 |
* This does nothing, but can be used to keep the connection alive. |
552 |
*/ |
553 |
public void noop() |
554 |
throws IOException |
555 |
{ |
556 |
send(NOOP); |
557 |
getAllResponses(); |
558 |
} |
559 |
|
560 |
/** |
561 |
* Close the connection to the server. |
562 |
*/ |
563 |
public void quit() |
564 |
throws IOException |
565 |
{ |
566 |
try |
567 |
{ |
568 |
send(QUIT); |
569 |
getAllResponses(); |
570 |
/* RFC 2821 states that the server MUST send an OK reply here, but |
571 |
* many don't: postfix, for instance, sends 221. |
572 |
* In any case we have done our best. */ |
573 |
} |
574 |
catch (IOException e) |
575 |
{ |
576 |
} |
577 |
finally |
578 |
{ |
579 |
// Close the socket anyway. |
580 |
socket.close(); |
581 |
} |
582 |
} |
583 |
|
584 |
/** |
585 |
* Issues a HELO command. |
586 |
* @param hostname the local host name |
587 |
*/ |
588 |
public boolean helo(String hostname) |
589 |
throws IOException |
590 |
{ |
591 |
String command = HELO + ' ' + hostname; |
592 |
send(command); |
593 |
return (getAllResponses() == OK); |
594 |
} |
595 |
|
596 |
/** |
597 |
* Issues an EHLO command. |
598 |
* If successful, returns a list of the SMTP extensions supported by the |
599 |
* server. |
600 |
* Otherwise returns null, and HELO should be called. |
601 |
* @param hostname the local host name |
602 |
*/ |
603 |
public List ehlo(String hostname) |
604 |
throws IOException |
605 |
{ |
606 |
String command = EHLO + ' ' + hostname; |
607 |
send(command); |
608 |
List extensions = new ArrayList(); |
609 |
do |
610 |
{ |
611 |
switch (getResponse()) |
612 |
{ |
613 |
case OK: |
614 |
extensions.add(response); |
615 |
break; |
616 |
default: |
617 |
return null; |
618 |
} |
619 |
} |
620 |
while (continuation); |
621 |
return Collections.unmodifiableList(extensions); |
622 |
} |
623 |
|
624 |
/** |
625 |
* Returns a configured SSLSocketFactory to use in creating new SSL |
626 |
* sockets. |
627 |
* @param tm an optional trust manager to use |
628 |
*/ |
629 |
protected SSLSocketFactory getSSLSocketFactory(TrustManager tm) |
630 |
throws GeneralSecurityException |
631 |
{ |
632 |
if (tm == null) |
633 |
{ |
634 |
tm = new EmptyX509TrustManager(); |
635 |
} |
636 |
SSLContext context = SSLContext.getInstance("TLS"); |
637 |
TrustManager[] trust = new TrustManager[] { tm }; |
638 |
context.init(null, trust, null); |
639 |
return context.getSocketFactory(); |
640 |
} |
641 |
|
642 |
/** |
643 |
* Negotiate TLS over the current connection. |
644 |
* This depends on many features, such as the JSSE classes being in the |
645 |
* classpath. Returns true if successful, false otherwise. |
646 |
*/ |
647 |
public boolean starttls() |
648 |
throws IOException |
649 |
{ |
650 |
return starttls(new EmptyX509TrustManager()); |
651 |
} |
652 |
|
653 |
/** |
654 |
* Negotiate TLS over the current connection. |
655 |
* This depends on many features, such as the JSSE classes being in the |
656 |
* classpath. Returns true if successful, false otherwise. |
657 |
* @param tm the custom trust manager to use |
658 |
*/ |
659 |
public boolean starttls(TrustManager tm) |
660 |
throws IOException |
661 |
{ |
662 |
try |
663 |
{ |
664 |
SSLSocketFactory factory = getSSLSocketFactory(tm); |
665 |
|
666 |
send(STARTTLS); |
667 |
if (getAllResponses() != READY) |
668 |
{ |
669 |
return false; |
670 |
} |
671 |
|
672 |
String hostname = socket.getInetAddress().getHostName(); |
673 |
int port = socket.getPort(); |
674 |
SSLSocket ss = |
675 |
(SSLSocket) factory.createSocket(socket, hostname, port, true); |
676 |
String[] protocols = { "TLSv1", "SSLv3" }; |
677 |
ss.setEnabledProtocols(protocols); |
678 |
ss.setUseClientMode(true); |
679 |
ss.startHandshake(); |
680 |
|
681 |
// Set up streams |
682 |
InputStream is = ss.getInputStream(); |
683 |
is = new BufferedInputStream(is); |
684 |
is = new CRLFInputStream(is); |
685 |
in = new LineInputStream(is); |
686 |
OutputStream os = ss.getOutputStream(); |
687 |
os = new BufferedOutputStream(os); |
688 |
out = new SMTPOutputStream(os); |
689 |
return true; |
690 |
} |
691 |
catch (GeneralSecurityException e) |
692 |
{ |
693 |
return false; |
694 |
} |
695 |
} |
696 |
|
697 |
// -- Authentication -- |
698 |
|
699 |
/** |
700 |
* Authenticates the connection using the specified SASL mechanism, |
701 |
* username, and password. |
702 |
* @param mechanism a SASL authentication mechanism, e.g. LOGIN, PLAIN, |
703 |
* CRAM-MD5, GSSAPI |
704 |
* @param username the authentication principal |
705 |
* @param password the authentication credentials |
706 |
* @return true if authentication was successful, false otherwise |
707 |
*/ |
708 |
public boolean authenticate(String mechanism, String username, |
709 |
String password) throws IOException |
710 |
{ |
711 |
try |
712 |
{ |
713 |
String[] m = new String[] { mechanism }; |
714 |
CallbackHandler ch = new SaslCallbackHandler(username, password); |
715 |
// Avoid lengthy callback procedure for GNU Crypto |
716 |
HashMap p = new HashMap(); |
717 |
p.put("gnu.crypto.sasl.username", username); |
718 |
p.put("gnu.crypto.sasl.password", password); |
719 |
SaslClient sasl = |
720 |
Sasl.createSaslClient(m, null, "smtp", |
721 |
socket.getInetAddress().getHostName(), |
722 |
p, ch); |
723 |
if (sasl == null) |
724 |
{ |
725 |
// Fall back to home-grown SASL clients |
726 |
if ("LOGIN".equalsIgnoreCase(mechanism)) |
727 |
{ |
728 |
sasl = new SaslLogin(username, password); |
729 |
} |
730 |
else if ("PLAIN".equalsIgnoreCase(mechanism)) |
731 |
{ |
732 |
sasl = new SaslPlain(username, password); |
733 |
} |
734 |
else if ("CRAM-MD5".equalsIgnoreCase(mechanism)) |
735 |
{ |
736 |
sasl = new SaslCramMD5(username, password); |
737 |
} |
738 |
else |
739 |
{ |
740 |
return false; |
741 |
} |
742 |
} |
743 |
|
744 |
StringBuffer cmd = new StringBuffer(AUTH); |
745 |
cmd.append(' '); |
746 |
cmd.append(mechanism); |
747 |
if (sasl.hasInitialResponse()) |
748 |
{ |
749 |
cmd.append(' '); |
750 |
byte[] init = sasl.evaluateChallenge(new byte[0]); |
751 |
if (init.length == 0) |
752 |
{ |
753 |
cmd.append('='); |
754 |
} |
755 |
else |
756 |
{ |
757 |
cmd.append(new String(BASE64.encode(init), "US-ASCII")); |
758 |
} |
759 |
} |
760 |
send(cmd.toString()); |
761 |
while (true) |
762 |
{ |
763 |
switch (getAllResponses()) |
764 |
{ |
765 |
case 334: |
766 |
try |
767 |
{ |
768 |
byte[] c0 = response.getBytes("US-ASCII"); |
769 |
byte[] c1 = BASE64.decode(c0); // challenge |
770 |
byte[] r0 = sasl.evaluateChallenge(c1); |
771 |
byte[] r1 = BASE64.encode(r0); // response |
772 |
out.write(r1); |
773 |
out.write(0x0d); |
774 |
out.flush(); |
775 |
logger.log(SMTP_TRACE, "> " + |
776 |
new String(r1, "US-ASCII")); |
777 |
} |
778 |
catch (SaslException e) |
779 |
{ |
780 |
// Error in SASL challenge evaluation - cancel exchange |
781 |
out.write(0x2a); |
782 |
out.write(0x0d); |
783 |
out.flush(); |
784 |
logger.log(SMTP_TRACE, "> *"); |
785 |
} |
786 |
break; |
787 |
case 235: |
788 |
String qop = (String) sasl.getNegotiatedProperty(Sasl.QOP); |
789 |
if ("auth-int".equalsIgnoreCase(qop) |
790 |
|| "auth-conf".equalsIgnoreCase(qop)) |
791 |
{ |
792 |
InputStream is = socket.getInputStream(); |
793 |
is = new BufferedInputStream(is); |
794 |
is = new SaslInputStream(sasl, is); |
795 |
is = new CRLFInputStream(is); |
796 |
in = new LineInputStream(is); |
797 |
OutputStream os = socket.getOutputStream(); |
798 |
os = new BufferedOutputStream(os); |
799 |
os = new SaslOutputStream(sasl, os); |
800 |
out = new SMTPOutputStream(os); |
801 |
} |
802 |
return true; |
803 |
default: |
804 |
return false; |
805 |
} |
806 |
} |
807 |
} |
808 |
catch (SaslException e) |
809 |
{ |
810 |
logger.log(SMTP_TRACE, e.getMessage(), e); |
811 |
return false; // No provider for mechanism |
812 |
} |
813 |
catch (RuntimeException e) |
814 |
{ |
815 |
logger.log(SMTP_TRACE, e.getMessage(), e); |
816 |
return false; // No javax.security.sasl classes |
817 |
} |
818 |
} |
819 |
|
820 |
// -- Utility methods -- |
821 |
|
822 |
/** |
823 |
* Send the specified command string to the server. |
824 |
* @param command the command to send |
825 |
*/ |
826 |
protected void send(String command) |
827 |
throws IOException |
828 |
{ |
829 |
logger.log(SMTP_TRACE, "> " + command); |
830 |
out.write(command.getBytes("US-ASCII")); |
831 |
out.write(0x0d); |
832 |
out.flush(); |
833 |
} |
834 |
|
835 |
/** |
836 |
* Returns the next response from the server. |
837 |
*/ |
838 |
protected int getResponse() |
839 |
throws IOException |
840 |
{ |
841 |
String line = null; |
842 |
try |
843 |
{ |
844 |
line = in.readLine(); |
845 |
// Handle special case eg 334 where CRLF occurs after code. |
846 |
if (line.length() < 4) |
847 |
{ |
848 |
line = line + '\n' + in.readLine(); |
849 |
} |
850 |
logger.log(SMTP_TRACE, "< " + line); |
851 |
int code = Integer.parseInt(line.substring(0, 3)); |
852 |
continuation = (line.charAt(3) == '-'); |
853 |
response = line.substring(4); |
854 |
return code; |
855 |
} |
856 |
catch (NumberFormatException e) |
857 |
{ |
858 |
throw new ProtocolException("Unexpected response: " + line); |
859 |
} |
860 |
} |
861 |
|
862 |
/** |
863 |
* Returns the next response from the server. |
864 |
* If the response is a continuation, continues to read responses until |
865 |
* continuation ceases. If a different response code from the first is |
866 |
* encountered, this causes a protocol exception. |
867 |
*/ |
868 |
protected int getAllResponses() |
869 |
throws IOException |
870 |
{ |
871 |
int code1, code; |
872 |
boolean err = false; |
873 |
code1 = code = getResponse(); |
874 |
while (continuation) |
875 |
{ |
876 |
code = getResponse(); |
877 |
if (code != code1) |
878 |
{ |
879 |
err = true; |
880 |
} |
881 |
} |
882 |
if (err) |
883 |
{ |
884 |
throw new ProtocolException("Conflicting response codes"); |
885 |
} |
886 |
return code; |
887 |
} |
888 |
|
889 |
} |
890 |
|