blob: 206f4ad77fc1de3b62e3bc04bbb259dd6c508efd [file] [log] [blame]
/*
* 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.List;
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.
* <p/> Supported properties : <p/>
* <ul>
* <li> mail.host : to set the server to deliver to. Default = localhost</li>
* <li> mail.smtp.port : to set the port. Default = 25</li>
* <li> mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()</li>
* </ul>
* <p/> 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... <p/> TODO : lots.
* ESMTP, user/pass, indicate failure, etc...
*
* @version $Rev$ $Date$
*/
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 {
if (debug) {
debugOut("Connecting to server " + host + ":" + port + " for user " + username);
}
// 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 = 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;
}
// 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);
}
}
// Before we do anything, let's make sure that we succesfully received a host
if ( host == null ) {
host = DEFAULT_MAIL_HOST;
}
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();
super.close();
}
}
/**
* 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();
// just return the error status...we don't care about any of the
// response information
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());
// 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());
}
} 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 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() != COMMAND_ACCEPTED) {
return false;
}
// get a fresh extension mapping table.
serverExtensionArgs = new HashMap();
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 {
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);
}
}