/[classpath]/inetlib/source/gnu/inet/smtp/SMTPConnection.java
ViewVC logotype

Contents of /inetlib/source/gnu/inet/smtp/SMTPConnection.java

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.25 - (show annotations) (download)
Sun Oct 27 20:12:31 2013 UTC (10 years, 5 months ago) by dog
Branch: MAIN
CVS Tags: HEAD
Changes since 1.24: +4 -5 lines
Escape first dot on a line in SMTP client

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

savannah-hackers-public@gnu.org
ViewVC Help
Powered by ViewVC 1.1.26