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 org.apache.geronimo.javamail.transport.smtp;
021
022 import java.io.BufferedReader;
023 import java.io.BufferedOutputStream;
024 import java.io.IOException;
025 import java.io.InputStream;
026 import java.io.InputStreamReader;
027 import java.io.OutputStream;
028 import java.io.PrintStream;
029 import java.io.PrintWriter;
030 import java.io.UnsupportedEncodingException;
031 import java.lang.reflect.InvocationTargetException;
032 import java.lang.reflect.Method;
033 import java.net.InetAddress;
034 import java.net.Socket;
035 import java.net.SocketException;
036 import java.util.ArrayList;
037 import java.util.HashMap;
038 import java.util.List;
039 import java.util.StringTokenizer;
040
041 import javax.mail.Address;
042 import javax.mail.AuthenticationFailedException;
043 import javax.mail.Message;
044 import javax.mail.MessagingException;
045 import javax.mail.internet.InternetAddress;
046 import javax.mail.internet.MimeMessage;
047 import javax.mail.internet.MimeMultipart;
048 import javax.mail.internet.MimePart;
049 import javax.mail.Session;
050
051 import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
052 import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
053 import org.apache.geronimo.javamail.util.CountingOutputStream;
054 import org.apache.geronimo.javamail.util.MailConnection;
055 import org.apache.geronimo.javamail.util.MIMEOutputStream;
056 import org.apache.geronimo.javamail.util.ProtocolProperties;
057 import org.apache.geronimo.mail.util.Base64;
058 import org.apache.geronimo.mail.util.XText;
059
060 /**
061 * Simple implementation of SMTP transport. Just does plain RFC977-ish delivery.
062 *
063 * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $
064 */
065 public class SMTPConnection extends MailConnection {
066 protected static final String MAIL_SMTP_QUITWAIT = "quitwait";
067 protected static final String MAIL_SMTP_EXTENSION = "mailextension";
068 protected static final String MAIL_SMTP_EHLO = "ehlo";
069 protected static final String MAIL_SMTP_ALLOW8BITMIME = "allow8bitmime";
070 protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess";
071 protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable";
072 protected static final String MAIL_SMTP_AUTH = "auth";
073 protected static final String MAIL_SMTP_FROM = "from";
074 protected static final String MAIL_SMTP_DSN_RET = "dsn.ret";
075 protected static final String MAIL_SMTP_SUBMITTER = "submitter";
076
077 /**
078 * property keys for protocol properties.
079 */
080 protected static final int DEFAULT_NNTP_PORT = 119;
081
082 // the last response line received from the server.
083 protected SMTPReply lastServerResponse = null;
084
085 // input reader wrapped around the socket input stream
086 protected BufferedReader reader;
087 // output writer wrapped around the socket output stream.
088 protected PrintWriter writer;
089
090 // do we report success after completion of each mail send.
091 protected boolean reportSuccess;
092 // does the server support transport level security?
093 protected boolean serverTLS = false;
094 // is TLS enabled on our part?
095 protected boolean useTLS = false;
096 // should we use 8BITMIME encoding if supported by the server?
097 protected boolean use8bit = false;
098
099 /**
100 * Normal constructor for an SMTPConnection() object.
101 *
102 * @param props The property bundle for this protocol instance.
103 */
104 public SMTPConnection(ProtocolProperties props) {
105 super(props);
106
107 // check to see if we need to throw an exception after a send operation.
108 reportSuccess = props.getBooleanProperty(MAIL_SMTP_REPORT_SUCCESS, false);
109 // and also check for TLS enablement.
110 useTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_ENABLE, false);
111 // and also check for 8bitmime support
112 use8bit = props.getBooleanProperty(MAIL_SMTP_ALLOW8BITMIME, false);
113 }
114
115
116 /**
117 * Connect to the server and do the initial handshaking.
118 *
119 * @param host The target host name.
120 * @param port The target port
121 * @param username The connection username (can be null)
122 * @param password The authentication password (can be null).
123 *
124 * @return true if we were able to obtain a connection and
125 * authenticate.
126 * @exception MessagingException
127 */
128 public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
129
130 // now check to see if we need to authenticate. If we need this, then
131 // we must have a username and
132 // password specified. Failing this may result in a user prompt to
133 // collect the information.
134 boolean mustAuthenticate = props.getBooleanProperty(MAIL_SMTP_AUTH, false);
135
136 // if we need to authenticate, and we don't have both a userid and
137 // password, then we fail this
138 // immediately. The Service.connect() method will try to obtain the user
139 // information and retry the
140 // connection one time.
141 if (mustAuthenticate && (username == null || password == null)) {
142 debugOut("Failing connection for missing authentication information");
143 return false;
144 }
145
146 super.protocolConnect(host, port, username, password);
147
148 try {
149 // create socket and connect to server.
150 getConnection();
151
152 // receive welcoming message
153 if (!getWelcome()) {
154 debugOut("Error getting welcome message");
155 throw new MessagingException("Error in getting welcome msg");
156 }
157
158 // say hello
159 if (!sendHandshake()) {
160 debugOut("Error getting processing handshake message");
161 throw new MessagingException("Error in saying EHLO to server");
162 }
163
164 // authenticate with the server, if necessary
165 if (!processAuthentication()) {
166 debugOut("User authentication failure");
167 throw new AuthenticationFailedException("Error authenticating with server");
168 }
169 } catch (IOException e) {
170 debugOut("I/O exception establishing connection", e);
171 throw new MessagingException("Connection error", e);
172 }
173 debugOut("Successful connection");
174 return true;
175 }
176
177
178 /**
179 * Close the connection. On completion, we'll be disconnected from the
180 * server and unable to send more data.
181 *
182 * @exception MessagingException
183 */
184 public void close() throws MessagingException {
185 // if we're already closed, get outta here.
186 if (socket == null) {
187 return;
188 }
189 try {
190 // say goodbye
191 sendQuit();
192 } finally {
193 // and close up the connection. We do this in a finally block to
194 // make sure the connection
195 // is shut down even if quit gets an error.
196 closeServerConnection();
197 }
198 }
199
200 public String toString() {
201 return "SMTPConnection host: " + serverHost + " port: " + serverPort;
202 }
203
204
205 /**
206 * Set the sender for this mail.
207 *
208 * @param message
209 * The message we're sending.
210 *
211 * @return True if the command was accepted, false otherwise.
212 * @exception MessagingException
213 */
214 protected boolean sendMailFrom(Message message) throws MessagingException {
215
216 // need to sort the from value out from a variety of sources.
217 String from = null;
218
219 // first potential source is from the message itself, if it's an
220 // instance of SMTPMessage.
221 if (message instanceof SMTPMessage) {
222 from = ((SMTPMessage) message).getEnvelopeFrom();
223 }
224
225 // if not available from the message, check the protocol property next
226 if (from == null || from.length() == 0) {
227 // the from value can be set explicitly as a property
228 from = props.getProperty(MAIL_SMTP_FROM);
229 }
230
231 // if not there, see if we have something in the message header.
232 if (from == null || from.length() == 0) {
233 Address[] fromAddresses = message.getFrom();
234
235 // if we have some addresses in the header, then take the first one
236 // as our From: address
237 if (fromAddresses != null && fromAddresses.length > 0) {
238 from = ((InternetAddress) fromAddresses[0]).getAddress();
239 }
240 // get what the InternetAddress class believes to be the local
241 // address.
242 else {
243 InternetAddress local = InternetAddress.getLocalAddress(session);
244 if (local != null) {
245 from = local.getAddress();
246 }
247 }
248 }
249
250 if (from == null || from.length() == 0) {
251 throw new MessagingException("no FROM address");
252 }
253
254 StringBuffer command = new StringBuffer();
255
256 // start building up the command
257 command.append("MAIL FROM: ");
258 command.append(fixEmailAddress(from));
259
260 // If the server supports the 8BITMIME extension, we might need to change the
261 // transfer encoding for the content to allow for direct transmission of the
262 // 8-bit codes.
263 if (supportsExtension("8BITMIME")) {
264 // we only do this if the capability was enabled via a property option or
265 // by explicitly setting the property on the message object.
266 if (use8bit || (message instanceof SMTPMessage && ((SMTPMessage)message).getAllow8bitMIME())) {
267 // make sure we add the BODY= option to the FROM message.
268 command.append(" BODY=8BITMIME");
269
270 // go check the content and see if the can convert the transfer encoding to
271 // allow direct 8-bit transmission.
272 if (convertTransferEncoding((MimeMessage)message)) {
273 // if we changed the encoding on any of the parts, then we
274 // need to save the message again
275 message.saveChanges();
276 }
277 }
278 }
279
280 // some servers ask for a size estimate on the initial send
281 if (supportsExtension("SIZE")) {
282 int estimate = getSizeEstimate(message);
283 if (estimate > 0) {
284 command.append(" SIZE=" + estimate);
285 }
286 }
287
288 // does this server support Delivery Status Notification? Then we may
289 // need to add some extra to the command.
290 if (supportsExtension("DSN")) {
291 String returnNotification = null;
292
293 // the return notification stuff might be set as value on the
294 // message object itself.
295 if (message instanceof SMTPMessage) {
296 // we need to convert the option into a string value.
297 switch (((SMTPMessage) message).getReturnOption()) {
298 case SMTPMessage.RETURN_FULL:
299 returnNotification = "FULL";
300 break;
301
302 case SMTPMessage.RETURN_HDRS:
303 returnNotification = "HDRS";
304 break;
305 }
306 }
307
308 // if not obtained from the message object, it can also be set as a
309 // property.
310 if (returnNotification == null) {
311 // the DSN value is set by yet another property.
312 returnNotification = props.getProperty(MAIL_SMTP_DSN_RET);
313 }
314
315 // if we have a target, add the notification stuff to our FROM
316 // command.
317 if (returnNotification != null) {
318 command.append(" RET=");
319 command.append(returnNotification);
320 }
321 }
322
323 // if this server supports AUTH and we have submitter information, then
324 // we also add the
325 // "AUTH=" keyword to the MAIL FROM command (see RFC 2554).
326
327 if (supportsExtension("AUTH")) {
328 String submitter = null;
329
330 // another option that can be specified on the message object.
331 if (message instanceof SMTPMessage) {
332 submitter = ((SMTPMessage) message).getSubmitter();
333 }
334 // if not part of the object, try for a propery version.
335 if (submitter == null) {
336 // we only send the extra keyword is a submitter is specified.
337 submitter = props.getProperty(MAIL_SMTP_SUBMITTER);
338 }
339 // we have one...add the keyword, plus the submitter info in xtext
340 // format (defined by RFC 1891).
341 if (submitter != null) {
342 command.append(" AUTH=");
343 try {
344 // add this encoded
345 command.append(new String(XText.encode(submitter.getBytes("US-ASCII"))));
346 } catch (UnsupportedEncodingException e) {
347 throw new MessagingException("Invalid submitter value " + submitter);
348 }
349 }
350 }
351
352 String extension = null;
353
354 // now see if we need to add any additional extension info to this
355 // command. The extension is not
356 // checked for validity. That's the reponsibility of the caller.
357 if (message instanceof SMTPMessage) {
358 extension = ((SMTPMessage) message).getMailExtension();
359 }
360 // this can come either from the object or from a set property.
361 if (extension == null) {
362 extension = props.getProperty(MAIL_SMTP_EXTENSION);
363 }
364
365 // have something real to add?
366 if (extension != null && extension.length() != 0) {
367 // tack this on the end with a blank delimiter.
368 command.append(' ');
369 command.append(extension);
370 }
371
372 // and finally send the command
373 SMTPReply line = sendCommand(command.toString());
374
375 // 250 response indicates success.
376 return line.getCode() == SMTPReply.COMMAND_ACCEPTED;
377 }
378
379
380 /**
381 * Check to see if a MIME body part can have its
382 * encoding changed from quoted-printable or base64
383 * encoding to 8bit encoding. In order for this
384 * to work, it must follow the rules laid out in
385 * RFC 2045. To qualify for conversion, the text
386 * must be:
387 *
388 * 1) No more than 998 bytes long
389 * 2) All lines are terminated with CRLF sequences
390 * 3) CR and LF characters only occur in properly
391 * formed line separators
392 * 4) No null characters are allowed.
393 *
394 * The conversion will only be applied to text
395 * elements, and this will recurse through the
396 * different elements of MultiPart content.
397 *
398 * @param bodyPart The bodyPart to convert. Initially, this will be
399 * the message itself.
400 *
401 * @return true if any conversion was performed, false if
402 * nothing was converted.
403 */
404 protected boolean convertTransferEncoding(MimePart bodyPart)
405 {
406 boolean converted = false;
407 try {
408 // if this is a multipart element, apply the conversion rules
409 // to each of the parts.
410 if (bodyPart.isMimeType("multipart/")) {
411 MimeMultipart parts = (MimeMultipart)bodyPart.getContent();
412 for (int i = 0; i < parts.getCount(); i++) {
413 // convert each body part, and accumulate the conversion result
414 converted = converted && convertTransferEncoding((MimePart)parts.getBodyPart(i));
415 }
416 }
417 else {
418 // we only do this if the encoding is quoted-printable or base64
419 String encoding = bodyPart.getEncoding();
420 if (encoding != null) {
421 encoding = encoding.toLowerCase();
422 if (encoding.equals("quoted-printable") || encoding.equals("base64")) {
423 // this requires encoding. Read the actual content to see if
424 // it conforms to the 8bit encoding rules.
425 if (isValid8bit(bodyPart.getInputStream())) {
426 // There's a huge hidden gotcha lurking under the covers here.
427 // If the content just exists as an encoded byte array, then just
428 // switching the transfer encoding will mess things up because the
429 // already encoded data gets transmitted in encoded form, but with
430 // and 8bit encoding style. As a result, it doesn't get unencoded on
431 // the receiving end. This is a nasty problem to debug.
432 //
433 // The solution is to get the content as it's object type, set it back
434 // on the the message in raw form. Requesting the content will apply the
435 // current transfer encoding value to the data. Once we have set the
436 // content value back, we can reset the transfer encoding.
437 bodyPart.setContent(bodyPart.getContent(), bodyPart.getContentType());
438
439 // it's valid, so change the transfer encoding to just
440 // pass the data through.
441 bodyPart.setHeader("Content-Transfer-Encoding", "8bit");
442 converted = true; // we've changed something
443 }
444 }
445 }
446 }
447 } catch (MessagingException e) {
448 } catch (IOException e) {
449 }
450 return converted;
451 }
452
453
454 /**
455 * Get the server's welcome blob from the wire....
456 */
457 protected boolean getWelcome() throws MessagingException {
458 SMTPReply line = getReply();
459 // just return the error status...we don't care about any of the
460 // response information
461 return !line.isError();
462 }
463
464
465 /**
466 * Get an estimate of the transmission size for this
467 * message. This size is the complete message as it is
468 * encoded and transmitted on the DATA command, not counting
469 * the terminating ".CRLF".
470 *
471 * @param msg The message we're sending.
472 *
473 * @return The count of bytes, if it can be calculated.
474 */
475 protected int getSizeEstimate(Message msg) {
476 // now the data... I could look at the type, but
477 try {
478 CountingOutputStream outputStream = new CountingOutputStream();
479
480 // the data content has two requirements we need to meet by
481 // filtering the
482 // output stream. Requirement 1 is to conicalize any line breaks.
483 // All line
484 // breaks will be transformed into properly formed CRLF sequences.
485 //
486 // Requirement 2 is to perform byte-stuff for any line that begins
487 // with a "."
488 // so that data is not confused with the end-of-data marker (a
489 // "\r\n.\r\n" sequence.
490 //
491 // The MIME output stream performs those two functions on behalf of
492 // the content
493 // writer.
494 MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);
495
496 msg.writeTo(mimeOut);
497
498 // now to finish, we make sure there's a line break at the end.
499 mimeOut.forceTerminatingLineBreak();
500 // and flush the data to send it along
501 mimeOut.flush();
502
503 return outputStream.getCount();
504 } catch (IOException e) {
505 return 0; // can't get an estimate
506 } catch (MessagingException e) {
507 return 0; // can't get an estimate
508 }
509 }
510
511
512 /**
513 * Sends the data in the message down the socket. This presumes the server
514 * is in the right place and ready for getting the DATA message and the data
515 * right place in the sequence
516 */
517 protected void sendData(Message msg) throws MessagingException {
518
519 // send the DATA command
520 SMTPReply line = sendCommand("DATA");
521
522 if (line.isError()) {
523 throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
524 }
525
526 // now the data... I could look at the type, but
527 try {
528 // the data content has two requirements we need to meet by
529 // filtering the
530 // output stream. Requirement 1 is to conicalize any line breaks.
531 // All line
532 // breaks will be transformed into properly formed CRLF sequences.
533 //
534 // Requirement 2 is to perform byte-stuff for any line that begins
535 // with a "."
536 // so that data is not confused with the end-of-data marker (a
537 // "\r\n.\r\n" sequence.
538 //
539 // The MIME output stream performs those two functions on behalf of
540 // the content
541 // writer.
542 MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);
543
544 msg.writeTo(mimeOut);
545
546 // now to finish, we send a CRLF sequence, followed by a ".".
547 mimeOut.writeSMTPTerminator();
548 // and flush the data to send it along
549 mimeOut.flush();
550 } catch (IOException e) {
551 throw new MessagingException(e.toString());
552 } catch (MessagingException e) {
553 throw new MessagingException(e.toString());
554 }
555
556 // use a longer time out here to give the server time to process the
557 // data.
558 try {
559 line = new SMTPReply(receiveLine(TIMEOUT * 2));
560 } catch (MalformedSMTPReplyException e) {
561 throw new MessagingException(e.toString());
562 } catch (MessagingException e) {
563 throw new MessagingException(e.toString());
564 }
565
566 if (line.isError()) {
567 throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
568 }
569 }
570
571 /**
572 * Sends the QUIT message and receieves the response
573 */
574 protected void sendQuit() throws MessagingException {
575 // there's yet another property that controls whether we should wait for
576 // a reply for a QUIT command. If true, we're suppposed to wait for a response
577 // from the QUIT command. Otherwise we just send the QUIT and bail. The default
578 // is "false"
579 if (props.getBooleanProperty(MAIL_SMTP_QUITWAIT, true)) {
580 // handle as a real command...we're going to ignore the response.
581 sendCommand("QUIT");
582 } else {
583 // just send the command without waiting for a response.
584 sendLine("QUIT");
585 }
586 }
587
588 /**
589 * Sets a receiver address for the current message
590 *
591 * @param addr
592 * The target address.
593 * @param dsn
594 * An optional DSN option appended to the RCPT TO command.
595 *
596 * @return The status for this particular send operation.
597 * @exception MessagingException
598 */
599 public SendStatus sendRcptTo(InternetAddress addr, String dsn) throws MessagingException {
600 // compose the command using the fixed up email address. Normally, this
601 // involves adding
602 // "<" and ">" around the address.
603
604 StringBuffer command = new StringBuffer();
605
606 // compose the first part of the command
607 command.append("RCPT TO: ");
608 command.append(fixEmailAddress(addr.getAddress()));
609
610 // if we have DSN information, append it to the command.
611 if (dsn != null) {
612 command.append(" NOTIFY=");
613 command.append(dsn);
614 }
615
616 // get a string version of this command.
617 String commandString = command.toString();
618
619 SMTPReply line = sendCommand(commandString);
620
621 switch (line.getCode()) {
622 // these two are both successful transmissions
623 case SMTPReply.COMMAND_ACCEPTED:
624 case SMTPReply.ADDRESS_NOT_LOCAL:
625 // we get out of here with the status information.
626 return new SendStatus(SendStatus.SUCCESS, addr, commandString, line);
627
628 // these are considered invalid address errors
629 case SMTPReply.PARAMETER_SYNTAX_ERROR:
630 case SMTPReply.INVALID_COMMAND_SEQUENCE:
631 case SMTPReply.MAILBOX_NOT_FOUND:
632 case SMTPReply.INVALID_MAILBOX:
633 case SMTPReply.USER_NOT_LOCAL:
634 // we get out of here with the status information.
635 return new SendStatus(SendStatus.INVALID_ADDRESS, addr, commandString, line);
636
637 // the command was valid, but something went wrong in the server.
638 case SMTPReply.SERVICE_NOT_AVAILABLE:
639 case SMTPReply.MAILBOX_BUSY:
640 case SMTPReply.PROCESSING_ERROR:
641 case SMTPReply.INSUFFICIENT_STORAGE:
642 case SMTPReply.MAILBOX_FULL:
643 // we get out of here with the status information.
644 return new SendStatus(SendStatus.SEND_FAILURE, addr, commandString, line);
645
646 // everything else is considered really bad...
647 default:
648 // we get out of here with the status information.
649 return new SendStatus(SendStatus.GENERAL_ERROR, addr, commandString, line);
650 }
651 }
652
653 /**
654 * Send a command to the server, returning the first response line back as a
655 * reply.
656 *
657 * @param data
658 * The data to send.
659 *
660 * @return A reply object with the reply line.
661 * @exception MessagingException
662 */
663 protected SMTPReply sendCommand(String data) throws MessagingException {
664 sendLine(data);
665 return getReply();
666 }
667
668 /**
669 * Sends a message down the socket and terminates with the appropriate CRLF
670 */
671 protected void sendLine(String data) throws MessagingException {
672 if (socket == null || !socket.isConnected()) {
673 throw new MessagingException("no connection");
674 }
675 try {
676 outputStream.write(data.getBytes());
677 outputStream.write(CR);
678 outputStream.write(LF);
679 outputStream.flush();
680 } catch (IOException e) {
681 throw new MessagingException(e.toString());
682 }
683 }
684
685 /**
686 * Receives one line from the server. A line is a sequence of bytes
687 * terminated by a CRLF
688 *
689 * @return the line from the server as String
690 */
691 protected String receiveLine() throws MessagingException {
692 return receiveLine(TIMEOUT);
693 }
694
695 /**
696 * Get a reply line for an SMTP command.
697 *
698 * @return An SMTP reply object from the stream.
699 */
700 protected SMTPReply getReply() throws MessagingException {
701 try {
702 lastServerResponse = new SMTPReply(receiveLine());
703 // if the first line we receive is a continuation, continue
704 // reading lines until we reach the non-continued one.
705 while (lastServerResponse.isContinued()) {
706 lastServerResponse.addLine(receiveLine());
707 }
708 } catch (MalformedSMTPReplyException e) {
709 throw new MessagingException(e.toString());
710 } catch (MessagingException e) {
711 throw e;
712 }
713 return lastServerResponse;
714 }
715
716 /**
717 * Retrieve the last response received from the SMTP server.
718 *
719 * @return The raw response string (including the error code) returned from
720 * the SMTP server.
721 */
722 public SMTPReply getLastServerResponse() {
723 return lastServerResponse;
724 }
725
726
727 /**
728 * Receives one line from the server. A line is a sequence of bytes
729 * terminated by a CRLF
730 *
731 * @return the line from the server as String
732 */
733 protected String receiveLine(int delayMillis) throws MessagingException {
734 if (socket == null || !socket.isConnected()) {
735 throw new MessagingException("no connection");
736 }
737
738 int timeout = 0;
739
740 try {
741 // for now, read byte for byte, looking for a CRLF
742 timeout = socket.getSoTimeout();
743
744 socket.setSoTimeout(delayMillis);
745
746 StringBuffer buff = new StringBuffer();
747
748 int c;
749 boolean crFound = false, lfFound = false;
750
751 while ((c = inputStream.read()) != -1 && crFound == false && lfFound == false) {
752 // we're looking for a CRLF sequence, so mark each one as seen.
753 // Any other
754 // character gets appended to the end of the buffer.
755 if (c == CR) {
756 crFound = true;
757 } else if (c == LF) {
758 lfFound = true;
759 } else {
760 buff.append((char) c);
761 }
762 }
763
764 String line = buff.toString();
765 return line;
766
767 } catch (SocketException e) {
768 throw new MessagingException(e.toString());
769 } catch (IOException e) {
770 throw new MessagingException(e.toString());
771 } finally {
772 try {
773 socket.setSoTimeout(timeout);
774 } catch (SocketException e) {
775 // ignore - was just trying to do the decent thing...
776 }
777 }
778 }
779
780 /**
781 * Convert an InternetAddress into a form sendable on an SMTP mail command.
782 * InternetAddress.getAddress() generally returns just the address portion
783 * of the full address, minus route address markers. We need to ensure we
784 * have an address with '<' and '>' delimiters.
785 *
786 * @param mail
787 * The mail address returned from InternetAddress.getAddress().
788 *
789 * @return A string formatted for sending.
790 */
791 protected String fixEmailAddress(String mail) {
792 if (mail.charAt(0) == '<') {
793 return mail;
794 }
795 return "<" + mail + ">";
796 }
797
798 /**
799 * Start the handshake process with the server, including setting up and
800 * TLS-level work. At the completion of this task, we should be ready to
801 * authenticate with the server, if needed.
802 */
803 protected boolean sendHandshake() throws MessagingException {
804 // check to see what sort of initial handshake we need to make.
805 boolean useEhlo = props.getBooleanProperty(MAIL_SMTP_EHLO, true);
806 // if we're to use Ehlo, send it and then fall back to just a HELO
807 // message if it fails.
808 if (useEhlo) {
809 if (!sendEhlo()) {
810 sendHelo();
811 }
812 } else {
813 // send the initial hello response.
814 sendHelo();
815 }
816
817 if (useTLS) {
818 // if we've been told to use TLS, and this server doesn't support
819 // it, then this is a failure
820 if (!serverTLS) {
821 throw new MessagingException("Server doesn't support required transport level security");
822 }
823 // if the server supports TLS, then use it for the connection.
824 // on our connection.
825 getConnectedTLSSocket();
826
827 // some servers (gmail is one that I know of) only send a STARTTLS
828 // extension message on the
829 // first EHLO command. Now that we have the TLS handshaking
830 // established, we need to send a
831 // second EHLO message to retrieve the AUTH records from the server.
832 if (!sendEhlo()) {
833 throw new MessagingException("Failure sending EHLO command to SMTP server");
834 }
835 }
836
837 // this worked.
838 return true;
839 }
840
841
842 /**
843 * Switch the connection to using TLS level security, switching to an SSL
844 * socket.
845 */
846 protected void getConnectedTLSSocket() throws MessagingException {
847 debugOut("Attempting to negotiate STARTTLS with server " + serverHost);
848 // tell the server of our intention to start a TLS session
849 SMTPReply line = sendCommand("STARTTLS");
850
851 if (line.getCode() != SMTPReply.SERVICE_READY) {
852 debugOut("STARTTLS command rejected by SMTP server " + serverHost);
853 throw new MessagingException("Unable to make TLS server connection");
854 }
855
856 debugOut("STARTTLS command accepted");
857
858 // the base class handles the socket switch details
859 super.getConnectedTLSSocket();
860 }
861
862
863 /**
864 * Send the EHLO command to the SMTP server.
865 *
866 * @return True if the command was accepted ok, false for any errors.
867 * @exception SMTPTransportException
868 * @exception MalformedSMTPReplyException
869 * @exception MessagingException
870 */
871 protected boolean sendEhlo() throws MessagingException {
872 sendLine("EHLO " + getLocalHost());
873
874 SMTPReply reply = getReply();
875
876 // we get a 250 code back. The first line is just a greeting, and
877 // extensions are identifed on
878 // continuations. If this fails, then we'll try once more with HELO to
879 // establish bona fides.
880 if (reply.getCode() != SMTPReply.COMMAND_ACCEPTED) {
881 return false;
882 }
883
884 // create a fresh mapping and authentications table
885 capabilities = new HashMap();
886 authentications = new ArrayList();
887
888 List lines = reply.getLines();
889 // process all of the continuation lines
890 for (int i = 1; i < lines.size(); i++) {
891 // go process the extention
892 processExtension((String)lines.get(i));
893 }
894 return true;
895 }
896
897 /**
898 * Send the HELO command to the SMTP server.
899 *
900 * @exception MessagingException
901 */
902 protected void sendHelo() throws MessagingException {
903 // create a fresh mapping and authentications table
904 // these will be empty, but it will prevent NPEs
905 capabilities = new HashMap();
906 authentications = new ArrayList();
907
908 sendLine("HELO " + getLocalHost());
909
910 SMTPReply line = getReply();
911
912 // we get a 250 code back. The first line is just a greeting, and
913 // extensions are identifed on
914 // continuations. If this fails, then we'll try once more with HELO to
915 // establish bona fides.
916 if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
917 throw new MessagingException("Failure sending HELO command to SMTP server");
918 }
919 }
920
921 /**
922 * Return the current startTLS property.
923 *
924 * @return The current startTLS property.
925 */
926 public boolean getStartTLS() {
927 return useTLS;
928 }
929
930
931 /**
932 * Set a new value for the startTLS property.
933 *
934 * @param start
935 * The new setting.
936 */
937 public void setStartTLS(boolean start) {
938 useTLS = start;
939 }
940
941
942 /**
943 * Process an extension string passed back as the EHLP response.
944 *
945 * @param extension
946 * The string value of the extension (which will be of the form
947 * "NAME arguments").
948 */
949 protected void processExtension(String extension) {
950 debugOut("Processing extension " + extension);
951 String extensionName = extension.toUpperCase();
952 String argument = "";
953
954 int delimiter = extension.indexOf(' ');
955 // if we have a keyword with arguments, parse them out and add to the
956 // argument map.
957 if (delimiter != -1) {
958 extensionName = extension.substring(0, delimiter).toUpperCase();
959 argument = extension.substring(delimiter + 1);
960 }
961
962 // add this to the map so it can be tested later.
963 capabilities.put(extensionName, argument);
964
965 // process a few special ones that don't require extra parsing.
966 // AUTH and AUTH=LOGIN are handled the same
967 if (extensionName.equals("AUTH")) {
968 // if we don't have an argument on AUTH, this means LOGIN.
969 if (argument == null) {
970 authentications.add("LOGIN");
971 } else {
972 // The security mechanisms are blank delimited tokens.
973 StringTokenizer tokenizer = new StringTokenizer(argument);
974
975 while (tokenizer.hasMoreTokens()) {
976 String mechanism = tokenizer.nextToken().toUpperCase();
977 authentications.add(mechanism);
978 }
979 }
980 }
981 // special case for some older servers.
982 else if (extensionName.equals("AUTH=LOGIN")) {
983 authentications.add("LOGIN");
984 }
985 // does this support transport level security?
986 else if (extensionName.equals("STARTTLS")) {
987 // flag this for later
988 serverTLS = true;
989 }
990 }
991
992
993 /**
994 * Retrieve any argument information associated with a extension reported
995 * back by the server on the EHLO command.
996 *
997 * @param name
998 * The name of the target server extension.
999 *
1000 * @return Any argument passed on a server extension. Returns null if the
1001 * extension did not include an argument or the extension was not
1002 * supported.
1003 */
1004 public String extensionParameter(String name) {
1005 if (capabilities != null) {
1006 return (String)capabilities.get(name);
1007 }
1008 return null;
1009 }
1010
1011
1012 /**
1013 * Tests whether the target server supports a named extension.
1014 *
1015 * @param name
1016 * The target extension name.
1017 *
1018 * @return true if the target server reported on the EHLO command that is
1019 * supports the targer server, false if the extension was not
1020 * supported.
1021 */
1022 public boolean supportsExtension(String name) {
1023 // this only returns null if we don't have this extension
1024 return extensionParameter(name) != null;
1025 }
1026
1027
1028 /**
1029 * Authenticate with the server, if necessary (or possible).
1030 *
1031 * @return true if we are ok to proceed, false for an authentication
1032 * failures.
1033 */
1034 protected boolean processAuthentication() throws MessagingException {
1035 // no authentication defined?
1036 if (!props.getBooleanProperty(MAIL_SMTP_AUTH, false)) {
1037 return true;
1038 }
1039
1040 // we need to authenticate, but we don't have userid/password
1041 // information...fail this
1042 // immediately.
1043 if (username == null || password == null) {
1044 return false;
1045 }
1046
1047 // if unable to get an appropriate authenticator, just fail it.
1048 ClientAuthenticator authenticator = getSaslAuthenticator();
1049 if (authenticator == null) {
1050 throw new MessagingException("Unable to obtain SASL authenticator");
1051 }
1052
1053
1054 if (debug) {
1055 debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
1056 }
1057
1058 // if the authenticator has some initial data, we compose a command
1059 // containing the initial data.
1060 if (authenticator.hasInitialResponse()) {
1061 StringBuffer command = new StringBuffer();
1062 // the auth command initiates the handshaking.
1063 command.append("AUTH ");
1064 // and tell the server which mechanism we're using.
1065 command.append(authenticator.getMechanismName());
1066 command.append(" ");
1067 // and append the response data
1068 command.append(new String(Base64.encode(authenticator.evaluateChallenge(null))));
1069 // send the command now
1070 sendLine(command.toString());
1071 }
1072 // we just send an auth command with the command type.
1073 else {
1074 StringBuffer command = new StringBuffer();
1075 // the auth command initiates the handshaking.
1076 command.append("AUTH ");
1077 // and tell the server which mechanism we're using.
1078 command.append(authenticator.getMechanismName());
1079 // send the command now
1080 sendLine(command.toString());
1081 }
1082
1083 // now process the challenge sequence. We get a 235 response back when
1084 // the server accepts the
1085 // authentication, and a 334 indicates we have an additional challenge.
1086 while (true) {
1087 // get the next line, and if it is an error response, return now.
1088 SMTPReply line;
1089 try {
1090 line = new SMTPReply(receiveLine());
1091 } catch (MalformedSMTPReplyException e) {
1092 throw new MessagingException(e.toString());
1093 } catch (MessagingException e) {
1094 throw e;
1095 }
1096
1097 // if we get a completion return, we've passed muster, so give an
1098 // authentication response.
1099 if (line.getCode() == SMTPReply.AUTHENTICATION_COMPLETE) {
1100 debugOut("Successful SMTP authentication");
1101 return true;
1102 }
1103 // we have an additional challenge to process.
1104 else if (line.getCode() == SMTPReply.AUTHENTICATION_CHALLENGE) {
1105 // Does the authenticator think it is finished? We can't answer
1106 // an additional challenge,
1107 // so fail this.
1108 if (authenticator.isComplete()) {
1109 return false;
1110 }
1111
1112 // we're passed back a challenge value, Base64 encoded.
1113 byte[] challenge = Base64.decode(line.getMessage().getBytes());
1114
1115 // have the authenticator evaluate and send back the encoded
1116 // response.
1117 sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge))));
1118 }
1119 // completion or challenge are the only responses we know how to
1120 // handle. Anything else must
1121 // be a failure.
1122 else {
1123 if (debug) {
1124 debugOut("Authentication failure " + line);
1125 }
1126 return false;
1127 }
1128 }
1129 }
1130
1131
1132 /**
1133 * Attempt to retrieve a SASL authenticator for this
1134 * protocol.
1135 *
1136 * @return A SASL authenticator, or null if a suitable one
1137 * was not located.
1138 */
1139 protected ClientAuthenticator getSaslAuthenticator() {
1140 return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
1141 }
1142
1143
1144 /**
1145 * Read the bytes in a stream a test to see if this
1146 * conforms to the RFC 2045 rules for 8bit encoding.
1147 *
1148 * 1) No more than 998 bytes long
1149 * 2) All lines are terminated with CRLF sequences
1150 * 3) CR and LF characters only occur in properly
1151 * formed line separators
1152 * 4) No null characters are allowed.
1153 *
1154 * @param inStream The source input stream.
1155 *
1156 * @return true if this can be transmitted successfully
1157 * using 8bit encoding, false if an alternate encoding
1158 * will be required.
1159 */
1160 protected boolean isValid8bit(InputStream inStream) {
1161 try {
1162 int ch;
1163 int lineLength = 0;
1164 while ((ch = inStream.read()) >= 0) {
1165 // nulls are decidedly not allowed
1166 if (ch == 0) {
1167 return false;
1168 }
1169 // start of a CRLF sequence (potentially)
1170 else if (ch == '\r') {
1171 // check the next character. There must be one,
1172 // and it must be a LF for this to be value
1173 ch = inStream.read();
1174 if (ch != '\n') {
1175 return false;
1176 }
1177 // reset the line length
1178 lineLength = 0;
1179 }
1180 else {
1181 // a normal character
1182 lineLength++;
1183 // make sure the line is not too long
1184 if (lineLength > 998) {
1185 return false;
1186 }
1187 }
1188
1189 }
1190 } catch (IOException e) {
1191 return false; // can't read this, don't try passing it
1192 }
1193 // this converted ok
1194 return true;
1195 }
1196
1197
1198 /**
1199 * Simple holder class for the address/send status duple, as we can have
1200 * mixed success for a set of addresses and a message
1201 */
1202 static public class SendStatus {
1203 public final static int SUCCESS = 0;
1204
1205 public final static int INVALID_ADDRESS = 1;
1206
1207 public final static int SEND_FAILURE = 2;
1208
1209 public final static int GENERAL_ERROR = 3;
1210
1211 // the status type of the send operation.
1212 int status;
1213
1214 // the address associated with this status
1215 InternetAddress address;
1216
1217 // the command string send to the server.
1218 String cmd;
1219
1220 // the reply from the server.
1221 SMTPReply reply;
1222
1223 /**
1224 * Constructor for a SendStatus item.
1225 *
1226 * @param s
1227 * The status type.
1228 * @param a
1229 * The address this is the status for.
1230 * @param c
1231 * The command string associated with this status.
1232 * @param r
1233 * The reply information from the server.
1234 */
1235 public SendStatus(int s, InternetAddress a, String c, SMTPReply r) {
1236 this.cmd = c;
1237 this.status = s;
1238 this.address = a;
1239 this.reply = r;
1240 }
1241
1242 /**
1243 * Get the status information for this item.
1244 *
1245 * @return The current status code.
1246 */
1247 public int getStatus() {
1248 return this.status;
1249 }
1250
1251 /**
1252 * Retrieve the InternetAddress object associated with this send
1253 * operation.
1254 *
1255 * @return The associated address object.
1256 */
1257 public InternetAddress getAddress() {
1258 return this.address;
1259 }
1260
1261 /**
1262 * Retrieve the reply information associated with this send operati
1263 *
1264 * @return The SMTPReply object received for the operation.
1265 */
1266 public SMTPReply getReply() {
1267 return reply;
1268 }
1269
1270 /**
1271 * Get the command string sent for this send operation.
1272 *
1273 * @return The command string for the MAIL TO command sent to the
1274 * server.
1275 */
1276 public String getCommand() {
1277 return cmd;
1278 }
1279
1280 /**
1281 * Get an exception object associated with this send operation. There is
1282 * a mechanism for reporting send success via a send operation, so this
1283 * will be either a success or failure exception.
1284 *
1285 * @param reportSuccess
1286 * Indicates if we want success operations too.
1287 *
1288 * @return A newly constructed exception object.
1289 */
1290 public MessagingException getException(boolean reportSuccess) {
1291 if (status != SUCCESS) {
1292 return new SMTPAddressFailedException(address, cmd, reply.getCode(), reply.getMessage());
1293 } else {
1294 if (reportSuccess) {
1295 return new SMTPAddressSucceededException(address, cmd, reply.getCode(), reply.getMessage());
1296 }
1297 }
1298 return null;
1299 }
1300 }
1301
1302
1303 /**
1304 * Reset the server connection after an error.
1305 *
1306 * @exception MessagingException
1307 */
1308 public void resetConnection() throws MessagingException {
1309 // we want the caller to retrieve the last response responsbile for
1310 // requiring the reset, so save and
1311 // restore that info around the reset.
1312 SMTPReply last = lastServerResponse;
1313
1314 // send a reset command.
1315 SMTPReply line = sendCommand("RSET");
1316
1317 // if this did not reset ok, just close the connection
1318 if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
1319 close();
1320 }
1321 // restore this.
1322 lastServerResponse = last;
1323 }
1324
1325
1326 /**
1327 * Return the current reportSuccess property.
1328 *
1329 * @return The current reportSuccess property.
1330 */
1331 public boolean getReportSuccess() {
1332 return reportSuccess;
1333 }
1334
1335 /**
1336 * Set a new value for the reportSuccess property.
1337 *
1338 * @param report
1339 * The new setting.
1340 */
1341 public void setReportSuccess(boolean report) {
1342 reportSuccess = report;
1343 }
1344 }
1345