All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.geronimo.javamail.transport.smtp.SMTPTransport Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.geronimo.javamail.transport.smtp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.StringTokenizer;

import javax.mail.Address;
import javax.mail.AuthenticationFailedException;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.event.TransportEvent;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.net.ssl.SSLSocket;

import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
import org.apache.geronimo.javamail.authentication.CramMD5Authenticator;
import org.apache.geronimo.javamail.authentication.DigestMD5Authenticator;
import org.apache.geronimo.javamail.authentication.LoginAuthenticator;
import org.apache.geronimo.javamail.authentication.PlainAuthenticator;
import org.apache.geronimo.javamail.util.MIMEOutputStream;
import org.apache.geronimo.javamail.util.TraceInputStream;
import org.apache.geronimo.javamail.util.TraceOutputStream;
import org.apache.geronimo.mail.util.Base64;
import org.apache.geronimo.mail.util.XText;

/**
 * Simple implementation of SMTP transport. Just does plain RFC821-ish delivery.
 * 

Supported properties :

*

    *
  • mail.host : to set the server to deliver to. Default = localhost
  • *
  • mail.smtp.port : to set the port. Default = 25
  • *
  • mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()
  • *
*

There is no way to indicate failure for a given recipient (it's possible * to have a recipient address rejected). The sun impl throws exceptions even if * others successful), but maybe we do a different way...

TODO : lots. * ESMTP, user/pass, indicate failure, etc... * * @version $Rev: 515522 $ $Date: 2007-03-07 05:42:30 -0500 (Wed, 07 Mar 2007) $ */ public class SMTPTransport extends Transport { /** * constants for EOL termination */ protected static final char CR = '\r'; protected static final char LF = '\n'; /** * property keys for top level session properties. */ protected static final String MAIL_LOCALHOST = "mail.localhost"; protected static final String MAIL_SSLFACTORY_CLASS = "mail.SSLSocketFactory.class"; /** * property keys for protocol properties. The actual property name will be * appended with "mail." + protocol + ".", where the protocol is either * "smtp" or "smtps". */ protected static final String MAIL_SMTP_AUTH = "auth"; protected static final String MAIL_SMTP_PORT = "port"; protected static final String MAIL_SMTP_LOCALHOST = "localhost"; protected static final String MAIL_SMTP_TIMEOUT = "timeout"; protected static final String MAIL_SMTP_SASL_REALM = "sasl.realm"; protected static final String MAIL_SMTP_TLS = "starttls.enable"; protected static final String MAIL_SMTP_FACTORY_CLASS = "socketFactory.class"; protected static final String MAIL_SMTP_FACTORY_FALLBACK = "socketFactory.fallback"; protected static final String MAIL_SMTP_FACTORY_PORT = "socketFactory.port"; 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_DSN_NOTIFY = "dsn.notify"; protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial"; protected static final String MAIL_SMTP_LOCALADDRESS = "localaddress"; protected static final String MAIL_SMTP_LOCALPORT = "localport"; protected static final String MAIL_SMTP_QUITWAIT = "quitwait"; 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"; protected static final String MAIL_SMTP_EXTENSION = "mailextension"; protected static final String MAIL_SMTP_EHLO = "ehlo"; protected static final String MAIL_SMTP_ENCODE_TRACE = "encodetrace"; protected static final int MIN_MILLIS = 1000 * 60; protected static final int TIMEOUT = MIN_MILLIS * 5; protected static final String DEFAULT_MAIL_HOST = "localhost"; protected static final int DEFAULT_MAIL_SMTP_PORT = 25; protected static final int DEFAULT_MAIL_SMTPS_PORT = 465; // SMTP reply codes protected static final int SERVICE_READY = 220; protected static final int SERVICE_CLOSING = 221; protected static final int AUTHENTICATION_COMPLETE = 235; protected static final int COMMAND_ACCEPTED = 250; protected static final int ADDRESS_NOT_LOCAL = 251; protected static final int AUTHENTICATION_CHALLENGE = 334; protected static final int START_MAIL_INPUT = 354; protected static final int SERVICE_NOT_AVAILABLE = 421; protected static final int MAILBOX_BUSY = 450; protected static final int PROCESSING_ERROR = 451; protected static final int INSUFFICIENT_STORAGE = 452; protected static final int COMMAND_SYNTAX_ERROR = 500; protected static final int PARAMETER_SYNTAX_ERROR = 501; protected static final int COMMAND_NOT_IMPLEMENTED = 502; protected static final int INVALID_COMMAND_SEQUENCE = 503; protected static final int COMMAND_PARAMETER_NOT_IMPLEMENTED = 504; protected static final int MAILBOX_NOT_FOUND = 550; protected static final int USER_NOT_LOCAL = 551; protected static final int MAILBOX_FULL = 552; protected static final int INVALID_MAILBOX = 553; protected static final int TRANSACTION_FAILED = 553; protected static final String AUTHENTICATION_PLAIN = "PLAIN"; protected static final String AUTHENTICATION_LOGIN = "LOGIN"; protected static final String AUTHENTICATION_CRAMMD5 = "CRAM-MD5"; protected static final String AUTHENTICATION_DIGESTMD5 = "DIGEST-MD5"; // the protocol we're working with. This will be either "smtp" or "smtps". protected String protocol; // the target host protected String host; // the default port to use for this protocol (differs between "smtp" and // "smtps"). protected int defaultPort; // the target server port. protected int port; // the connection socket...can be a plain socket or SSLSocket, if TLS is // being used. protected Socket socket; // our local host name protected String localHost; // input stream used to read data. If Sasl is in use, this might be other // than the // direct access to the socket input stream. protected InputStream inputStream; // the other end of the connection pipeline. protected OutputStream outputStream; // list of authentication mechanisms supported by the server protected HashMap serverAuthenticationMechanisms; // map of server extension arguments protected HashMap serverExtensionArgs; // 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; // do we use SSL for our initial connection? protected boolean sslConnection = false; // the username we connect with protected String username; // the authentication password. protected String password; // the target SASL realm (normally null unless explicitly set or we have an // authentication mechanism that // requires it. protected String realm; // the last response line received from the server. protected SMTPReply lastServerResponse = null; // our session provided debug output stream. protected PrintStream debugStream; /** * Normal constructor for an SMTPTransport() object. This constructor is * used to build a transport instance for the "smtp" protocol. * * @param session * The attached session. * @param name * An optional URLName object containing target information. */ public SMTPTransport(Session session, URLName name) { this(session, name, "smtp", DEFAULT_MAIL_SMTP_PORT, false); } /** * Common constructor used by the SMTPTransport and SMTPSTransport classes * to do common initialization of defaults. * * @param session * The host session instance. * @param name * The URLName of the target. * @param protocol * The protocol type (either "smtp" or "smtps". This helps us in * retrieving protocol-specific session properties. * @param defaultPort * The default port used by this protocol. For "smtp", this will * be 25. The default for "smtps" is 465. * @param sslConnection * Indicates whether an SSL connection should be used to initial * contact the server. This is different from the STARTTLS * support, which switches the connection to SSL after the * initial startup. */ protected SMTPTransport(Session session, URLName name, String protocol, int defaultPort, boolean sslConnection) { super(session, name); this.protocol = protocol; // these are defaults based on what the superclass specifies. this.defaultPort = defaultPort; this.sslConnection = sslConnection; // check to see if we need to throw an exception after a send operation. reportSuccess = isProtocolPropertyTrue(MAIL_SMTP_REPORT_SUCCESS); // and also check for TLS enablement. useTLS = isProtocolPropertyTrue(MAIL_SMTP_STARTTLS_ENABLE); // get our debug output. debugStream = session.getDebugOut(); } /** * Connect to a server using an already created socket. This connection is * just like any other connection, except we will not create a new socket. * * @param socket * The socket connection to use. */ public void connect(Socket socket) throws MessagingException { this.socket = socket; super.connect(); } /** * Do the protocol connection for an SMTP transport. This handles server * authentication, if possible. Returns false if unable to connect to the * server. * * @param host * The target host name. * @param port * The server port number. * @param user * The authentication user (if any). * @param password * The server password. Might not be sent directly if more * sophisticated authentication is used. * * @return true if we were able to connect to the server properly, false for * any failures. * @exception MessagingException */ protected boolean protocolConnect(String host, int port, String username, String password) throws MessagingException { // first 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 = isProtocolPropertyTrue(MAIL_SMTP_AUTH); // 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)) { return false; } // Before we do anything, let's make sure that we succesfully received a host if ( host == null ) { host = DEFAULT_MAIL_HOST; } // if the port is defaulted, then see if we have something configured in // the session. // if not configured, we just use the default default. if (port == -1) { // take the default first. port = defaultPort; String configuredPort = getProtocolProperty(MAIL_SMTP_PORT); if (configuredPort != null) { port = Integer.parseInt(configuredPort); } } if (debug) { debugOut("Connecting to server " + host + ":" + port + " for user " + username); } try { // create socket and connect to server. getConnection(host, port, username, password); // receive welcoming message if (!getWelcome()) { throw new MessagingException("Error in getting welcome msg"); } // say hello if (!sendHandshake()) { throw new MessagingException("Error in saying EHLO to server"); } // authenticate with the server, if necessary if (!processAuthentication()) { if (debug) { debugOut("User authentication failure"); } throw new AuthenticationFailedException("Error authenticating with server"); } } catch (IOException e) { if (debug) { debugOut("I/O exception establishing connection", e); } throw new MessagingException("Connection error", e); } return true; } /** * Send a message to multiple addressees. * * @param message * The message we're sending. * @param addresses * An array of addresses to send to. * * @exception MessagingException */ public void sendMessage(Message message, Address[] addresses) throws MessagingException { if (!isConnected()) { throw new IllegalStateException("Not connected"); } // don't bother me w/ null messages or no addreses if (message == null) { throw new MessagingException("Null message"); } // SMTP only handles instances of MimeMessage, not the more general // message case. if (!(message instanceof MimeMessage)) { throw new MessagingException("SMTP can only send MimeMessages"); } // we must have a message list. if (addresses == null || addresses.length == 0) { throw new MessagingException("Null or empty address array"); } boolean haveGroup = false; // enforce the requirement that all of the targets are InternetAddress // instances. for (int i = 0; i < addresses.length; i++) { if (addresses[i] instanceof InternetAddress) { // and while we're here, see if we have a groups in the address // list. If we do, then // we're going to need to expand these before sending. if (((InternetAddress) addresses[i]).isGroup()) { haveGroup = true; } } else { throw new MessagingException("Illegal InternetAddress " + addresses[i]); } } // did we find a group? Time to expand this into our full target list. if (haveGroup) { addresses = expandGroups(addresses); } SendStatus[] stats = new SendStatus[addresses.length]; // create our lists for notification and exception reporting. Address[] sent = null; Address[] unsent = null; Address[] invalid = null; try { // send sender first. If this failed, send a failure notice of the // event, using the full list of // addresses as the unsent, and nothing for the rest. if (!sendMailFrom(message)) { unsent = addresses; sent = new Address[0]; invalid = new Address[0]; // notify of the error. notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); // include the reponse information here. SMTPReply last = lastServerResponse; // now send an "uber-exception" to indicate the failure. throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent, invalid); } String dsn = null; // there's an optional notification argument that can be added to // MAIL TO. See if we've been // provided with one. // an SMTPMessage object is the first source if (message instanceof SMTPMessage) { // get the notification options int options = ((SMTPMessage) message).getNotifyOptions(); switch (options) { // a zero value indicates nothing is set. case 0: break; case SMTPMessage.NOTIFY_NEVER: dsn = "NEVER"; break; case SMTPMessage.NOTIFY_SUCCESS: dsn = "SUCCESS"; break; case SMTPMessage.NOTIFY_FAILURE: dsn = "FAILURE"; break; case SMTPMessage.NOTIFY_DELAY: dsn = "DELAY"; break; // now for combinations...there are few enough combinations here // that we can just handle this in the switch statement rather // than have to // concatentate everything together. case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE): dsn = "SUCCESS,FAILURE"; break; case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_DELAY): dsn = "SUCCESS,DELAY"; break; case (SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): dsn = "FAILURE,DELAY"; break; case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): dsn = "SUCCESS,FAILURE,DELAY"; break; } } // if still null, grab a property value (yada, yada, yada...) if (dsn == null) { dsn = getProtocolProperty(MAIL_SMTP_DSN_NOTIFY); } // we need to know about any failures once we've gone through the // complete list, so keep a // failure flag. boolean sendFailure = false; // event notifcation requires we send lists of successes and // failures broken down by category. // The categories are: // // 1) addresses successfully processed. // 2) addresses deemed valid, but had a processing failure that // prevented sending. // 3) addressed deemed invalid (basically all other processing // failures). ArrayList sentAddresses = new ArrayList(); ArrayList unsentAddresses = new ArrayList(); ArrayList invalidAddresses = new ArrayList(); // Now we add a MAIL TO record for each recipient. At this point, we // just collect for (int i = 0; i < addresses.length; i++) { InternetAddress target = (InternetAddress) addresses[i]; // write out the record now. SendStatus status = sendRcptTo(target, dsn); stats[i] = status; switch (status.getStatus()) { // successfully sent case SendStatus.SUCCESS: sentAddresses.add(target); break; // we have an invalid address of some sort, or a general sending // error (which we'll // interpret as due to an invalid address. case SendStatus.INVALID_ADDRESS: case SendStatus.GENERAL_ERROR: sendFailure = true; invalidAddresses.add(target); break; // good address, but this was a send failure. case SendStatus.SEND_FAILURE: sendFailure = true; unsentAddresses.add(target); break; } } // if we had a send failure, then we need to check if we allow // partial sends. If not allowed, // we abort the send operation now. if (sendFailure) { // now see how we're configured for this send operation. boolean partialSends = false; // this can be attached directly to the message. if (message instanceof SMTPMessage) { partialSends = ((SMTPMessage) message).getSendPartial(); } // if still false on the message object, check for a property // version also if (!partialSends) { partialSends = isProtocolPropertyTrue(MAIL_SMTP_SENDPARTIAL); } // if we're not allowing partial successes or we've failed on // all of the addresses, it's // time to abort. if (!partialSends || sentAddresses.isEmpty()) { // we send along the valid and invalid address lists on the // notifications and // exceptions. // however, since we're aborting the entire send, the // successes need to become // members of the failure list. unsentAddresses.addAll(sentAddresses); // this one is empty. sent = new Address[0]; unsent = (Address[]) unsentAddresses.toArray(new Address[0]); invalid = (Address[]) invalidAddresses.toArray(new Address[0]); // go reset our connection so we can process additional // sends. resetConnection(); // get a list of chained exceptions for all of the failures. MessagingException failures = generateExceptionChain(stats, false); // now send an "uber-exception" to indicate the failure. throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); } } try { // try to send the data sendData(message); } catch (MessagingException e) { // If there's an error at this point, this is a complete // delivery failure. // we send along the valid and invalid address lists on the // notifications and // exceptions. // however, since we're aborting the entire send, the successes // need to become // members of the failure list. unsentAddresses.addAll(sentAddresses); // this one is empty. sent = new Address[0]; unsent = (Address[]) unsentAddresses.toArray(new Address[0]); invalid = (Address[]) invalidAddresses.toArray(new Address[0]); // notify of the error. notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); // send a send failure exception. throw new SMTPSendFailedException("DATA", 0, "Send failure", e, sent, unsent, invalid); } // create our lists for notification and exception reporting from // this point on. sent = (Address[]) sentAddresses.toArray(new Address[0]); unsent = (Address[]) unsentAddresses.toArray(new Address[0]); invalid = (Address[]) invalidAddresses.toArray(new Address[0]); // if sendFailure is true, we had an error during the address phase, // but we had permission to // process this as a partial send operation. Now that the data has // been sent ok, it's time to // report the partial failure. if (sendFailure) { // notify our listeners of the partial delivery. notifyTransportListeners(TransportEvent.MESSAGE_PARTIALLY_DELIVERED, sent, unsent, invalid, message); // get a list of chained exceptions for all of the failures (and // the successes, if reportSuccess has been // turned on). MessagingException failures = generateExceptionChain(stats, getReportSuccess()); // now send an "uber-exception" to indicate the failure. throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); } // notify our listeners of successful delivery. notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, sent, unsent, invalid, message); // we've not had any failures, but we've been asked to report // success as an exception. Do // this now. if (reportSuccess) { // generate the chain of success exceptions (we already know // there are no failure ones to report). MessagingException successes = generateExceptionChain(stats, reportSuccess); if (successes != null) { throw successes; } } } catch (SMTPSendFailedException e) { // if this is a send failure, we've already handled // notifications....just rethrow it. throw e; } catch (MessagingException e) { // notify of the error. notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); throw e; } } /** * 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(); } } /** * Turn a series of send status items into a chain of exceptions indicating * the state of each send operation. * * @param stats * The list of SendStatus items. * @param reportSuccess * Indicates whether we should include the report success items. * * @return The head of a chained list of MessagingExceptions. */ protected MessagingException generateExceptionChain(SendStatus[] stats, boolean reportSuccess) { MessagingException current = null; for (int i = 0; i < stats.length; i++) { SendStatus status = stats[i]; if (status != null) { MessagingException nextException = stats[i].getException(reportSuccess); // if there's an exception associated with this status, chain it // up with the rest. if (nextException != null) { if (current == null) { current = nextException; } else { current.setNextException(nextException); current = nextException; } } } } return current; } /** * Reset the server connection after an error. * * @exception MessagingException */ protected 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() != COMMAND_ACCEPTED) { close(); } // restore this. lastServerResponse = last; } /** * Expand the address list by converting any group addresses into single * address targets. * * @param addresses * The input array of addresses. * * @return The expanded array of addresses. * @exception MessagingException */ protected Address[] expandGroups(Address[] addresses) throws MessagingException { ArrayList expandedAddresses = new ArrayList(); // run the list looking for group addresses, and add the full group list // to our targets. for (int i = 0; i < addresses.length; i++) { InternetAddress address = (InternetAddress) addresses[i]; // not a group? Just copy over to the other list. if (!address.isGroup()) { expandedAddresses.add(address); } else { // get the group address and copy each member of the group into // the expanded list. InternetAddress[] groupAddresses = address.getGroup(true); for (int j = 1; j < groupAddresses.length; j++) { expandedAddresses.add(groupAddresses[j]); } } } // convert back into an array. return (Address[]) expandedAddresses.toArray(new Address[0]); } /** * Create a transport connection object and connect it to the target server. * * @param host * The target server host. * @param port * The connection port. * * @exception MessagingException */ protected void getConnection(String host, int port, String username, String password) throws IOException { this.host = host; this.port = port; this.username = username; this.password = password; // and see if STARTTLS is enabled. useTLS = isProtocolPropertyTrue(MAIL_SMTP_TLS); serverAuthenticationMechanisms = new HashMap(); // We might have been passed a socket to connect with...if not, we need // to create one of the correct type. if (socket == null) { // if this is the "smtps" protocol, we start with an SSLSocket if (sslConnection) { getConnectedSSLSocket(); } else { getConnectedSocket(); } } // if we already have a socket, get some information from it and // override what we've been passed. else { port = socket.getPort(); host = socket.getInetAddress().getHostName(); } // now set up the input/output streams. inputStream = new TraceInputStream(socket.getInputStream(), debugStream, debug, isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); ; outputStream = new TraceOutputStream(socket.getOutputStream(), debugStream, debug, isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); } /** * Get a property associated with this mail protocol. * * @param name * The name of the property. * * @return The property value (returns null if the property has not been * set). */ protected String getProtocolProperty(String name) { // the name we're given is the least qualified part of the name. We // construct the full property name // using the protocol (either "smtp" or "smtps"). String fullName = "mail." + protocol + "." + name; return getSessionProperty(fullName); } /** * Get a property associated with this mail session. * * @param name * The name of the property. * * @return The property value (returns null if the property has not been * set). */ protected String getSessionProperty(String name) { return session.getProperty(name); } /** * Get a property associated with this mail session. Returns the provided * default if it doesn't exist. * * @param name * The name of the property. * @param defaultValue * The default value to return if the property doesn't exist. * * @return The property value (returns defaultValue if the property has not * been set). */ protected String getSessionProperty(String name, String defaultValue) { String result = session.getProperty(name); if (result == null) { return defaultValue; } return result; } /** * Get a property associated with this mail session. Returns the provided * default if it doesn't exist. * * @param name * The name of the property. * @param defaultValue * The default value to return if the property doesn't exist. * * @return The property value (returns defaultValue if the property has not * been set). */ protected String getProtocolProperty(String name, String defaultValue) { // the name we're given is the least qualified part of the name. We // construct the full property name // using the protocol (either "smtp" or "smtps"). String fullName = "mail." + protocol + "." + name; return getSessionProperty(fullName, defaultValue); } /** * Get a property associated with this mail session as an integer value. * Returns the default value if the property doesn't exist or it doesn't * have a valid int value. * * @param name * The name of the property. * @param defaultValue * The default value to return if the property doesn't exist. * * @return The property value converted to an int. */ protected int getIntSessionProperty(String name, int defaultValue) { String result = getSessionProperty(name); if (result != null) { try { // convert into an int value. return Integer.parseInt(result); } catch (NumberFormatException e) { } } // return default value if it doesn't exist is isn't convertable. return defaultValue; } /** * Get a property associated with this mail session as an integer value. * Returns the default value if the property doesn't exist or it doesn't * have a valid int value. * * @param name * The name of the property. * @param defaultValue * The default value to return if the property doesn't exist. * * @return The property value converted to an int. */ protected int getIntProtocolProperty(String name, int defaultValue) { // the name we're given is the least qualified part of the name. We // construct the full property name // using the protocol (either "smtp" or "smtps"). String fullName = "mail." + protocol + "." + name; return getIntSessionProperty(fullName, defaultValue); } /** * Process a session property as a boolean value, returning either true or * false. * * @return True if the property value is "true". Returns false for any other * value (including null). */ protected boolean isProtocolPropertyTrue(String name) { // the name we're given is the least qualified part of the name. We // construct the full property name // using the protocol (either "smtp" or "smtps"). String fullName = "mail." + protocol + "." + name; return isSessionPropertyTrue(fullName); } /** * Process a session property as a boolean value, returning either true or * false. * * @return True if the property value is "true". Returns false for any other * value (including null). */ protected boolean isSessionPropertyTrue(String name) { String property = session.getProperty(name); if (property != null) { return property.equals("true"); } return false; } /** * Process a session property as a boolean value, returning either true or * false. * * @return True if the property value is "false". Returns false for other * value (including null). */ protected boolean isSessionPropertyFalse(String name) { String property = session.getProperty(name); if (property != null) { return property.equals("false"); } return false; } /** * Process a session property as a boolean value, returning either true or * false. * * @return True if the property value is "false". Returns false for other * value (including null). */ protected boolean isProtocolPropertyFalse(String name) { // the name we're given is the least qualified part of the name. We // construct the full property name // using the protocol (either "smtp" or "smtps"). String fullName = "mail." + protocol + "." + name; return isSessionPropertyTrue(fullName); } /** * Close the server connection at termination. */ protected void closeServerConnection() { try { socket.close(); } catch (IOException ignored) { } socket = null; inputStream = null; outputStream = null; } /** * Creates a connected socket * * @exception MessagingException */ protected void getConnectedSocket() throws IOException { if (debug) { debugOut("Attempting plain socket connection to server " + host + ":" + port); } // the socket factory can be specified via a session property. By // default, we just directly // instantiate a socket without using a factor. String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS); // there are several protocol properties that can be set to tune the // created socket. We need to // retrieve those bits before creating the socket. int timeout = getIntProtocolProperty(MAIL_SMTP_TIMEOUT, -1); InetAddress localAddress = null; // see if we have a local address override. String localAddrProp = getProtocolProperty(MAIL_SMTP_LOCALADDRESS); if (localAddrProp != null) { localAddress = InetAddress.getByName(localAddrProp); } // check for a local port...default is to allow socket to choose. int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0); socket = null; // if there is no socket factory defined (normal), we just create a // socket directly. if (socketFactory == null) { socket = new Socket(host, port, localAddress, localPort); } else { try { int socketFactoryPort = getIntProtocolProperty(MAIL_SMTP_FACTORY_PORT, -1); // we choose the port used by the socket based on overrides. Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort); // use the current context loader to resolve this. ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class factoryClass = loader.loadClass(socketFactory); // done indirectly, we need to invoke the method using // reflection. // This retrieves a factory instance. Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); Object defFactory = getDefault.invoke(new Object(), new Object[0]); // now that we have the factory, there are two different // createSocket() calls we use, // depending on whether we have a localAddress override. if (localAddress != null) { // retrieve the createSocket(String, int, InetAddress, int) // method. Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE }; Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) }; socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); } else { // retrieve the createSocket(String, int) method. Class[] createSocketSig = new Class[] { String.class, Integer.TYPE }; Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); Object[] createSocketArgs = new Object[] { host, portArg }; socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); } } catch (Throwable e) { // if a socket factor is specified, then we may need to fall // back to a default. This behavior // is controlled by (surprise) more session properties. if (isProtocolPropertyTrue(MAIL_SMTP_FACTORY_FALLBACK)) { if (debug) { debugOut("First plain socket attempt faile, falling back to default factory", e); } socket = new Socket(host, port, localAddress, localPort); } // we have an exception. We're going to throw an IOException, // which may require unwrapping // or rewrapping the exception. else { // we have an exception from the reflection, so unwrap the // base exception if (e instanceof InvocationTargetException) { e = ((InvocationTargetException) e).getTargetException(); } if (debug) { debugOut("Plain socket creation failure", e); } // throw this as an IOException, with the original exception // attached. IOException ioe = new IOException("Error connecting to " + host + ", " + port); ioe.initCause(e); throw ioe; } } } if (timeout >= 0) { socket.setSoTimeout(timeout); } } /** * Creates a connected SSL socket for an initial SSL connection. * * @exception MessagingException */ protected void getConnectedSSLSocket() throws IOException { if (debug) { debugOut("Attempting SSL socket connection to server " + host + ":" + port); } // the socket factory can be specified via a protocol property, a // session property, and if all else // fails (which it usually does), we fall back to the standard factory // class. String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS, getSessionProperty(MAIL_SSLFACTORY_CLASS, "javax.net.ssl.SSLSocketFactory")); // there are several protocol properties that can be set to tune the // created socket. We need to // retrieve those bits before creating the socket. int timeout = getIntProtocolProperty(MAIL_SMTP_TIMEOUT, -1); InetAddress localAddress = null; // see if we have a local address override. String localAddrProp = getProtocolProperty(MAIL_SMTP_LOCALADDRESS); if (localAddrProp != null) { localAddress = InetAddress.getByName(localAddrProp); } // check for a local port...default is to allow socket to choose. int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0); socket = null; // if there is no socket factory defined (normal), we just create a // socket directly. if (socketFactory == null) { socket = new Socket(host, port, localAddress, localPort); } else { // we'll try this with potentially two different factories if we're // allowed to fall back. boolean fallback = isProtocolPropertyTrue(MAIL_SMTP_FACTORY_FALLBACK); while (true) { try { if (debug) { debugOut("Creating SSL socket using factory " + socketFactory); } int socketFactoryPort = getIntProtocolProperty(MAIL_SMTP_FACTORY_PORT, -1); // we choose the port used by the socket based on overrides. Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort); // use the current context loader to resolve this. ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class factoryClass = loader.loadClass(socketFactory); // done indirectly, we need to invoke the method using // reflection. // This retrieves a factory instance. Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); Object defFactory = getDefault.invoke(new Object(), new Object[0]); // now that we have the factory, there are two different // createSocket() calls we use, // depending on whether we have a localAddress override. if (localAddress != null) { // retrieve the createSocket(String, int, InetAddress, // int) method. Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE }; Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) }; socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); } else { // retrieve the createSocket(String, int) method. Class[] createSocketSig = new Class[] { String.class, Integer.TYPE }; Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); Object[] createSocketArgs = new Object[] { host, portArg }; socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); } // now break out and configure the socket. break; } catch (Throwable e) { // if we're allowed to fallback, then use the default // factory and try this again. We only // allow this to happen once. if (fallback) { if (debug) { debugOut("First attempt at creating SSL socket failed, falling back to default factory"); } socketFactory = "javax.net.ssl.SSLSocketFactory"; fallback = false; continue; } // we have an exception. We're going to throw an // IOException, which may require unwrapping // or rewrapping the exception. else { // we have an exception from the reflection, so unwrap // the base exception if (e instanceof InvocationTargetException) { e = ((InvocationTargetException) e).getTargetException(); } if (debug) { debugOut("Failure creating SSL socket", e); } // throw this as an IOException, with the original // exception attached. IOException ioe = new IOException("Error connecting to " + host + ", " + port); ioe.initCause(e); throw ioe; } } } } if (timeout >= 0) { socket.setSoTimeout(timeout); } } /** * Switch the connection to using TLS level security, switching to an SSL * socket. */ protected void getConnectedTLSSocket() throws MessagingException { if (debug) { debugOut("Attempting to negotiate STARTTLS with server " + host); } // tell the server of our intention to start a TLS session SMTPReply line = sendCommand("STARTTLS"); if (line.getCode() != SERVICE_READY) { if (debug) { debugOut("STARTTLS command rejected by SMTP server " + host); } throw new MessagingException("Unable to make TLS server connection"); } // it worked, now switch the socket into TLS mode try { // we use the same target and port as the current connection. String host = socket.getInetAddress().getHostName(); int port = socket.getPort(); // the socket factory can be specified via a session property. By // default, we use // the native SSL factory. String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS, "javax.net.ssl.SSLSocketFactory"); // use the current context loader to resolve this. ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class factoryClass = loader.loadClass(socketFactory); // done indirectly, we need to invoke the method using reflection. // This retrieves a factory instance. Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); Object defFactory = getDefault.invoke(new Object(), new Object[0]); // now we need to invoke createSocket() Class[] createSocketSig = new Class[] { Socket.class, String.class, Integer.TYPE, Boolean.TYPE }; Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); Object[] createSocketArgs = new Object[] { socket, host, new Integer(port), Boolean.TRUE }; // and finally create the socket Socket sslSocket = (Socket) createSocket.invoke(defFactory, createSocketArgs); // if this is an instance of SSLSocket (very common), try setting // the protocol to be // "TLSv1". If this is some other class because of a factory // override, we'll just have to // accept that things will work. if (sslSocket instanceof SSLSocket) { ((SSLSocket) sslSocket).setEnabledProtocols(new String[] { "TLSv1" }); ((SSLSocket) sslSocket).setUseClientMode(true); ((SSLSocket) sslSocket).startHandshake(); } // and finally, as a last step, replace our input streams with the // secure ones. // now set up the input/output streams. inputStream = new TraceInputStream(sslSocket.getInputStream(), debugStream, debug, isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); ; outputStream = new TraceOutputStream(sslSocket.getOutputStream(), debugStream, debug, isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); // this is our active socket now socket = sslSocket; } catch (Exception e) { if (debug) { debugOut("Failure attempting to convert connection to TLS", e); } throw new MessagingException("Unable to convert connection to SSL", e); } } /** * Get the servers welcome blob from the wire.... */ protected boolean getWelcome() throws MessagingException { SMTPReply line = getReply(); return !line.isError(); } /** * 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(Message 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. OutputStream mimeOut = new MIMEOutputStream(outputStream); msg.writeTo(mimeOut); mimeOut.flush(); } catch (IOException e) { throw new MessagingException(e.toString()); } catch (MessagingException e) { throw new MessagingException(e.toString()); } // now to finish, we send a CRLF sequence, followed by a ".". sendLine(""); sendLine("."); // use a longer time out here to give the server time to process the // data. try { line = new SMTPReply(receiveLine(TIMEOUT * 2)); } catch (MalformedSMTPReplyException e) { throw new MessagingException(e.toString()); } catch (MessagingException e) { throw new MessagingException(e.toString()); } 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 (isProtocolPropertyTrue(MAIL_SMTP_QUITWAIT)) { // 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 notification address appended to the MAIL command. * * @return The status for this particular send operation. * @exception MessagingException */ protected 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 COMMAND_ACCEPTED: case 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 PARAMETER_SYNTAX_ERROR: case INVALID_COMMAND_SEQUENCE: case MAILBOX_NOT_FOUND: case INVALID_MAILBOX: case 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 SERVICE_NOT_AVAILABLE: case MAILBOX_BUSY: case PROCESSING_ERROR: case INSUFFICIENT_STORAGE: case 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); } } /** * Set the sender for this mail. * * @param message * The message we're sending. * * @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 = getProtocolProperty(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)); // 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 = getProtocolProperty(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 = getProtocolProperty(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")))); } 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 = getProtocolProperty(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() == COMMAND_ACCEPTED; } /** * 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 { System.out.println(">>>>>Sending data " + data + "<<<<<<"); outputStream.write(data.getBytes()); outputStream.write(CR); outputStream.write(LF); 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); } /** * Get a reply line for an SMTP command. * * @return An SMTP reply object from the stream. */ protected SMTPReply getReply() throws MessagingException { try { lastServerResponse = new SMTPReply(receiveLine()); } catch (MalformedSMTPReplyException e) { throw new MessagingException(e.toString()); } catch (MessagingException e) { throw e; } 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 String getLastServerResponse() { if (lastServerResponse == null) { return ""; } return lastServerResponse.getReply(); } /** * 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 = !isProtocolPropertyFalse(MAIL_SMTP_EHLO); // 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) { // if we've been told to use TLS, and this server doesn't support // it, then this is a failure if (!serverTLS) { throw new MessagingException("Server doesn't support required transport level security"); } // 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. serverAuthenticationMechanisms.clear(); if (!sendEhlo()) { throw new MessagingException("Failure sending EHLO command to SMTP server"); } } // this worked. return true; } /** * 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 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() != COMMAND_ACCEPTED) { return false; } // get a fresh extension mapping table. serverExtensionArgs = new HashMap(); // process all of the continuation lines while (line.isContinued()) { // get the next line line = getReply(); if (line.getCode() != COMMAND_ACCEPTED) { // all EHLO failures go back to the HELO failback step. return false; } // go process the extention processExtension(line.getMessage()); } return true; } /** * Send the HELO command to the SMTP server. * * @exception MessagingException */ protected void sendHelo() throws MessagingException { 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() != COMMAND_ACCEPTED) { throw new MessagingException("Failure sending HELO command to SMTP server"); } } /** * Retrieve the local client host name. * * @return The string version of the local host name. * @exception SMTPTransportException */ public String getLocalHost() throws MessagingException { if (localHost == null) { try { localHost = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { // fine, we're misconfigured - ignore } if (localHost == null) { localHost = getProtocolProperty(MAIL_SMTP_LOCALHOST); } if (localHost == null) { localHost = getSessionProperty(MAIL_LOCALHOST); } if (localHost == null) { throw new MessagingException("Can't get local hostname. " + " Please correctly configure JDK/DNS or set mail.smtp.localhost"); } } return localHost; } /** * 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; } /** * Return the current startTLS property. * * @return The current startTLS property. */ public boolean getStartTLS() { return reportSuccess; } /** * Set a new value for the startTLS property. * * @param start * The new setting. */ public void setStartTLS(boolean start) { useTLS = start; } /** * Retrieve the SASL realm used for DIGEST-MD5 authentication. This will * either be explicitly set, or retrieved using the mail.smtp.sasl.realm * session property. * * @return The current realm information (which can be null). */ public String getSASLRealm() { // if the realm is null, retrieve it using the realm session property. if (realm == null) { realm = getProtocolProperty(MAIL_SMTP_SASL_REALM); } return realm; } /** * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton. * * @param name * The new realm name. */ public void setSASLRealm(String name) { realm = name; } /** * Explicitly set the local host information. * * @param localHost * The new localHost name. */ public void setLocalHost(String localHost) { this.localHost = localHost; } /** * 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) { 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. serverExtensionArgs.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) { serverAuthenticationMechanisms.put("LOGIN", "LOGIN"); } else { // The security mechanisms are blank delimited tokens. StringTokenizer tokenizer = new StringTokenizer(argument); while (tokenizer.hasMoreTokens()) { String mechanism = tokenizer.nextToken().toUpperCase(); serverAuthenticationMechanisms.put(mechanism, mechanism); } } } // special case for some older servers. else if (extensionName.equals("AUTH=LOGIN")) { serverAuthenticationMechanisms.put("LOGIN", "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 (serverExtensionArgs != null) { return (String) serverExtensionArgs.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; } /** * Determine if the target server supports a given authentication mechanism. * * @param mechanism * The mechanism name. * * @return true if the server EHLO response indicates it supports the * mechanism, false otherwise. */ protected boolean supportsAuthentication(String mechanism) { return serverAuthenticationMechanisms.get(mechanism) != 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 (!isProtocolPropertyTrue(MAIL_SMTP_AUTH)) { return true; } // we need to authenticate, but we don't have userid/password // information...fail this // immediately. if (username == null || password == null) { return false; } ClientAuthenticator authenticator = null; // now go through the progression of mechanisms we support, from the // most secure to the // least secure. if (supportsAuthentication(AUTHENTICATION_DIGESTMD5)) { authenticator = new DigestMD5Authenticator(host, username, password, getSASLRealm()); } else if (supportsAuthentication(AUTHENTICATION_CRAMMD5)) { authenticator = new CramMD5Authenticator(username, password); } else if (supportsAuthentication(AUTHENTICATION_LOGIN)) { authenticator = new LoginAuthenticator(username, password); } else if (supportsAuthentication(AUTHENTICATION_PLAIN)) { authenticator = new PlainAuthenticator(username, password); } else { // can't find a mechanism we support in common return false; } 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 command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)))); // 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() == AUTHENTICATION_COMPLETE) { if (debug) { debugOut("Successful SMTP authentication"); } return true; } // we have an additional challenge to process. else if (line.getCode() == AUTHENTICATION_CHALLENGE) { // Does the authenticator think it is finished? We can't answer // an additional challenge, // so fail this. if (authenticator.isComplete()) { return false; } // we're passed back a challenge value, Base64 encoded. byte[] challenge = Base64.decode(line.getMessage().getBytes()); // have the authenticator evaluate and send back the encoded // response. sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)))); } // 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; } } } /** * Simple holder class for the address/send status duple, as we can have * mixed success for a set of addresses and a message */ 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; } } /** * Internal debug output routine. * * @param value * The string value to output. */ protected void debugOut(String message) { debugStream.println("SMTPTransport DEBUG: " + message); } /** * Internal debugging routine for reporting exceptions. * * @param message * A message associated with the exception context. * @param e * The received exception. */ protected void debugOut(String message, Throwable e) { debugOut("Received exception -> " + message); debugOut("Exception message -> " + e.getMessage()); e.printStackTrace(debugStream); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy