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