View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.geronimo.javamail.transport.smtp;
21  
22  import java.io.BufferedReader;
23  import java.io.BufferedOutputStream; 
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.OutputStream;
28  import java.io.PrintStream;
29  import java.io.PrintWriter;
30  import java.io.UnsupportedEncodingException;
31  import java.lang.reflect.InvocationTargetException;
32  import java.lang.reflect.Method;
33  import java.net.InetAddress;
34  import java.net.Socket;
35  import java.net.SocketException; 
36  import java.util.ArrayList; 
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.StringTokenizer;
40  
41  import javax.mail.Address;
42  import javax.mail.AuthenticationFailedException;
43  import javax.mail.Message;
44  import javax.mail.MessagingException;
45  import javax.mail.internet.InternetAddress;
46  import javax.mail.internet.MimeMessage; 
47  import javax.mail.internet.MimeMultipart; 
48  import javax.mail.internet.MimePart;
49  import javax.mail.Session;
50  
51  import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 
52  import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 
53  import org.apache.geronimo.javamail.util.CountingOutputStream;
54  import org.apache.geronimo.javamail.util.MailConnection; 
55  import org.apache.geronimo.javamail.util.MIMEOutputStream;
56  import org.apache.geronimo.javamail.util.ProtocolProperties; 
57  import org.apache.geronimo.mail.util.Base64;
58  import org.apache.geronimo.mail.util.XText;
59  
60  /**
61   * Simple implementation of SMTP transport. Just does plain RFC977-ish delivery.
62   * 
63   * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $
64   */
65  public class SMTPConnection extends MailConnection {
66      protected static final String MAIL_SMTP_QUITWAIT = "quitwait";
67      protected static final String MAIL_SMTP_EXTENSION = "mailextension";
68      protected static final String MAIL_SMTP_EHLO = "ehlo";
69      protected static final String MAIL_SMTP_ALLOW8BITMIME = "allow8bitmime";
70      protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess";
71      protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable";
72      protected static final String MAIL_SMTP_AUTH = "auth";
73      protected static final String MAIL_SMTP_FROM = "from";
74      protected static final String MAIL_SMTP_DSN_RET = "dsn.ret";
75      protected static final String MAIL_SMTP_SUBMITTER = "submitter";
76  
77      /**
78       * property keys for protocol properties.
79       */
80      protected static final int DEFAULT_NNTP_PORT = 119;
81      
82      // the last response line received from the server.
83      protected SMTPReply lastServerResponse = null;
84      
85      // input reader wrapped around the socket input stream 
86      protected BufferedReader reader; 
87      // output writer wrapped around the socket output stream. 
88      protected PrintWriter writer; 
89      
90      // do we report success after completion of each mail send.
91      protected boolean reportSuccess;
92      // does the server support transport level security?
93      protected boolean serverTLS = false;
94      // is TLS enabled on our part?
95      protected boolean useTLS = false;
96      // should we use 8BITMIME encoding if supported by the server?
97      protected boolean use8bit = false; 
98  
99      /**
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