geronimo-javamail_1.4/geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/transport/smtp/SMTPConnection.java [60:1367]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public class SMTPConnection extends MailConnection {
    protected static final String MAIL_SMTP_QUITWAIT = "quitwait";
    protected static final String MAIL_SMTP_EXTENSION = "mailextension";
    protected static final String MAIL_SMTP_EHLO = "ehlo";
    protected static final String MAIL_SMTP_ALLOW8BITMIME = "allow8bitmime";
    protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess";
    protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable";
    protected static final String MAIL_SMTP_STARTTLS_REQUIRED = "starttls.required";
    protected static final String MAIL_SMTP_AUTH = "auth";
    protected static final String MAIL_SMTP_FROM = "from";
    protected static final String MAIL_SMTP_DSN_RET = "dsn.ret";
    protected static final String MAIL_SMTP_SUBMITTER = "submitter";

    /**
     * property keys for protocol properties.
     */
    protected static final int DEFAULT_NNTP_PORT = 119;

    // the last response line received from the server.
    protected SMTPReply lastServerResponse = null;

    // do we report success after completion of each mail send.
    protected boolean reportSuccess;
    // does the server support transport level security?
    protected boolean serverTLS = false;
    // is TLS enabled on our part?
    protected boolean useTLS = false;
    // is TLS required on our part?
    protected boolean requireTLS = false;
    // should we use 8BITMIME encoding if supported by the server?
    protected boolean use8bit = false;

    /**
     * Normal constructor for an SMTPConnection() object.
     *
     * @param props  The property bundle for this protocol instance.
     */
    public SMTPConnection(ProtocolProperties props) {
        super(props);

        // check to see if we need to throw an exception after a send operation.
        reportSuccess = props.getBooleanProperty(MAIL_SMTP_REPORT_SUCCESS, false);
        // and also check for TLS enablement.
        useTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_ENABLE, false);
        // and also check if TLS is required.
        requireTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_REQUIRED, false);
        // and also check for 8bitmime support
        use8bit = props.getBooleanProperty(MAIL_SMTP_ALLOW8BITMIME, false);
    }


    /**
     * Connect to the server and do the initial handshaking.
     *
     * @param host     The target host name.
     * @param port     The target port
     * @param username The connection username (can be null)
     * @param password The authentication password (can be null).
     *
     * @return true if we were able to obtain a connection and
     *         authenticate.
     * @exception MessagingException
     */
    public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {

        // now check to see if we need to authenticate. If we need this, then
        // we must have a username and
        // password specified. Failing this may result in a user prompt to
        // collect the information.
        boolean mustAuthenticate = props.getBooleanProperty(MAIL_SMTP_AUTH, false);

        // if we need to authenticate, and we don't have both a userid and
        // password, then we fail this
        // immediately. The Service.connect() method will try to obtain the user
        // information and retry the
        // connection one time.
        if (mustAuthenticate && (username == null || password == null)) {
            debugOut("Failing connection for missing authentication information");
            return false;
        }

        super.protocolConnect(host, port, username, password);

        try {
            // create socket and connect to server.
            getConnection();

            // receive welcoming message
            if (!getWelcome()) {
                debugOut("Error getting welcome message");
                throw new MessagingException("Error in getting welcome msg");
            }

            // say hello
            if (!sendHandshake()) {
                debugOut("Error getting processing handshake message");
                throw new MessagingException("Error in saying EHLO to server");
            }

            // authenticate with the server, if necessary
            if (!processAuthentication()) {
                debugOut("User authentication failure");
                throw new AuthenticationFailedException("Error authenticating with server");
            }
        } catch (IOException e) {
            debugOut("I/O exception establishing connection", e);
            throw new MessagingException("Connection error", e);
        }
        debugOut("Successful connection");
        return true;
    }


    /**
     * Close the connection. On completion, we'll be disconnected from the
     * server and unable to send more data.
     *
     * @exception MessagingException
     */
    public void close() throws MessagingException {
        // if we're already closed, get outta here.
        if (socket == null) {
            return;
        }
        try {
            // say goodbye
            sendQuit();
        } finally {
            // and close up the connection. We do this in a finally block to
            // make sure the connection
            // is shut down even if quit gets an error.
            closeServerConnection();
        }
    }

    public String toString() {
        return "SMTPConnection host: " + serverHost + " port: " + serverPort;
    }


    /**
     * Set the sender for this mail.
     *
     * @param message
     *                   The message we're sending.
     *
     * @return True if the command was accepted, false otherwise.
     * @exception MessagingException
     */
    protected boolean sendMailFrom(Message message) throws MessagingException {

        // need to sort the from value out from a variety of sources.
        String from = null;

        // first potential source is from the message itself, if it's an
        // instance of SMTPMessage.
        if (message instanceof SMTPMessage) {
            from = ((SMTPMessage) message).getEnvelopeFrom();
        }

        // if not available from the message, check the protocol property next
        if (from == null || from.length() == 0) {
            // the from value can be set explicitly as a property
            from = props.getProperty(MAIL_SMTP_FROM);
        }

        // if not there, see if we have something in the message header.
        if (from == null || from.length() == 0) {
            Address[] fromAddresses = message.getFrom();

            // if we have some addresses in the header, then take the first one
            // as our From: address
            if (fromAddresses != null && fromAddresses.length > 0) {
                from = ((InternetAddress) fromAddresses[0]).getAddress();
            }
            // get what the InternetAddress class believes to be the local
            // address.
            else {
                InternetAddress local = InternetAddress.getLocalAddress(session);
                if (local != null) {
                    from = local.getAddress();
                }
            }
        }

        if (from == null || from.length() == 0) {
            throw new MessagingException("no FROM address");
        }

        StringBuffer command = new StringBuffer();

        // start building up the command
        command.append("MAIL FROM: ");
        command.append(fixEmailAddress(from));

        // If the server supports the 8BITMIME extension, we might need to change the
        // transfer encoding for the content to allow for direct transmission of the
        // 8-bit codes.
        if (supportsExtension("8BITMIME")) {
            // we only do this if the capability was enabled via a property option or
            // by explicitly setting the property on the message object.
            if (use8bit || (message instanceof SMTPMessage && ((SMTPMessage)message).getAllow8bitMIME())) {
                // make sure we add the BODY= option to the FROM message.
                command.append(" BODY=8BITMIME");

                // go check the content and see if the can convert the transfer encoding to
                // allow direct 8-bit transmission.
                if (convertTransferEncoding((MimeMessage)message)) {
                    // if we changed the encoding on any of the parts, then we
                    // need to save the message again
                    message.saveChanges();
                }
            }
        }

        // some servers ask for a size estimate on the initial send
        if (supportsExtension("SIZE")) {
            int estimate = getSizeEstimate(message);
            if (estimate > 0) {
                command.append(" SIZE=" + estimate);
            }
        }

        // does this server support Delivery Status Notification? Then we may
        // need to add some extra to the command.
        if (supportsExtension("DSN")) {
            String returnNotification = null;

            // the return notification stuff might be set as value on the
            // message object itself.
            if (message instanceof SMTPMessage) {
                // we need to convert the option into a string value.
                switch (((SMTPMessage) message).getReturnOption()) {
                case SMTPMessage.RETURN_FULL:
                    returnNotification = "FULL";
                    break;

                case SMTPMessage.RETURN_HDRS:
                    returnNotification = "HDRS";
                    break;
                }
            }

            // if not obtained from the message object, it can also be set as a
            // property.
            if (returnNotification == null) {
                // the DSN value is set by yet another property.
                returnNotification = props.getProperty(MAIL_SMTP_DSN_RET);
            }

            // if we have a target, add the notification stuff to our FROM
            // command.
            if (returnNotification != null) {
                command.append(" RET=");
                command.append(returnNotification);
            }
        }

        // if this server supports AUTH and we have submitter information, then
        // we also add the
        // "AUTH=" keyword to the MAIL FROM command (see RFC 2554).

        if (supportsExtension("AUTH")) {
            String submitter = null;

            // another option that can be specified on the message object.
            if (message instanceof SMTPMessage) {
                submitter = ((SMTPMessage) message).getSubmitter();
            }
            // if not part of the object, try for a propery version.
            if (submitter == null) {
                // we only send the extra keyword is a submitter is specified.
                submitter = props.getProperty(MAIL_SMTP_SUBMITTER);
            }
            // we have one...add the keyword, plus the submitter info in xtext
            // format (defined by RFC 1891).
            if (submitter != null) {
                command.append(" AUTH=");
                try {
                    // add this encoded
                    command.append(new String(XText.encode(submitter.getBytes("US-ASCII")), "US-ASCII"));
                } catch (UnsupportedEncodingException e) {
                    throw new MessagingException("Invalid submitter value " + submitter);
                }
            }
        }

        String extension = null;

        // now see if we need to add any additional extension info to this
        // command. The extension is not
        // checked for validity. That's the reponsibility of the caller.
        if (message instanceof SMTPMessage) {
            extension = ((SMTPMessage) message).getMailExtension();
        }
        // this can come either from the object or from a set property.
        if (extension == null) {
            extension = props.getProperty(MAIL_SMTP_EXTENSION);
        }

        // have something real to add?
        if (extension != null && extension.length() != 0) {
            // tack this on the end with a blank delimiter.
            command.append(' ');
            command.append(extension);
        }

        // and finally send the command
        SMTPReply line = sendCommand(command.toString());

        // 250 response indicates success.
        return line.getCode() == SMTPReply.COMMAND_ACCEPTED;
    }


    /**
     * Check to see if a MIME body part can have its
     * encoding changed from quoted-printable or base64
     * encoding to 8bit encoding.  In order for this
     * to work, it must follow the rules laid out in
     * RFC 2045.  To qualify for conversion, the text
     * must be:
     *
     * 1)  No more than 998 bytes long
     * 2)  All lines are terminated with CRLF sequences
     * 3)  CR and LF characters only occur in properly
     * formed line separators
     * 4)  No null characters are allowed.
     *
     * The conversion will only be applied to text
     * elements, and this will recurse through the
     * different elements of MultiPart content.
     *
     * @param bodyPart The bodyPart to convert. Initially, this will be
     *                 the message itself.
     *
     * @return true if any conversion was performed, false if
     *         nothing was converted.
     */
    protected boolean convertTransferEncoding(MimePart bodyPart)
    {
        boolean converted = false;
        try {
            // if this is a multipart element, apply the conversion rules
            // to each of the parts.
            if (bodyPart.isMimeType("multipart/")) {
                MimeMultipart parts = (MimeMultipart)bodyPart.getContent();
                for (int i = 0; i < parts.getCount(); i++) {
                    // convert each body part, and accumulate the conversion result
                    converted = converted && convertTransferEncoding((MimePart)parts.getBodyPart(i));
                }
            }
            else {
                // we only do this if the encoding is quoted-printable or base64
                String encoding =  bodyPart.getEncoding();
                if (encoding != null) {
                    encoding = encoding.toLowerCase();
                    if (encoding.equals("quoted-printable") || encoding.equals("base64")) {
                        // this requires encoding.  Read the actual content to see if
                        // it conforms to the 8bit encoding rules.
                        if (isValid8bit(bodyPart.getInputStream())) {
                            // There's a huge hidden gotcha lurking under the covers here.
                            // If the content just exists as an encoded byte array, then just
                            // switching the transfer encoding will mess things up because the
                            // already encoded data gets transmitted in encoded form, but with
                            // and 8bit encoding style.  As a result, it doesn't get unencoded on
                            // the receiving end.  This is a nasty problem to debug.
                            //
                            // The solution is to get the content as it's object type, set it back
                            // on the the message in raw form.  Requesting the content will apply the
                            // current transfer encoding value to the data.  Once we have set the
                            // content value back, we can reset the transfer encoding.
                            bodyPart.setContent(bodyPart.getContent(), bodyPart.getContentType());

                            // it's valid, so change the transfer encoding to just
                            // pass the data through.
                            bodyPart.setHeader("Content-Transfer-Encoding", "8bit");
                            converted = true;   // we've changed something
                        }
                    }
                }
            }
        } catch (MessagingException e) {
        } catch (IOException e) {
        }
        return converted;
    }


    /**
     * Get the server's welcome blob from the wire....
     */
    protected boolean getWelcome() throws MessagingException {
        SMTPReply line = getReply();
        // just return the error status...we don't care about any of the
        // response information
        return !line.isError();
    }


    /**
     * Get an estimate of the transmission size for this
     * message.  This size is the complete message as it is
     * encoded and transmitted on the DATA command, not counting
     * the terminating ".CRLF".
     *
     * @param msg    The message we're sending.
     *
     * @return The count of bytes, if it can be calculated.
     */
    protected int getSizeEstimate(Message msg) {
        // now the data... I could look at the type, but
        try {
            CountingOutputStream outputStream = new CountingOutputStream();

            // the data content has two requirements we need to meet by
            // filtering the
            // output stream. Requirement 1 is to conicalize any line breaks.
            // All line
            // breaks will be transformed into properly formed CRLF sequences.
            //
            // Requirement 2 is to perform byte-stuff for any line that begins
            // with a "."
            // so that data is not confused with the end-of-data marker (a
            // "\r\n.\r\n" sequence.
            //
            // The MIME output stream performs those two functions on behalf of
            // the content
            // writer.
            MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);

            msg.writeTo(mimeOut);

            // now to finish, we make sure there's a line break at the end.
            mimeOut.forceTerminatingLineBreak();
            // and flush the data to send it along
            mimeOut.flush();

            return outputStream.getCount();
        } catch (IOException e) {
            return 0;     // can't get an estimate
        } catch (MessagingException e) {
            return 0;     // can't get an estimate
        }
    }


    /**
     * Sends the data in the message down the socket. This presumes the server
     * is in the right place and ready for getting the DATA message and the data
     * right place in the sequence
     */
    protected void sendData(MimeMessage msg) throws MessagingException {

        // send the DATA command
        SMTPReply line = sendCommand("DATA");

        if (line.isError()) {
            throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
        }

        // now the data... I could look at the type, but
        try {
            // the data content has two requirements we need to meet by
            // filtering the
            // output stream. Requirement 1 is to conicalize any line breaks.
            // All line
            // breaks will be transformed into properly formed CRLF sequences.
            //
            // Requirement 2 is to perform byte-stuff for any line that begins
            // with a "."
            // so that data is not confused with the end-of-data marker (a
            // "\r\n.\r\n" sequence.
            //
            // The MIME output stream performs those two functions on behalf of
            // the content
            // writer.
            MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);

            msg.writeTo(mimeOut, new String[] {"Bcc", "Content-Length"});

            // now to finish, we send a CRLF sequence, followed by a ".".
            mimeOut.writeSMTPTerminator();
            // and flush the data to send it along
            mimeOut.flush();
            this.outputStream.flush(); // most of the time MIMEOutputStream#flush does nothing so ensure we actually flush the data
        } catch (IOException e) {
            throw new MessagingException(e.toString());
        } catch (MessagingException e) {
            throw new MessagingException(e.toString());
        }

        // use a longer time out here to give the server time to process the
        // data.
        line = getReply(TIMEOUT * 2);

        if (line.isError()) {
            throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
        }
    }

    /**
     * Sends the QUIT message and receieves the response
     */
    protected void sendQuit() throws MessagingException {
        // there's yet another property that controls whether we should wait for
        // a reply for a QUIT command. If true, we're suppposed to wait for a response
        // from the QUIT command.  Otherwise we just send the QUIT and bail.  The default
        // is "false"
        if (props.getBooleanProperty(MAIL_SMTP_QUITWAIT, true)) {
            // handle as a real command...we're going to ignore the response.
            sendCommand("QUIT");
        } else {
            // just send the command without waiting for a response.
            sendLine("QUIT");
        }
    }

    /**
     * Sets a receiver address for the current message
     *
     * @param addr
     *            The target address.
     * @param dsn
     *            An optional DSN option appended to the RCPT TO command.
     *
     * @return The status for this particular send operation.
     * @exception MessagingException
     */
    public SendStatus sendRcptTo(InternetAddress addr, String dsn) throws MessagingException {
        // compose the command using the fixed up email address. Normally, this
        // involves adding
        // "<" and ">" around the address.

        StringBuffer command = new StringBuffer();

        // compose the first part of the command
        command.append("RCPT TO: ");
        command.append(fixEmailAddress(addr.getAddress()));

        // if we have DSN information, append it to the command.
        if (dsn != null) {
            command.append(" NOTIFY=");
            command.append(dsn);
        }

        // get a string version of this command.
        String commandString = command.toString();

        SMTPReply line = sendCommand(commandString);

        switch (line.getCode()) {
        // these two are both successful transmissions
        case SMTPReply.COMMAND_ACCEPTED:
        case SMTPReply.ADDRESS_NOT_LOCAL:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.SUCCESS, addr, commandString, line);

        // these are considered invalid address errors
        case SMTPReply.PARAMETER_SYNTAX_ERROR:
        case SMTPReply.INVALID_COMMAND_SEQUENCE:
        case SMTPReply.MAILBOX_NOT_FOUND:
        case SMTPReply.INVALID_MAILBOX:
        case SMTPReply.USER_NOT_LOCAL:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.INVALID_ADDRESS, addr, commandString, line);

        // the command was valid, but something went wrong in the server.
        case SMTPReply.SERVICE_NOT_AVAILABLE:
        case SMTPReply.MAILBOX_BUSY:
        case SMTPReply.PROCESSING_ERROR:
        case SMTPReply.INSUFFICIENT_STORAGE:
        case SMTPReply.MAILBOX_FULL:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.SEND_FAILURE, addr, commandString, line);

        // everything else is considered really bad...
        default:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.GENERAL_ERROR, addr, commandString, line);
        }
    }

    /**
     * Send a command to the server, returning the first response line back as a
     * reply.
     *
     * @param data
     *            The data to send.
     *
     * @return A reply object with the reply line.
     * @exception MessagingException
     */
    protected SMTPReply sendCommand(String data) throws MessagingException {
        sendLine(data);
        return getReply();
    }

    /**
     * Sends a message down the socket and terminates with the appropriate CRLF
     */
    protected void sendLine(String data) throws MessagingException {
        if (socket == null || !socket.isConnected()) {
            throw new MessagingException("no connection");
        }
        try { // don't write it in multiple times, ie build the data + "\r\n" string in memory to not get surprises on servers read() side
            outputStream.write((data + "\r\n").getBytes("ISO8859-1"));
            outputStream.flush();
        } catch (IOException e) {
            throw new MessagingException(e.toString());
        }
    }

    /**
     * Receives one line from the server. A line is a sequence of bytes
     * terminated by a CRLF
     *
     * @return the line from the server as String
     */
    protected String receiveLine() throws MessagingException {
        return receiveLine(TIMEOUT);
    }

    protected SMTPReply getReply() throws MessagingException {
        return getReply(TIMEOUT);
    }

    /**
     * Get a reply line for an SMTP command.
     *
     * @return An SMTP reply object from the stream.
     */
    protected SMTPReply getReply(int timeout) throws MessagingException {
        try {
            lastServerResponse = new SMTPReply(receiveLine(timeout));
            // if the first line we receive is a continuation, continue
            // reading lines until we reach the non-continued one.
            while (lastServerResponse.isContinued()) {
                lastServerResponse.addLine(receiveLine(timeout));
            }
        } catch (MalformedSMTPReplyException e) {
            throw new MessagingException(e.toString());
        }
        return lastServerResponse;
    }

    /**
     * Retrieve the last response received from the SMTP server.
     *
     * @return The raw response string (including the error code) returned from
     *         the SMTP server.
     */
    public SMTPReply getLastServerResponse() {
        return lastServerResponse;
    }


    /**
     * Receives one line from the server. A line is a sequence of bytes
     * terminated by a CRLF
     *
     * @return the line from the server as String
     */
    protected String receiveLine(int delayMillis) throws MessagingException {
        if (socket == null || !socket.isConnected()) {
            throw new MessagingException("no connection");
        }

        int timeout = 0;

        try {
            // for now, read byte for byte, looking for a CRLF
            timeout = socket.getSoTimeout();

            socket.setSoTimeout(delayMillis);

            StringBuffer buff = new StringBuffer();

            int c;
            boolean crFound = false, lfFound = false;

            while ((c = inputStream.read()) != -1 && crFound == false && lfFound == false) {
                // we're looking for a CRLF sequence, so mark each one as seen.
                // Any other
                // character gets appended to the end of the buffer.
                if (c == CR) {
                    crFound = true;
                } else if (c == LF) {
                    lfFound = true;
                } else {
                    buff.append((char) c);
                }
            }

            String line = buff.toString();
            return line;

        } catch (SocketException e) {
            throw new MessagingException(e.toString());
        } catch (IOException e) {
            throw new MessagingException(e.toString());
        } finally {
            try {
                socket.setSoTimeout(timeout);
            } catch (SocketException e) {
                // ignore - was just trying to do the decent thing...
            }
        }
    }

    /**
     * Convert an InternetAddress into a form sendable on an SMTP mail command.
     * InternetAddress.getAddress() generally returns just the address portion
     * of the full address, minus route address markers. We need to ensure we
     * have an address with '<' and '>' delimiters.
     *
     * @param mail
     *            The mail address returned from InternetAddress.getAddress().
     *
     * @return A string formatted for sending.
     */
    protected String fixEmailAddress(String mail) {
        if (mail.charAt(0) == '<') {
            return mail;
        }
        return "<" + mail + ">";
    }

    /**
     * Start the handshake process with the server, including setting up and
     * TLS-level work. At the completion of this task, we should be ready to
     * authenticate with the server, if needed.
     */
    protected boolean sendHandshake() throws MessagingException {
        // check to see what sort of initial handshake we need to make.
        boolean useEhlo = props.getBooleanProperty(MAIL_SMTP_EHLO, true);
        // if we're to use Ehlo, send it and then fall back to just a HELO
        // message if it fails.
        if (useEhlo) {
            if (!sendEhlo()) {
                sendHelo();
            }
        } else {
            // send the initial hello response.
            sendHelo();
        }

        if (useTLS || requireTLS) {
            // if we've been told to use TLS
            // if its not required and server does not support it we establish an unsecure connection
            //see GERONIMO-5873 and GERONIMO-5430
            if (requireTLS && !serverTLS) {
                // if we've been told to use TLS, and this server doesn't support
                // it, then this is a failure
                throw new MessagingException("Server doesn't support required transport level security");
            } else if (serverTLS){
                // if the server supports TLS, then use it for the connection.
                // on our connection.
                getConnectedTLSSocket();

                // some servers (gmail is one that I know of) only send a STARTTLS
                // extension message on the
                // first EHLO command. Now that we have the TLS handshaking
                // established, we need to send a
                // second EHLO message to retrieve the AUTH records from the server.
                if (!sendEhlo()) {
                    throw new MessagingException("Failure sending EHLO command to SMTP server");
                }
            } else {
                if (debug) {
                    debugOut("STARTTLS is enabled but not required and server does not support it. So we establish a connection without transport level security");
                }
            }
        }

        // this worked.
        return true;
    }


    /**
     * Switch the connection to using TLS level security, switching to an SSL
     * socket.
     */
    protected void getConnectedTLSSocket() throws MessagingException {
        debugOut("Attempting to negotiate STARTTLS with server " + serverHost);
        // tell the server of our intention to start a TLS session
        SMTPReply line = sendCommand("STARTTLS");

        if (line.getCode() != SMTPReply.SERVICE_READY) {
            debugOut("STARTTLS command rejected by SMTP server " + serverHost);
            throw new MessagingException("Unable to make TLS server connection");
        }

        debugOut("STARTTLS command accepted");

        // the base class handles the socket switch details
        super.getConnectedTLSSocket();
    }


    /**
     * Send the EHLO command to the SMTP server.
     *
     * @return True if the command was accepted ok, false for any errors.
     * @exception SMTPTransportException
     * @exception MalformedSMTPReplyException
     * @exception MessagingException
     */
    protected boolean sendEhlo() throws MessagingException {
        sendLine("EHLO " + getLocalHost());

        SMTPReply reply = getReply();

        // we get a 250 code back. The first line is just a greeting, and
        // extensions are identifed on
        // continuations. If this fails, then we'll try once more with HELO to
        // establish bona fides.
        if (reply.getCode() != SMTPReply.COMMAND_ACCEPTED) {
            return false;
        }

        // create a fresh mapping and authentications table
        capabilities = new HashMap();
        authentications = new ArrayList();

        List lines = reply.getLines();
        // process all of the continuation lines
        for (int i = 1; i < lines.size(); i++) {
            // go process the extention
            processExtension((String)lines.get(i));
        }
        return true;
    }

    /**
     * Send the HELO command to the SMTP server.
     *
     * @exception MessagingException
     */
    protected void sendHelo() throws MessagingException {
        // create a fresh mapping and authentications table
        // these will be empty, but it will prevent NPEs
        capabilities = new HashMap();
        authentications = new ArrayList();

        sendLine("HELO " + getLocalHost());

        SMTPReply line = getReply();

        // we get a 250 code back. The first line is just a greeting, and
        // extensions are identifed on
        // continuations. If this fails, then we'll try once more with HELO to
        // establish bona fides.
        if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
            throw new MessagingException("Failure sending HELO command to SMTP server");
        }
    }

    /**
     * Return the current startTLS property.
     *
     * @return The current startTLS property.
     */
    public boolean getStartTLS() {
        return useTLS;
    }


    /**
     * Set a new value for the startTLS property.
     *
     * @param start
     *            The new setting.
     */
    public void setStartTLS(boolean start) {
        useTLS = start;
    }

    
    /**
     * Return the current requireTLS property.
     * 
     * @return The current requireTLS property.
     */
    public boolean getRequireTLS() {
        return requireTLS;
    }


    /**
     * Set a new value for the requireTLS property.
     * 
     * @param require
     *                  The new setting.
     */
    public void setRequireTLS(boolean require) {
        requireTLS = require;
    }


    /**
     * Process an extension string passed back as the EHLP response.
     *
     * @param extension
     *            The string value of the extension (which will be of the form
     *            "NAME arguments").
     */
    protected void processExtension(String extension) {
        debugOut("Processing extension " + extension);
        String extensionName = extension.toUpperCase();
        String argument = "";

        int delimiter = extension.indexOf(' ');
        // if we have a keyword with arguments, parse them out and add to the
        // argument map.
        if (delimiter != -1) {
            extensionName = extension.substring(0, delimiter).toUpperCase();
            argument = extension.substring(delimiter + 1);
        }

        // add this to the map so it can be tested later.
        capabilities.put(extensionName, argument);

        // process a few special ones that don't require extra parsing.
        // AUTH and AUTH=LOGIN are handled the same
        if (extensionName.equals("AUTH")) {
            // if we don't have an argument on AUTH, this means LOGIN.
            if (argument == null) {
                authentications.add("LOGIN");
            } else {
                // The security mechanisms are blank delimited tokens.
                StringTokenizer tokenizer = new StringTokenizer(argument);

                while (tokenizer.hasMoreTokens()) {
                    String mechanism = tokenizer.nextToken().toUpperCase();
                    authentications.add(mechanism);
                }
            }
        }
        // special case for some older servers.
        else if (extensionName.equals("AUTH=LOGIN")) {
            authentications.add("LOGIN");
        }
        // does this support transport level security?
        else if (extensionName.equals("STARTTLS")) {
            // flag this for later
            serverTLS = true;
        }
    }


    /**
     * Retrieve any argument information associated with a extension reported
     * back by the server on the EHLO command.
     *
     * @param name
     *            The name of the target server extension.
     *
     * @return Any argument passed on a server extension. Returns null if the
     *         extension did not include an argument or the extension was not
     *         supported.
     */
    public String extensionParameter(String name) {
        if (capabilities != null) {
            return (String)capabilities.get(name);
        }
        return null;
    }


    /**
     * Tests whether the target server supports a named extension.
     *
     * @param name
     *            The target extension name.
     *
     * @return true if the target server reported on the EHLO command that is
     *         supports the targer server, false if the extension was not
     *         supported.
     */
    public boolean supportsExtension(String name) {
        // this only returns null if we don't have this extension
        return extensionParameter(name) != null;
    }


    /**
     * Authenticate with the server, if necessary (or possible).
     *
     * @return true if we are ok to proceed, false for an authentication
     *         failures.
     */
    protected boolean processAuthentication() throws MessagingException {
        // no authentication defined?
        if (!props.getBooleanProperty(MAIL_SMTP_AUTH, false)) {
            return true;
        }

        // we need to authenticate, but we don't have userid/password
        // information...fail this
        // immediately.
        if (username == null || password == null) {
            return false;
        }

        // if unable to get an appropriate authenticator, just fail it.
        ClientAuthenticator authenticator = getSaslAuthenticator();
        if (authenticator == null) {
            throw new MessagingException("Unable to obtain SASL authenticator");
        }


        if (debug) {
            debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
        }

        // if the authenticator has some initial data, we compose a command
        // containing the initial data.
        if (authenticator.hasInitialResponse()) {
            StringBuffer command = new StringBuffer();
            // the auth command initiates the handshaking.
            command.append("AUTH ");
            // and tell the server which mechanism we're using.
            command.append(authenticator.getMechanismName());
            command.append(" ");
            // and append the response data
            try {
                command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)), "US-ASCII"));
            } catch (UnsupportedEncodingException e) {
            }
            // send the command now
            sendLine(command.toString());
        }
        // we just send an auth command with the command type.
        else {
            StringBuffer command = new StringBuffer();
            // the auth command initiates the handshaking.
            command.append("AUTH ");
            // and tell the server which mechanism we're using.
            command.append(authenticator.getMechanismName());
            // send the command now
            sendLine(command.toString());
        }

        // now process the challenge sequence. We get a 235 response back when
        // the server accepts the
        // authentication, and a 334 indicates we have an additional challenge.
        while (true) {
            // get the next line, and if it is an error response, return now.
            SMTPReply line;
            try {
                line = new SMTPReply(receiveLine());
            } catch (MalformedSMTPReplyException e) {
                throw new MessagingException(e.toString());
            } catch (MessagingException e) {
                throw e;
            }

            // if we get a completion return, we've passed muster, so give an
            // authentication response.
            if (line.getCode() == SMTPReply.AUTHENTICATION_COMPLETE) {
                debugOut("Successful SMTP authentication");
                return true;
            }
            // we have an additional challenge to process.
            else if (line.getCode() == SMTPReply.AUTHENTICATION_CHALLENGE) {
                // Does the authenticator think it is finished? We can't answer
                // an additional challenge,
                // so fail this.
                if (authenticator.isComplete()) {
                    return false;
                }

                try {
                    // we're passed back a challenge value, Base64 encoded.
                    byte[] challenge = Base64.decode(line.getMessage().getBytes("ISO8859-1"));

                    // have the authenticator evaluate and send back the encoded
                    // response.
                    sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)), "US-ASCII"));
                } catch (UnsupportedEncodingException e) {
                }
            }
            // completion or challenge are the only responses we know how to
            // handle. Anything else must
            // be a failure.
            else {
                if (debug) {
                    debugOut("Authentication failure " + line);
                }
                return false;
            }
        }
    }


    /**
     * Attempt to retrieve a SASL authenticator for this
     * protocol.
     *
     * @return A SASL authenticator, or null if a suitable one
     *         was not located.
     */
    protected ClientAuthenticator getSaslAuthenticator() {
        return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
    }


    /**
     * Read the bytes in a stream a test to see if this
     * conforms to the RFC 2045 rules for 8bit encoding.
     *
     * 1)  No more than 998 bytes long
     * 2)  All lines are terminated with CRLF sequences
     * 3)  CR and LF characters only occur in properly
     * formed line separators
     * 4)  No null characters are allowed.
     *
     * @param inStream The source input stream.
     *
     * @return true if this can be transmitted successfully
     *         using 8bit encoding, false if an alternate encoding
     *         will be required.
     */
    protected boolean isValid8bit(InputStream inStream) {
        try {
            int ch;
            int lineLength = 0;
            while ((ch = inStream.read()) >= 0) {
                // nulls are decidedly not allowed
                if (ch == 0) {
                    return false;
                }
                // start of a CRLF sequence (potentially)
                else if (ch == '\r') {
                    // check the next character.  There must be one,
                    // and it must be a LF for this to be value
                    ch = inStream.read();
                    if (ch != '\n') {
                        return false;
                    }
                    // reset the line length
                    lineLength = 0;
                }
                else {
                    // a normal character
                    lineLength++;
                    // make sure the line is not too long
                    if (lineLength > 998) {
                        return false;
                    }
                }

            }
        } catch (IOException e) {
            return false;  // can't read this, don't try passing it
        }
        // this converted ok
        return true;
    }


    /**
     * Simple holder class for the address/send status duple, as we can have
     * mixed success for a set of addresses and a message
     */
    static public class SendStatus {
        public final static int SUCCESS = 0;

        public final static int INVALID_ADDRESS = 1;

        public final static int SEND_FAILURE = 2;

        public final static int GENERAL_ERROR = 3;

        // the status type of the send operation.
        int status;

        // the address associated with this status
        InternetAddress address;

        // the command string send to the server.
        String cmd;

        // the reply from the server.
        SMTPReply reply;

        /**
         * Constructor for a SendStatus item.
         *
         * @param s
         *            The status type.
         * @param a
         *            The address this is the status for.
         * @param c
         *            The command string associated with this status.
         * @param r
         *            The reply information from the server.
         */
        public SendStatus(int s, InternetAddress a, String c, SMTPReply r) {
            this.cmd = c;
            this.status = s;
            this.address = a;
            this.reply = r;
        }

        /**
         * Get the status information for this item.
         *
         * @return The current status code.
         */
        public int getStatus() {
            return this.status;
        }

        /**
         * Retrieve the InternetAddress object associated with this send
         * operation.
         *
         * @return The associated address object.
         */
        public InternetAddress getAddress() {
            return this.address;
        }

        /**
         * Retrieve the reply information associated with this send operati
         *
         * @return The SMTPReply object received for the operation.
         */
        public SMTPReply getReply() {
            return reply;
        }

        /**
         * Get the command string sent for this send operation.
         *
         * @return The command string for the MAIL TO command sent to the
         *         server.
         */
        public String getCommand() {
            return cmd;
        }

        /**
         * Get an exception object associated with this send operation. There is
         * a mechanism for reporting send success via a send operation, so this
         * will be either a success or failure exception.
         *
         * @param reportSuccess
         *            Indicates if we want success operations too.
         *
         * @return A newly constructed exception object.
         */
        public MessagingException getException(boolean reportSuccess) {
            if (status != SUCCESS) {
                return new SMTPAddressFailedException(address, cmd, reply.getCode(), reply.getMessage());
            } else {
                if (reportSuccess) {
                    return new SMTPAddressSucceededException(address, cmd, reply.getCode(), reply.getMessage());
                }
            }
            return null;
        }
    }


    /**
     * Reset the server connection after an error.
     *
     * @exception MessagingException
     */
    public void resetConnection() throws MessagingException {
        // we want the caller to retrieve the last response responsbile for
        // requiring the reset, so save and
        // restore that info around the reset.
        SMTPReply last = lastServerResponse;

        // send a reset command.
        SMTPReply line = sendCommand("RSET");

        // if this did not reset ok, just close the connection
        if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
            close();
        }
        // restore this.
        lastServerResponse = last;
    }


    /**
     * Return the current reportSuccess property.
     *
     * @return The current reportSuccess property.
     */
    public boolean getReportSuccess() {
        return reportSuccess;
    }

    /**
     * Set a new value for the reportSuccess property.
     *
     * @param report
     *            The new setting.
     */
    public void setReportSuccess(boolean report) {
        reportSuccess = report;
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



geronimo-javamail_1.5/geronimo-javamail_1.5_provider/src/main/java/org/apache/geronimo/javamail/transport/smtp/SMTPConnection.java [60:1367]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public class SMTPConnection extends MailConnection {
    protected static final String MAIL_SMTP_QUITWAIT = "quitwait";
    protected static final String MAIL_SMTP_EXTENSION = "mailextension";
    protected static final String MAIL_SMTP_EHLO = "ehlo";
    protected static final String MAIL_SMTP_ALLOW8BITMIME = "allow8bitmime";
    protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess";
    protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable";
    protected static final String MAIL_SMTP_STARTTLS_REQUIRED = "starttls.required";
    protected static final String MAIL_SMTP_AUTH = "auth";
    protected static final String MAIL_SMTP_FROM = "from";
    protected static final String MAIL_SMTP_DSN_RET = "dsn.ret";
    protected static final String MAIL_SMTP_SUBMITTER = "submitter";

    /**
     * property keys for protocol properties.
     */
    protected static final int DEFAULT_NNTP_PORT = 119;

    // the last response line received from the server.
    protected SMTPReply lastServerResponse = null;

    // do we report success after completion of each mail send.
    protected boolean reportSuccess;
    // does the server support transport level security?
    protected boolean serverTLS = false;
    // is TLS enabled on our part?
    protected boolean useTLS = false;
    // is TLS required on our part?
    protected boolean requireTLS = false;
    // should we use 8BITMIME encoding if supported by the server?
    protected boolean use8bit = false;

    /**
     * Normal constructor for an SMTPConnection() object.
     *
     * @param props  The property bundle for this protocol instance.
     */
    public SMTPConnection(ProtocolProperties props) {
        super(props);

        // check to see if we need to throw an exception after a send operation.
        reportSuccess = props.getBooleanProperty(MAIL_SMTP_REPORT_SUCCESS, false);
        // and also check for TLS enablement.
        useTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_ENABLE, false);
        // and also check if TLS is required.
        requireTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_REQUIRED, false);
        // and also check for 8bitmime support
        use8bit = props.getBooleanProperty(MAIL_SMTP_ALLOW8BITMIME, false);
    }


    /**
     * Connect to the server and do the initial handshaking.
     *
     * @param host     The target host name.
     * @param port     The target port
     * @param username The connection username (can be null)
     * @param password The authentication password (can be null).
     *
     * @return true if we were able to obtain a connection and
     *         authenticate.
     * @exception MessagingException
     */
    public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {

        // now check to see if we need to authenticate. If we need this, then
        // we must have a username and
        // password specified. Failing this may result in a user prompt to
        // collect the information.
        boolean mustAuthenticate = props.getBooleanProperty(MAIL_SMTP_AUTH, false);

        // if we need to authenticate, and we don't have both a userid and
        // password, then we fail this
        // immediately. The Service.connect() method will try to obtain the user
        // information and retry the
        // connection one time.
        if (mustAuthenticate && (username == null || password == null)) {
            debugOut("Failing connection for missing authentication information");
            return false;
        }

        super.protocolConnect(host, port, username, password);

        try {
            // create socket and connect to server.
            getConnection();

            // receive welcoming message
            if (!getWelcome()) {
                debugOut("Error getting welcome message");
                throw new MessagingException("Error in getting welcome msg");
            }

            // say hello
            if (!sendHandshake()) {
                debugOut("Error getting processing handshake message");
                throw new MessagingException("Error in saying EHLO to server");
            }

            // authenticate with the server, if necessary
            if (!processAuthentication()) {
                debugOut("User authentication failure");
                throw new AuthenticationFailedException("Error authenticating with server");
            }
        } catch (IOException e) {
            debugOut("I/O exception establishing connection", e);
            throw new MessagingException("Connection error", e);
        }
        debugOut("Successful connection");
        return true;
    }


    /**
     * Close the connection. On completion, we'll be disconnected from the
     * server and unable to send more data.
     *
     * @exception MessagingException
     */
    public void close() throws MessagingException {
        // if we're already closed, get outta here.
        if (socket == null) {
            return;
        }
        try {
            // say goodbye
            sendQuit();
        } finally {
            // and close up the connection. We do this in a finally block to
            // make sure the connection
            // is shut down even if quit gets an error.
            closeServerConnection();
        }
    }

    public String toString() {
        return "SMTPConnection host: " + serverHost + " port: " + serverPort;
    }


    /**
     * Set the sender for this mail.
     *
     * @param message
     *                   The message we're sending.
     *
     * @return True if the command was accepted, false otherwise.
     * @exception MessagingException
     */
    protected boolean sendMailFrom(Message message) throws MessagingException {

        // need to sort the from value out from a variety of sources.
        String from = null;

        // first potential source is from the message itself, if it's an
        // instance of SMTPMessage.
        if (message instanceof SMTPMessage) {
            from = ((SMTPMessage) message).getEnvelopeFrom();
        }

        // if not available from the message, check the protocol property next
        if (from == null || from.length() == 0) {
            // the from value can be set explicitly as a property
            from = props.getProperty(MAIL_SMTP_FROM);
        }

        // if not there, see if we have something in the message header.
        if (from == null || from.length() == 0) {
            Address[] fromAddresses = message.getFrom();

            // if we have some addresses in the header, then take the first one
            // as our From: address
            if (fromAddresses != null && fromAddresses.length > 0) {
                from = ((InternetAddress) fromAddresses[0]).getAddress();
            }
            // get what the InternetAddress class believes to be the local
            // address.
            else {
                InternetAddress local = InternetAddress.getLocalAddress(session);
                if (local != null) {
                    from = local.getAddress();
                }
            }
        }

        if (from == null || from.length() == 0) {
            throw new MessagingException("no FROM address");
        }

        StringBuffer command = new StringBuffer();

        // start building up the command
        command.append("MAIL FROM: ");
        command.append(fixEmailAddress(from));

        // If the server supports the 8BITMIME extension, we might need to change the
        // transfer encoding for the content to allow for direct transmission of the
        // 8-bit codes.
        if (supportsExtension("8BITMIME")) {
            // we only do this if the capability was enabled via a property option or
            // by explicitly setting the property on the message object.
            if (use8bit || (message instanceof SMTPMessage && ((SMTPMessage)message).getAllow8bitMIME())) {
                // make sure we add the BODY= option to the FROM message.
                command.append(" BODY=8BITMIME");

                // go check the content and see if the can convert the transfer encoding to
                // allow direct 8-bit transmission.
                if (convertTransferEncoding((MimeMessage)message)) {
                    // if we changed the encoding on any of the parts, then we
                    // need to save the message again
                    message.saveChanges();
                }
            }
        }

        // some servers ask for a size estimate on the initial send
        if (supportsExtension("SIZE")) {
            int estimate = getSizeEstimate(message);
            if (estimate > 0) {
                command.append(" SIZE=" + estimate);
            }
        }

        // does this server support Delivery Status Notification? Then we may
        // need to add some extra to the command.
        if (supportsExtension("DSN")) {
            String returnNotification = null;

            // the return notification stuff might be set as value on the
            // message object itself.
            if (message instanceof SMTPMessage) {
                // we need to convert the option into a string value.
                switch (((SMTPMessage) message).getReturnOption()) {
                case SMTPMessage.RETURN_FULL:
                    returnNotification = "FULL";
                    break;

                case SMTPMessage.RETURN_HDRS:
                    returnNotification = "HDRS";
                    break;
                }
            }

            // if not obtained from the message object, it can also be set as a
            // property.
            if (returnNotification == null) {
                // the DSN value is set by yet another property.
                returnNotification = props.getProperty(MAIL_SMTP_DSN_RET);
            }

            // if we have a target, add the notification stuff to our FROM
            // command.
            if (returnNotification != null) {
                command.append(" RET=");
                command.append(returnNotification);
            }
        }

        // if this server supports AUTH and we have submitter information, then
        // we also add the
        // "AUTH=" keyword to the MAIL FROM command (see RFC 2554).

        if (supportsExtension("AUTH")) {
            String submitter = null;

            // another option that can be specified on the message object.
            if (message instanceof SMTPMessage) {
                submitter = ((SMTPMessage) message).getSubmitter();
            }
            // if not part of the object, try for a propery version.
            if (submitter == null) {
                // we only send the extra keyword is a submitter is specified.
                submitter = props.getProperty(MAIL_SMTP_SUBMITTER);
            }
            // we have one...add the keyword, plus the submitter info in xtext
            // format (defined by RFC 1891).
            if (submitter != null) {
                command.append(" AUTH=");
                try {
                    // add this encoded
                    command.append(new String(XText.encode(submitter.getBytes("US-ASCII")), "US-ASCII"));
                } catch (UnsupportedEncodingException e) {
                    throw new MessagingException("Invalid submitter value " + submitter);
                }
            }
        }

        String extension = null;

        // now see if we need to add any additional extension info to this
        // command. The extension is not
        // checked for validity. That's the reponsibility of the caller.
        if (message instanceof SMTPMessage) {
            extension = ((SMTPMessage) message).getMailExtension();
        }
        // this can come either from the object or from a set property.
        if (extension == null) {
            extension = props.getProperty(MAIL_SMTP_EXTENSION);
        }

        // have something real to add?
        if (extension != null && extension.length() != 0) {
            // tack this on the end with a blank delimiter.
            command.append(' ');
            command.append(extension);
        }

        // and finally send the command
        SMTPReply line = sendCommand(command.toString());

        // 250 response indicates success.
        return line.getCode() == SMTPReply.COMMAND_ACCEPTED;
    }


    /**
     * Check to see if a MIME body part can have its
     * encoding changed from quoted-printable or base64
     * encoding to 8bit encoding.  In order for this
     * to work, it must follow the rules laid out in
     * RFC 2045.  To qualify for conversion, the text
     * must be:
     *
     * 1)  No more than 998 bytes long
     * 2)  All lines are terminated with CRLF sequences
     * 3)  CR and LF characters only occur in properly
     * formed line separators
     * 4)  No null characters are allowed.
     *
     * The conversion will only be applied to text
     * elements, and this will recurse through the
     * different elements of MultiPart content.
     *
     * @param bodyPart The bodyPart to convert. Initially, this will be
     *                 the message itself.
     *
     * @return true if any conversion was performed, false if
     *         nothing was converted.
     */
    protected boolean convertTransferEncoding(MimePart bodyPart)
    {
        boolean converted = false;
        try {
            // if this is a multipart element, apply the conversion rules
            // to each of the parts.
            if (bodyPart.isMimeType("multipart/")) {
                MimeMultipart parts = (MimeMultipart)bodyPart.getContent();
                for (int i = 0; i < parts.getCount(); i++) {
                    // convert each body part, and accumulate the conversion result
                    converted = converted && convertTransferEncoding((MimePart)parts.getBodyPart(i));
                }
            }
            else {
                // we only do this if the encoding is quoted-printable or base64
                String encoding =  bodyPart.getEncoding();
                if (encoding != null) {
                    encoding = encoding.toLowerCase();
                    if (encoding.equals("quoted-printable") || encoding.equals("base64")) {
                        // this requires encoding.  Read the actual content to see if
                        // it conforms to the 8bit encoding rules.
                        if (isValid8bit(bodyPart.getInputStream())) {
                            // There's a huge hidden gotcha lurking under the covers here.
                            // If the content just exists as an encoded byte array, then just
                            // switching the transfer encoding will mess things up because the
                            // already encoded data gets transmitted in encoded form, but with
                            // and 8bit encoding style.  As a result, it doesn't get unencoded on
                            // the receiving end.  This is a nasty problem to debug.
                            //
                            // The solution is to get the content as it's object type, set it back
                            // on the the message in raw form.  Requesting the content will apply the
                            // current transfer encoding value to the data.  Once we have set the
                            // content value back, we can reset the transfer encoding.
                            bodyPart.setContent(bodyPart.getContent(), bodyPart.getContentType());

                            // it's valid, so change the transfer encoding to just
                            // pass the data through.
                            bodyPart.setHeader("Content-Transfer-Encoding", "8bit");
                            converted = true;   // we've changed something
                        }
                    }
                }
            }
        } catch (MessagingException e) {
        } catch (IOException e) {
        }
        return converted;
    }


    /**
     * Get the server's welcome blob from the wire....
     */
    protected boolean getWelcome() throws MessagingException {
        SMTPReply line = getReply();
        // just return the error status...we don't care about any of the
        // response information
        return !line.isError();
    }


    /**
     * Get an estimate of the transmission size for this
     * message.  This size is the complete message as it is
     * encoded and transmitted on the DATA command, not counting
     * the terminating ".CRLF".
     *
     * @param msg    The message we're sending.
     *
     * @return The count of bytes, if it can be calculated.
     */
    protected int getSizeEstimate(Message msg) {
        // now the data... I could look at the type, but
        try {
            CountingOutputStream outputStream = new CountingOutputStream();

            // the data content has two requirements we need to meet by
            // filtering the
            // output stream. Requirement 1 is to conicalize any line breaks.
            // All line
            // breaks will be transformed into properly formed CRLF sequences.
            //
            // Requirement 2 is to perform byte-stuff for any line that begins
            // with a "."
            // so that data is not confused with the end-of-data marker (a
            // "\r\n.\r\n" sequence.
            //
            // The MIME output stream performs those two functions on behalf of
            // the content
            // writer.
            MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);

            msg.writeTo(mimeOut);

            // now to finish, we make sure there's a line break at the end.
            mimeOut.forceTerminatingLineBreak();
            // and flush the data to send it along
            mimeOut.flush();

            return outputStream.getCount();
        } catch (IOException e) {
            return 0;     // can't get an estimate
        } catch (MessagingException e) {
            return 0;     // can't get an estimate
        }
    }


    /**
     * Sends the data in the message down the socket. This presumes the server
     * is in the right place and ready for getting the DATA message and the data
     * right place in the sequence
     */
    protected void sendData(MimeMessage msg) throws MessagingException {

        // send the DATA command
        SMTPReply line = sendCommand("DATA");

        if (line.isError()) {
            throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
        }

        // now the data... I could look at the type, but
        try {
            // the data content has two requirements we need to meet by
            // filtering the
            // output stream. Requirement 1 is to conicalize any line breaks.
            // All line
            // breaks will be transformed into properly formed CRLF sequences.
            //
            // Requirement 2 is to perform byte-stuff for any line that begins
            // with a "."
            // so that data is not confused with the end-of-data marker (a
            // "\r\n.\r\n" sequence.
            //
            // The MIME output stream performs those two functions on behalf of
            // the content
            // writer.
            MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);

            msg.writeTo(mimeOut, new String[] {"Bcc", "Content-Length"});

            // now to finish, we send a CRLF sequence, followed by a ".".
            mimeOut.writeSMTPTerminator();
            // and flush the data to send it along
            mimeOut.flush();
            this.outputStream.flush(); // most of the time MIMEOutputStream#flush does nothing so ensure we actually flush the data
        } catch (IOException e) {
            throw new MessagingException(e.toString());
        } catch (MessagingException e) {
            throw new MessagingException(e.toString());
        }

        // use a longer time out here to give the server time to process the
        // data.
        line = getReply(TIMEOUT * 2);

        if (line.isError()) {
            throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
        }
    }

    /**
     * Sends the QUIT message and receieves the response
     */
    protected void sendQuit() throws MessagingException {
        // there's yet another property that controls whether we should wait for
        // a reply for a QUIT command. If true, we're suppposed to wait for a response
        // from the QUIT command.  Otherwise we just send the QUIT and bail.  The default
        // is "false"
        if (props.getBooleanProperty(MAIL_SMTP_QUITWAIT, true)) {
            // handle as a real command...we're going to ignore the response.
            sendCommand("QUIT");
        } else {
            // just send the command without waiting for a response.
            sendLine("QUIT");
        }
    }

    /**
     * Sets a receiver address for the current message
     *
     * @param addr
     *            The target address.
     * @param dsn
     *            An optional DSN option appended to the RCPT TO command.
     *
     * @return The status for this particular send operation.
     * @exception MessagingException
     */
    public SendStatus sendRcptTo(InternetAddress addr, String dsn) throws MessagingException {
        // compose the command using the fixed up email address. Normally, this
        // involves adding
        // "<" and ">" around the address.

        StringBuffer command = new StringBuffer();

        // compose the first part of the command
        command.append("RCPT TO: ");
        command.append(fixEmailAddress(addr.getAddress()));

        // if we have DSN information, append it to the command.
        if (dsn != null) {
            command.append(" NOTIFY=");
            command.append(dsn);
        }

        // get a string version of this command.
        String commandString = command.toString();

        SMTPReply line = sendCommand(commandString);

        switch (line.getCode()) {
        // these two are both successful transmissions
        case SMTPReply.COMMAND_ACCEPTED:
        case SMTPReply.ADDRESS_NOT_LOCAL:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.SUCCESS, addr, commandString, line);

        // these are considered invalid address errors
        case SMTPReply.PARAMETER_SYNTAX_ERROR:
        case SMTPReply.INVALID_COMMAND_SEQUENCE:
        case SMTPReply.MAILBOX_NOT_FOUND:
        case SMTPReply.INVALID_MAILBOX:
        case SMTPReply.USER_NOT_LOCAL:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.INVALID_ADDRESS, addr, commandString, line);

        // the command was valid, but something went wrong in the server.
        case SMTPReply.SERVICE_NOT_AVAILABLE:
        case SMTPReply.MAILBOX_BUSY:
        case SMTPReply.PROCESSING_ERROR:
        case SMTPReply.INSUFFICIENT_STORAGE:
        case SMTPReply.MAILBOX_FULL:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.SEND_FAILURE, addr, commandString, line);

        // everything else is considered really bad...
        default:
            // we get out of here with the status information.
            return new SendStatus(SendStatus.GENERAL_ERROR, addr, commandString, line);
        }
    }

    /**
     * Send a command to the server, returning the first response line back as a
     * reply.
     *
     * @param data
     *            The data to send.
     *
     * @return A reply object with the reply line.
     * @exception MessagingException
     */
    protected SMTPReply sendCommand(String data) throws MessagingException {
        sendLine(data);
        return getReply();
    }

    /**
     * Sends a message down the socket and terminates with the appropriate CRLF
     */
    protected void sendLine(String data) throws MessagingException {
        if (socket == null || !socket.isConnected()) {
            throw new MessagingException("no connection");
        }
        try { // don't write it in multiple times, ie build the data + "\r\n" string in memory to not get surprises on servers read() side
            outputStream.write((data + "\r\n").getBytes("ISO8859-1"));
            outputStream.flush();
        } catch (IOException e) {
            throw new MessagingException(e.toString());
        }
    }

    /**
     * Receives one line from the server. A line is a sequence of bytes
     * terminated by a CRLF
     *
     * @return the line from the server as String
     */
    protected String receiveLine() throws MessagingException {
        return receiveLine(TIMEOUT);
    }

    protected SMTPReply getReply() throws MessagingException {
        return getReply(TIMEOUT);
    }

    /**
     * Get a reply line for an SMTP command.
     *
     * @return An SMTP reply object from the stream.
     */
    protected SMTPReply getReply(int timeout) throws MessagingException {
        try {
            lastServerResponse = new SMTPReply(receiveLine(timeout));
            // if the first line we receive is a continuation, continue
            // reading lines until we reach the non-continued one.
            while (lastServerResponse.isContinued()) {
                lastServerResponse.addLine(receiveLine(timeout));
            }
        } catch (MalformedSMTPReplyException e) {
            throw new MessagingException(e.toString());
        }
        return lastServerResponse;
    }

    /**
     * Retrieve the last response received from the SMTP server.
     *
     * @return The raw response string (including the error code) returned from
     *         the SMTP server.
     */
    public SMTPReply getLastServerResponse() {
        return lastServerResponse;
    }


    /**
     * Receives one line from the server. A line is a sequence of bytes
     * terminated by a CRLF
     *
     * @return the line from the server as String
     */
    protected String receiveLine(int delayMillis) throws MessagingException {
        if (socket == null || !socket.isConnected()) {
            throw new MessagingException("no connection");
        }

        int timeout = 0;

        try {
            // for now, read byte for byte, looking for a CRLF
            timeout = socket.getSoTimeout();

            socket.setSoTimeout(delayMillis);

            StringBuffer buff = new StringBuffer();

            int c;
            boolean crFound = false, lfFound = false;

            while ((c = inputStream.read()) != -1 && crFound == false && lfFound == false) {
                // we're looking for a CRLF sequence, so mark each one as seen.
                // Any other
                // character gets appended to the end of the buffer.
                if (c == CR) {
                    crFound = true;
                } else if (c == LF) {
                    lfFound = true;
                } else {
                    buff.append((char) c);
                }
            }

            String line = buff.toString();
            return line;

        } catch (SocketException e) {
            throw new MessagingException(e.toString());
        } catch (IOException e) {
            throw new MessagingException(e.toString());
        } finally {
            try {
                socket.setSoTimeout(timeout);
            } catch (SocketException e) {
                // ignore - was just trying to do the decent thing...
            }
        }
    }

    /**
     * Convert an InternetAddress into a form sendable on an SMTP mail command.
     * InternetAddress.getAddress() generally returns just the address portion
     * of the full address, minus route address markers. We need to ensure we
     * have an address with '<' and '>' delimiters.
     *
     * @param mail
     *            The mail address returned from InternetAddress.getAddress().
     *
     * @return A string formatted for sending.
     */
    protected String fixEmailAddress(String mail) {
        if (mail.charAt(0) == '<') {
            return mail;
        }
        return "<" + mail + ">";
    }

    /**
     * Start the handshake process with the server, including setting up and
     * TLS-level work. At the completion of this task, we should be ready to
     * authenticate with the server, if needed.
     */
    protected boolean sendHandshake() throws MessagingException {
        // check to see what sort of initial handshake we need to make.
        boolean useEhlo = props.getBooleanProperty(MAIL_SMTP_EHLO, true);
        // if we're to use Ehlo, send it and then fall back to just a HELO
        // message if it fails.
        if (useEhlo) {
            if (!sendEhlo()) {
                sendHelo();
            }
        } else {
            // send the initial hello response.
            sendHelo();
        }

        if (useTLS || requireTLS) {
            // if we've been told to use TLS
            // if its not required and server does not support it we establish an unsecure connection
            //see GERONIMO-5873 and GERONIMO-5430
            if (requireTLS && !serverTLS) {
                // if we've been told to use TLS, and this server doesn't support
                // it, then this is a failure
                throw new MessagingException("Server doesn't support required transport level security");
            } else if (serverTLS){
                // if the server supports TLS, then use it for the connection.
                // on our connection.
                getConnectedTLSSocket();

                // some servers (gmail is one that I know of) only send a STARTTLS
                // extension message on the
                // first EHLO command. Now that we have the TLS handshaking
                // established, we need to send a
                // second EHLO message to retrieve the AUTH records from the server.
                if (!sendEhlo()) {
                    throw new MessagingException("Failure sending EHLO command to SMTP server");
                }
            } else {
                if (debug) {
                    debugOut("STARTTLS is enabled but not required and server does not support it. So we establish a connection without transport level security");
                }
            }
        }

        // this worked.
        return true;
    }


    /**
     * Switch the connection to using TLS level security, switching to an SSL
     * socket.
     */
    protected void getConnectedTLSSocket() throws MessagingException {
        debugOut("Attempting to negotiate STARTTLS with server " + serverHost);
        // tell the server of our intention to start a TLS session
        SMTPReply line = sendCommand("STARTTLS");

        if (line.getCode() != SMTPReply.SERVICE_READY) {
            debugOut("STARTTLS command rejected by SMTP server " + serverHost);
            throw new MessagingException("Unable to make TLS server connection");
        }

        debugOut("STARTTLS command accepted");

        // the base class handles the socket switch details
        super.getConnectedTLSSocket();
    }


    /**
     * Send the EHLO command to the SMTP server.
     *
     * @return True if the command was accepted ok, false for any errors.
     * @exception SMTPTransportException
     * @exception MalformedSMTPReplyException
     * @exception MessagingException
     */
    protected boolean sendEhlo() throws MessagingException {
        sendLine("EHLO " + getLocalHost());

        SMTPReply reply = getReply();

        // we get a 250 code back. The first line is just a greeting, and
        // extensions are identifed on
        // continuations. If this fails, then we'll try once more with HELO to
        // establish bona fides.
        if (reply.getCode() != SMTPReply.COMMAND_ACCEPTED) {
            return false;
        }

        // create a fresh mapping and authentications table
        capabilities = new HashMap();
        authentications = new ArrayList();

        List lines = reply.getLines();
        // process all of the continuation lines
        for (int i = 1; i < lines.size(); i++) {
            // go process the extention
            processExtension((String)lines.get(i));
        }
        return true;
    }

    /**
     * Send the HELO command to the SMTP server.
     *
     * @exception MessagingException
     */
    protected void sendHelo() throws MessagingException {
        // create a fresh mapping and authentications table
        // these will be empty, but it will prevent NPEs
        capabilities = new HashMap();
        authentications = new ArrayList();

        sendLine("HELO " + getLocalHost());

        SMTPReply line = getReply();

        // we get a 250 code back. The first line is just a greeting, and
        // extensions are identifed on
        // continuations. If this fails, then we'll try once more with HELO to
        // establish bona fides.
        if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
            throw new MessagingException("Failure sending HELO command to SMTP server");
        }
    }

    /**
     * Return the current startTLS property.
     *
     * @return The current startTLS property.
     */
    public boolean getStartTLS() {
        return useTLS;
    }


    /**
     * Set a new value for the startTLS property.
     *
     * @param start
     *            The new setting.
     */
    public void setStartTLS(boolean start) {
        useTLS = start;
    }

    
    /**
     * Return the current requireTLS property.
     * 
     * @return The current requireTLS property.
     */
    public boolean getRequireTLS() {
        return requireTLS;
    }


    /**
     * Set a new value for the requireTLS property.
     * 
     * @param require
     *                  The new setting.
     */
    public void setRequireTLS(boolean require) {
        requireTLS = require;
    }


    /**
     * Process an extension string passed back as the EHLP response.
     *
     * @param extension
     *            The string value of the extension (which will be of the form
     *            "NAME arguments").
     */
    protected void processExtension(String extension) {
        debugOut("Processing extension " + extension);
        String extensionName = extension.toUpperCase();
        String argument = "";

        int delimiter = extension.indexOf(' ');
        // if we have a keyword with arguments, parse them out and add to the
        // argument map.
        if (delimiter != -1) {
            extensionName = extension.substring(0, delimiter).toUpperCase();
            argument = extension.substring(delimiter + 1);
        }

        // add this to the map so it can be tested later.
        capabilities.put(extensionName, argument);

        // process a few special ones that don't require extra parsing.
        // AUTH and AUTH=LOGIN are handled the same
        if (extensionName.equals("AUTH")) {
            // if we don't have an argument on AUTH, this means LOGIN.
            if (argument == null) {
                authentications.add("LOGIN");
            } else {
                // The security mechanisms are blank delimited tokens.
                StringTokenizer tokenizer = new StringTokenizer(argument);

                while (tokenizer.hasMoreTokens()) {
                    String mechanism = tokenizer.nextToken().toUpperCase();
                    authentications.add(mechanism);
                }
            }
        }
        // special case for some older servers.
        else if (extensionName.equals("AUTH=LOGIN")) {
            authentications.add("LOGIN");
        }
        // does this support transport level security?
        else if (extensionName.equals("STARTTLS")) {
            // flag this for later
            serverTLS = true;
        }
    }


    /**
     * Retrieve any argument information associated with a extension reported
     * back by the server on the EHLO command.
     *
     * @param name
     *            The name of the target server extension.
     *
     * @return Any argument passed on a server extension. Returns null if the
     *         extension did not include an argument or the extension was not
     *         supported.
     */
    public String extensionParameter(String name) {
        if (capabilities != null) {
            return (String)capabilities.get(name);
        }
        return null;
    }


    /**
     * Tests whether the target server supports a named extension.
     *
     * @param name
     *            The target extension name.
     *
     * @return true if the target server reported on the EHLO command that is
     *         supports the targer server, false if the extension was not
     *         supported.
     */
    public boolean supportsExtension(String name) {
        // this only returns null if we don't have this extension
        return extensionParameter(name) != null;
    }


    /**
     * Authenticate with the server, if necessary (or possible).
     *
     * @return true if we are ok to proceed, false for an authentication
     *         failures.
     */
    protected boolean processAuthentication() throws MessagingException {
        // no authentication defined?
        if (!props.getBooleanProperty(MAIL_SMTP_AUTH, false)) {
            return true;
        }

        // we need to authenticate, but we don't have userid/password
        // information...fail this
        // immediately.
        if (username == null || password == null) {
            return false;
        }

        // if unable to get an appropriate authenticator, just fail it.
        ClientAuthenticator authenticator = getSaslAuthenticator();
        if (authenticator == null) {
            throw new MessagingException("Unable to obtain SASL authenticator");
        }


        if (debug) {
            debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
        }

        // if the authenticator has some initial data, we compose a command
        // containing the initial data.
        if (authenticator.hasInitialResponse()) {
            StringBuffer command = new StringBuffer();
            // the auth command initiates the handshaking.
            command.append("AUTH ");
            // and tell the server which mechanism we're using.
            command.append(authenticator.getMechanismName());
            command.append(" ");
            // and append the response data
            try {
                command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)), "US-ASCII"));
            } catch (UnsupportedEncodingException e) {
            }
            // send the command now
            sendLine(command.toString());
        }
        // we just send an auth command with the command type.
        else {
            StringBuffer command = new StringBuffer();
            // the auth command initiates the handshaking.
            command.append("AUTH ");
            // and tell the server which mechanism we're using.
            command.append(authenticator.getMechanismName());
            // send the command now
            sendLine(command.toString());
        }

        // now process the challenge sequence. We get a 235 response back when
        // the server accepts the
        // authentication, and a 334 indicates we have an additional challenge.
        while (true) {
            // get the next line, and if it is an error response, return now.
            SMTPReply line;
            try {
                line = new SMTPReply(receiveLine());
            } catch (MalformedSMTPReplyException e) {
                throw new MessagingException(e.toString());
            } catch (MessagingException e) {
                throw e;
            }

            // if we get a completion return, we've passed muster, so give an
            // authentication response.
            if (line.getCode() == SMTPReply.AUTHENTICATION_COMPLETE) {
                debugOut("Successful SMTP authentication");
                return true;
            }
            // we have an additional challenge to process.
            else if (line.getCode() == SMTPReply.AUTHENTICATION_CHALLENGE) {
                // Does the authenticator think it is finished? We can't answer
                // an additional challenge,
                // so fail this.
                if (authenticator.isComplete()) {
                    return false;
                }

                try {
                    // we're passed back a challenge value, Base64 encoded.
                    byte[] challenge = Base64.decode(line.getMessage().getBytes("ISO8859-1"));

                    // have the authenticator evaluate and send back the encoded
                    // response.
                    sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)), "US-ASCII"));
                } catch (UnsupportedEncodingException e) {
                }
            }
            // completion or challenge are the only responses we know how to
            // handle. Anything else must
            // be a failure.
            else {
                if (debug) {
                    debugOut("Authentication failure " + line);
                }
                return false;
            }
        }
    }


    /**
     * Attempt to retrieve a SASL authenticator for this
     * protocol.
     *
     * @return A SASL authenticator, or null if a suitable one
     *         was not located.
     */
    protected ClientAuthenticator getSaslAuthenticator() {
        return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
    }


    /**
     * Read the bytes in a stream a test to see if this
     * conforms to the RFC 2045 rules for 8bit encoding.
     *
     * 1)  No more than 998 bytes long
     * 2)  All lines are terminated with CRLF sequences
     * 3)  CR and LF characters only occur in properly
     * formed line separators
     * 4)  No null characters are allowed.
     *
     * @param inStream The source input stream.
     *
     * @return true if this can be transmitted successfully
     *         using 8bit encoding, false if an alternate encoding
     *         will be required.
     */
    protected boolean isValid8bit(InputStream inStream) {
        try {
            int ch;
            int lineLength = 0;
            while ((ch = inStream.read()) >= 0) {
                // nulls are decidedly not allowed
                if (ch == 0) {
                    return false;
                }
                // start of a CRLF sequence (potentially)
                else if (ch == '\r') {
                    // check the next character.  There must be one,
                    // and it must be a LF for this to be value
                    ch = inStream.read();
                    if (ch != '\n') {
                        return false;
                    }
                    // reset the line length
                    lineLength = 0;
                }
                else {
                    // a normal character
                    lineLength++;
                    // make sure the line is not too long
                    if (lineLength > 998) {
                        return false;
                    }
                }

            }
        } catch (IOException e) {
            return false;  // can't read this, don't try passing it
        }
        // this converted ok
        return true;
    }


    /**
     * Simple holder class for the address/send status duple, as we can have
     * mixed success for a set of addresses and a message
     */
    static public class SendStatus {
        public final static int SUCCESS = 0;

        public final static int INVALID_ADDRESS = 1;

        public final static int SEND_FAILURE = 2;

        public final static int GENERAL_ERROR = 3;

        // the status type of the send operation.
        int status;

        // the address associated with this status
        InternetAddress address;

        // the command string send to the server.
        String cmd;

        // the reply from the server.
        SMTPReply reply;

        /**
         * Constructor for a SendStatus item.
         *
         * @param s
         *            The status type.
         * @param a
         *            The address this is the status for.
         * @param c
         *            The command string associated with this status.
         * @param r
         *            The reply information from the server.
         */
        public SendStatus(int s, InternetAddress a, String c, SMTPReply r) {
            this.cmd = c;
            this.status = s;
            this.address = a;
            this.reply = r;
        }

        /**
         * Get the status information for this item.
         *
         * @return The current status code.
         */
        public int getStatus() {
            return this.status;
        }

        /**
         * Retrieve the InternetAddress object associated with this send
         * operation.
         *
         * @return The associated address object.
         */
        public InternetAddress getAddress() {
            return this.address;
        }

        /**
         * Retrieve the reply information associated with this send operati
         *
         * @return The SMTPReply object received for the operation.
         */
        public SMTPReply getReply() {
            return reply;
        }

        /**
         * Get the command string sent for this send operation.
         *
         * @return The command string for the MAIL TO command sent to the
         *         server.
         */
        public String getCommand() {
            return cmd;
        }

        /**
         * Get an exception object associated with this send operation. There is
         * a mechanism for reporting send success via a send operation, so this
         * will be either a success or failure exception.
         *
         * @param reportSuccess
         *            Indicates if we want success operations too.
         *
         * @return A newly constructed exception object.
         */
        public MessagingException getException(boolean reportSuccess) {
            if (status != SUCCESS) {
                return new SMTPAddressFailedException(address, cmd, reply.getCode(), reply.getMessage());
            } else {
                if (reportSuccess) {
                    return new SMTPAddressSucceededException(address, cmd, reply.getCode(), reply.getMessage());
                }
            }
            return null;
        }
    }


    /**
     * Reset the server connection after an error.
     *
     * @exception MessagingException
     */
    public void resetConnection() throws MessagingException {
        // we want the caller to retrieve the last response responsbile for
        // requiring the reset, so save and
        // restore that info around the reset.
        SMTPReply last = lastServerResponse;

        // send a reset command.
        SMTPReply line = sendCommand("RSET");

        // if this did not reset ok, just close the connection
        if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
            close();
        }
        // restore this.
        lastServerResponse = last;
    }


    /**
     * Return the current reportSuccess property.
     *
     * @return The current reportSuccess property.
     */
    public boolean getReportSuccess() {
        return reportSuccess;
    }

    /**
     * Set a new value for the reportSuccess property.
     *
     * @param report
     *            The new setting.
     */
    public void setReportSuccess(boolean report) {
        reportSuccess = report;
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



