blob: 063e3836cc48eead8add36e2986e5c7fa05e60ed [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.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.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.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import javax.mail.Session;
import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
import org.apache.geronimo.javamail.util.CountingOutputStream;
import org.apache.geronimo.javamail.util.MailConnection;
import org.apache.geronimo.javamail.util.MIMEOutputStream;
import org.apache.geronimo.javamail.util.ProtocolProperties;
import org.apache.geronimo.mail.util.Base64;
import org.apache.geronimo.mail.util.XText;
/**
* Simple implementation of SMTP transport. Just does plain RFC977-ish delivery.
*
* @version $Rev$ $Date$
*/
public class SMTPConnection extends MailConnection {
protected static final String MAIL_SMTP_QUITWAIT = "quitwait";
protected static final String MAIL_SMTP_EXTENSION = "mailextension";
protected static final String MAIL_SMTP_EHLO = "ehlo";
protected static final String MAIL_SMTP_ALLOW8BITMIME = "allow8bitmime";
protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess";
protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable";
protected static final String MAIL_SMTP_STARTTLS_REQUIRED = "starttls.required";
protected static final String MAIL_SMTP_AUTH = "auth";
protected static final String MAIL_SMTP_FROM = "from";
protected static final String MAIL_SMTP_DSN_RET = "dsn.ret";
protected static final String MAIL_SMTP_SUBMITTER = "submitter";
/**
* property keys for protocol properties.
*/
protected static final int DEFAULT_NNTP_PORT = 119;
// the last response line received from the server.
protected SMTPReply lastServerResponse = null;
// do we report success after completion of each mail send.
protected boolean reportSuccess;
// does the server support transport level security?
protected boolean serverTLS = false;
// is TLS enabled on our part?
protected boolean useTLS = false;
// is TLS required on our part?
protected boolean requireTLS = false;
// should we use 8BITMIME encoding if supported by the server?
protected boolean use8bit = false;
/**
* Normal constructor for an SMTPConnection() object.
*
* @param props The property bundle for this protocol instance.
*/
public SMTPConnection(ProtocolProperties props) {
super(props);
// check to see if we need to throw an exception after a send operation.
reportSuccess = props.getBooleanProperty(MAIL_SMTP_REPORT_SUCCESS, false);
// and also check for TLS enablement.
useTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_ENABLE, false);
// and also check if TLS is required.
requireTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_REQUIRED, false);
// and also check for 8bitmime support
use8bit = props.getBooleanProperty(MAIL_SMTP_ALLOW8BITMIME, false);
}
/**
* Connect to the server and do the initial handshaking.
*
* @param host The target host name.
* @param port The target port
* @param username The connection username (can be null)
* @param password The authentication password (can be null).
*
* @return true if we were able to obtain a connection and
* authenticate.
* @exception MessagingException
*/
public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
// now check to see if we need to authenticate. If we need this, then
// we must have a username and
// password specified. Failing this may result in a user prompt to
// collect the information.
boolean mustAuthenticate = props.getBooleanProperty(MAIL_SMTP_AUTH, false);
// if we need to authenticate, and we don't have both a userid and
// password, then we fail this
// immediately. The Service.connect() method will try to obtain the user
// information and retry the
// connection one time.
if (mustAuthenticate && (username == null || password == null)) {
debugOut("Failing connection for missing authentication information");
return false;
}
super.protocolConnect(host, port, username, password);
try {
// create socket and connect to server.
getConnection();
// receive welcoming message
if (!getWelcome()) {
debugOut("Error getting welcome message");
throw new MessagingException("Error in getting welcome msg");
}
// say hello
if (!sendHandshake()) {
debugOut("Error getting processing handshake message");
throw new MessagingException("Error in saying EHLO to server");
}
// authenticate with the server, if necessary
if (!processAuthentication()) {
debugOut("User authentication failure");
throw new AuthenticationFailedException("Error authenticating with server");
}
} catch (IOException e) {
debugOut("I/O exception establishing connection", e);
throw new MessagingException("Connection error", e);
}
debugOut("Successful connection");
return true;
}
/**
* Close the connection. On completion, we'll be disconnected from the
* server and unable to send more data.
*
* @exception MessagingException
*/
public void close() throws MessagingException {
// if we're already closed, get outta here.
if (socket == null) {
return;
}
try {
// say goodbye
sendQuit();
} finally {
// and close up the connection. We do this in a finally block to
// make sure the connection
// is shut down even if quit gets an error.
closeServerConnection();
}
}
public String toString() {
return "SMTPConnection host: " + serverHost + " port: " + serverPort;
}
/**
* Set the sender for this mail.
*
* @param message
* The message we're sending.
*
* @return True if the command was accepted, false otherwise.
* @exception MessagingException
*/
protected boolean sendMailFrom(Message message) throws MessagingException {
// need to sort the from value out from a variety of sources.
String from = null;
// first potential source is from the message itself, if it's an
// instance of SMTPMessage.
if (message instanceof SMTPMessage) {
from = ((SMTPMessage) message).getEnvelopeFrom();
}
// if not available from the message, check the protocol property next
if (from == null || from.length() == 0) {
// the from value can be set explicitly as a property
from = props.getProperty(MAIL_SMTP_FROM);
}
// if not there, see if we have something in the message header.
if (from == null || from.length() == 0) {
Address[] fromAddresses = message.getFrom();
// if we have some addresses in the header, then take the first one
// as our From: address
if (fromAddresses != null && fromAddresses.length > 0) {
from = ((InternetAddress) fromAddresses[0]).getAddress();
}
// get what the InternetAddress class believes to be the local
// address.
else {
InternetAddress local = InternetAddress.getLocalAddress(session);
if (local != null) {
from = local.getAddress();
}
}
}
if (from == null || from.length() == 0) {
throw new MessagingException("no FROM address");
}
StringBuffer command = new StringBuffer();
// start building up the command
command.append("MAIL FROM: ");
command.append(fixEmailAddress(from));
// If the server supports the 8BITMIME extension, we might need to change the
// transfer encoding for the content to allow for direct transmission of the
// 8-bit codes.
if (supportsExtension("8BITMIME")) {
// we only do this if the capability was enabled via a property option or
// by explicitly setting the property on the message object.
if (use8bit || (message instanceof SMTPMessage && ((SMTPMessage)message).getAllow8bitMIME())) {
// make sure we add the BODY= option to the FROM message.
command.append(" BODY=8BITMIME");
// go check the content and see if the can convert the transfer encoding to
// allow direct 8-bit transmission.
if (convertTransferEncoding((MimeMessage)message)) {
// if we changed the encoding on any of the parts, then we
// need to save the message again
message.saveChanges();
}
}
}
// some servers ask for a size estimate on the initial send
if (supportsExtension("SIZE")) {
int estimate = getSizeEstimate(message);
if (estimate > 0) {
command.append(" SIZE=" + estimate);
}
}
// does this server support Delivery Status Notification? Then we may
// need to add some extra to the command.
if (supportsExtension("DSN")) {
String returnNotification = null;
// the return notification stuff might be set as value on the
// message object itself.
if (message instanceof SMTPMessage) {
// we need to convert the option into a string value.
switch (((SMTPMessage) message).getReturnOption()) {
case SMTPMessage.RETURN_FULL:
returnNotification = "FULL";
break;
case SMTPMessage.RETURN_HDRS:
returnNotification = "HDRS";
break;
}
}
// if not obtained from the message object, it can also be set as a
// property.
if (returnNotification == null) {
// the DSN value is set by yet another property.
returnNotification = props.getProperty(MAIL_SMTP_DSN_RET);
}
// if we have a target, add the notification stuff to our FROM
// command.
if (returnNotification != null) {
command.append(" RET=");
command.append(returnNotification);
}
}
// if this server supports AUTH and we have submitter information, then
// we also add the
// "AUTH=" keyword to the MAIL FROM command (see RFC 2554).
if (supportsExtension("AUTH")) {
String submitter = null;
// another option that can be specified on the message object.
if (message instanceof SMTPMessage) {
submitter = ((SMTPMessage) message).getSubmitter();
}
// if not part of the object, try for a propery version.
if (submitter == null) {
// we only send the extra keyword is a submitter is specified.
submitter = props.getProperty(MAIL_SMTP_SUBMITTER);
}
// we have one...add the keyword, plus the submitter info in xtext
// format (defined by RFC 1891).
if (submitter != null) {
command.append(" AUTH=");
try {
// add this encoded
command.append(new String(XText.encode(submitter.getBytes("US-ASCII")), "US-ASCII"));
} catch (UnsupportedEncodingException e) {
throw new MessagingException("Invalid submitter value " + submitter);
}
}
}
String extension = null;
// now see if we need to add any additional extension info to this
// command. The extension is not
// checked for validity. That's the reponsibility of the caller.
if (message instanceof SMTPMessage) {
extension = ((SMTPMessage) message).getMailExtension();
}
// this can come either from the object or from a set property.
if (extension == null) {
extension = props.getProperty(MAIL_SMTP_EXTENSION);
}
// have something real to add?
if (extension != null && extension.length() != 0) {
// tack this on the end with a blank delimiter.
command.append(' ');
command.append(extension);
}
// and finally send the command
SMTPReply line = sendCommand(command.toString());
// 250 response indicates success.
return line.getCode() == SMTPReply.COMMAND_ACCEPTED;
}
/**
* Check to see if a MIME body part can have its
* encoding changed from quoted-printable or base64
* encoding to 8bit encoding. In order for this
* to work, it must follow the rules laid out in
* RFC 2045. To qualify for conversion, the text
* must be:
*
* 1) No more than 998 bytes long
* 2) All lines are terminated with CRLF sequences
* 3) CR and LF characters only occur in properly
* formed line separators
* 4) No null characters are allowed.
*
* The conversion will only be applied to text
* elements, and this will recurse through the
* different elements of MultiPart content.
*
* @param bodyPart The bodyPart to convert. Initially, this will be
* the message itself.
*
* @return true if any conversion was performed, false if
* nothing was converted.
*/
protected boolean convertTransferEncoding(MimePart bodyPart)
{
boolean converted = false;
try {
// if this is a multipart element, apply the conversion rules
// to each of the parts.
if (bodyPart.isMimeType("multipart/")) {
MimeMultipart parts = (MimeMultipart)bodyPart.getContent();
for (int i = 0; i < parts.getCount(); i++) {
// convert each body part, and accumulate the conversion result
converted = converted && convertTransferEncoding((MimePart)parts.getBodyPart(i));
}
}
else {
// we only do this if the encoding is quoted-printable or base64
String encoding = bodyPart.getEncoding();
if (encoding != null) {
encoding = encoding.toLowerCase();
if (encoding.equals("quoted-printable") || encoding.equals("base64")) {
// this requires encoding. Read the actual content to see if
// it conforms to the 8bit encoding rules.
if (isValid8bit(bodyPart.getInputStream())) {
// There's a huge hidden gotcha lurking under the covers here.
// If the content just exists as an encoded byte array, then just
// switching the transfer encoding will mess things up because the
// already encoded data gets transmitted in encoded form, but with
// and 8bit encoding style. As a result, it doesn't get unencoded on
// the receiving end. This is a nasty problem to debug.
//
// The solution is to get the content as it's object type, set it back
// on the the message in raw form. Requesting the content will apply the
// current transfer encoding value to the data. Once we have set the
// content value back, we can reset the transfer encoding.
bodyPart.setContent(bodyPart.getContent(), bodyPart.getContentType());
// it's valid, so change the transfer encoding to just
// pass the data through.
bodyPart.setHeader("Content-Transfer-Encoding", "8bit");
converted = true; // we've changed something
}
}
}
}
} catch (MessagingException e) {
} catch (IOException e) {
}
return converted;
}
/**
* Get the server's welcome blob from the wire....
*/
protected boolean getWelcome() throws MessagingException {
SMTPReply line = getReply();
// just return the error status...we don't care about any of the
// response information
return !line.isError();
}
/**
* Get an estimate of the transmission size for this
* message. This size is the complete message as it is
* encoded and transmitted on the DATA command, not counting
* the terminating ".CRLF".
*
* @param msg The message we're sending.
*
* @return The count of bytes, if it can be calculated.
*/
protected int getSizeEstimate(Message msg) {
// now the data... I could look at the type, but
try {
CountingOutputStream outputStream = new CountingOutputStream();
// the data content has two requirements we need to meet by
// filtering the
// output stream. Requirement 1 is to conicalize any line breaks.
// All line
// breaks will be transformed into properly formed CRLF sequences.
//
// Requirement 2 is to perform byte-stuff for any line that begins
// with a "."
// so that data is not confused with the end-of-data marker (a
// "\r\n.\r\n" sequence.
//
// The MIME output stream performs those two functions on behalf of
// the content
// writer.
MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);
msg.writeTo(mimeOut);
// now to finish, we make sure there's a line break at the end.
mimeOut.forceTerminatingLineBreak();
// and flush the data to send it along
mimeOut.flush();
return outputStream.getCount();
} catch (IOException e) {
return 0; // can't get an estimate
} catch (MessagingException e) {
return 0; // can't get an estimate
}
}
/**
* Sends the data in the message down the socket. This presumes the server
* is in the right place and ready for getting the DATA message and the data
* right place in the sequence
*/
protected void sendData(MimeMessage msg) throws MessagingException {
// send the DATA command
SMTPReply line = sendCommand("DATA");
if (line.isError()) {
throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
}
// now the data... I could look at the type, but
try {
// the data content has two requirements we need to meet by
// filtering the
// output stream. Requirement 1 is to conicalize any line breaks.
// All line
// breaks will be transformed into properly formed CRLF sequences.
//
// Requirement 2 is to perform byte-stuff for any line that begins
// with a "."
// so that data is not confused with the end-of-data marker (a
// "\r\n.\r\n" sequence.
//
// The MIME output stream performs those two functions on behalf of
// the content
// writer.
MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);
msg.writeTo(mimeOut, new String[] {"Bcc", "Content-Length"});
// now to finish, we send a CRLF sequence, followed by a ".".
mimeOut.writeSMTPTerminator();
// and flush the data to send it along
mimeOut.flush();
this.outputStream.flush(); // most of the time MIMEOutputStream#flush does nothing so ensure we actually flush the data
} catch (IOException e) {
throw new MessagingException(e.toString());
} catch (MessagingException e) {
throw new MessagingException(e.toString());
}
// use a longer time out here to give the server time to process the
// data.
line = getReply(TIMEOUT * 2);
if (line.isError()) {
throw new MessagingException("Error issuing SMTP 'DATA' command: " + line);
}
}
/**
* Sends the QUIT message and receieves the response
*/
protected void sendQuit() throws MessagingException {
// there's yet another property that controls whether we should wait for
// a reply for a QUIT command. If true, we're suppposed to wait for a response
// from the QUIT command. Otherwise we just send the QUIT and bail. The default
// is "false"
if (props.getBooleanProperty(MAIL_SMTP_QUITWAIT, true)) {
// handle as a real command...we're going to ignore the response.
sendCommand("QUIT");
} else {
// just send the command without waiting for a response.
sendLine("QUIT");
}
}
/**
* Sets a receiver address for the current message
*
* @param addr
* The target address.
* @param dsn
* An optional DSN option appended to the RCPT TO command.
*
* @return The status for this particular send operation.
* @exception MessagingException
*/
public SendStatus sendRcptTo(InternetAddress addr, String dsn) throws MessagingException {
// compose the command using the fixed up email address. Normally, this
// involves adding
// "<" and ">" around the address.
StringBuffer command = new StringBuffer();
// compose the first part of the command
command.append("RCPT TO: ");
command.append(fixEmailAddress(addr.getAddress()));
// if we have DSN information, append it to the command.
if (dsn != null) {
command.append(" NOTIFY=");
command.append(dsn);
}
// get a string version of this command.
String commandString = command.toString();
SMTPReply line = sendCommand(commandString);
switch (line.getCode()) {
// these two are both successful transmissions
case SMTPReply.COMMAND_ACCEPTED:
case SMTPReply.ADDRESS_NOT_LOCAL:
// we get out of here with the status information.
return new SendStatus(SendStatus.SUCCESS, addr, commandString, line);
// these are considered invalid address errors
case SMTPReply.PARAMETER_SYNTAX_ERROR:
case SMTPReply.INVALID_COMMAND_SEQUENCE:
case SMTPReply.MAILBOX_NOT_FOUND:
case SMTPReply.INVALID_MAILBOX:
case SMTPReply.USER_NOT_LOCAL:
// we get out of here with the status information.
return new SendStatus(SendStatus.INVALID_ADDRESS, addr, commandString, line);
// the command was valid, but something went wrong in the server.
case SMTPReply.SERVICE_NOT_AVAILABLE:
case SMTPReply.MAILBOX_BUSY:
case SMTPReply.PROCESSING_ERROR:
case SMTPReply.INSUFFICIENT_STORAGE:
case SMTPReply.MAILBOX_FULL:
// we get out of here with the status information.
return new SendStatus(SendStatus.SEND_FAILURE, addr, commandString, line);
// everything else is considered really bad...
default:
// we get out of here with the status information.
return new SendStatus(SendStatus.GENERAL_ERROR, addr, commandString, line);
}
}
/**
* Send a command to the server, returning the first response line back as a
* reply.
*
* @param data
* The data to send.
*
* @return A reply object with the reply line.
* @exception MessagingException
*/
protected SMTPReply sendCommand(String data) throws MessagingException {
sendLine(data);
return getReply();
}
/**
* Sends a message down the socket and terminates with the appropriate CRLF
*/
protected void sendLine(String data) throws MessagingException {
if (socket == null || !socket.isConnected()) {
throw new MessagingException("no connection");
}
try { // don't write it in multiple times, ie build the data + "\r\n" string in memory to not get surprises on servers read() side
outputStream.write((data + "\r\n").getBytes("ISO8859-1"));
outputStream.flush();
} catch (IOException e) {
throw new MessagingException(e.toString());
}
}
/**
* Receives one line from the server. A line is a sequence of bytes
* terminated by a CRLF
*
* @return the line from the server as String
*/
protected String receiveLine() throws MessagingException {
return receiveLine(TIMEOUT);
}
protected SMTPReply getReply() throws MessagingException {
return getReply(TIMEOUT);
}
/**
* Get a reply line for an SMTP command.
*
* @return An SMTP reply object from the stream.
*/
protected SMTPReply getReply(int timeout) throws MessagingException {
try {
lastServerResponse = new SMTPReply(receiveLine(timeout));
// if the first line we receive is a continuation, continue
// reading lines until we reach the non-continued one.
while (lastServerResponse.isContinued()) {
lastServerResponse.addLine(receiveLine(timeout));
}
} catch (MalformedSMTPReplyException e) {
throw new MessagingException(e.toString());
}
return lastServerResponse;
}
/**
* Retrieve the last response received from the SMTP server.
*
* @return The raw response string (including the error code) returned from
* the SMTP server.
*/
public SMTPReply getLastServerResponse() {
return lastServerResponse;
}
/**
* Receives one line from the server. A line is a sequence of bytes
* terminated by a CRLF
*
* @return the line from the server as String
*/
protected String receiveLine(int delayMillis) throws MessagingException {
if (socket == null || !socket.isConnected()) {
throw new MessagingException("no connection");
}
int timeout = 0;
try {
// for now, read byte for byte, looking for a CRLF
timeout = socket.getSoTimeout();
socket.setSoTimeout(delayMillis);
StringBuffer buff = new StringBuffer();
int c;
boolean crFound = false, lfFound = false;
while ((c = inputStream.read()) != -1 && crFound == false && lfFound == false) {
// we're looking for a CRLF sequence, so mark each one as seen.
// Any other
// character gets appended to the end of the buffer.
if (c == CR) {
crFound = true;
} else if (c == LF) {
lfFound = true;
} else {
buff.append((char) c);
}
}
String line = buff.toString();
return line;
} catch (SocketException e) {
throw new MessagingException(e.toString());
} catch (IOException e) {
throw new MessagingException(e.toString());
} finally {
try {
socket.setSoTimeout(timeout);
} catch (SocketException e) {
// ignore - was just trying to do the decent thing...
}
}
}
/**
* Convert an InternetAddress into a form sendable on an SMTP mail command.
* InternetAddress.getAddress() generally returns just the address portion
* of the full address, minus route address markers. We need to ensure we
* have an address with '<' and '>' delimiters.
*
* @param mail
* The mail address returned from InternetAddress.getAddress().
*
* @return A string formatted for sending.
*/
protected String fixEmailAddress(String mail) {
if (mail.charAt(0) == '<') {
return mail;
}
return "<" + mail + ">";
}
/**
* Start the handshake process with the server, including setting up and
* TLS-level work. At the completion of this task, we should be ready to
* authenticate with the server, if needed.
*/
protected boolean sendHandshake() throws MessagingException {
// check to see what sort of initial handshake we need to make.
boolean useEhlo = props.getBooleanProperty(MAIL_SMTP_EHLO, true);
// if we're to use Ehlo, send it and then fall back to just a HELO
// message if it fails.
if (useEhlo) {
if (!sendEhlo()) {
sendHelo();
}
} else {
// send the initial hello response.
sendHelo();
}
if (useTLS || requireTLS) {
// if we've been told to use TLS
// if its not required and server does not support it we establish an unsecure connection
//see GERONIMO-5873 and GERONIMO-5430
if (requireTLS && !serverTLS) {
// if we've been told to use TLS, and this server doesn't support
// it, then this is a failure
throw new MessagingException("Server doesn't support required transport level security");
} else if (serverTLS){
// if the server supports TLS, then use it for the connection.
// on our connection.
getConnectedTLSSocket();
// some servers (gmail is one that I know of) only send a STARTTLS
// extension message on the
// first EHLO command. Now that we have the TLS handshaking
// established, we need to send a
// second EHLO message to retrieve the AUTH records from the server.
if (!sendEhlo()) {
throw new MessagingException("Failure sending EHLO command to SMTP server");
}
} else {
if (debug) {
debugOut("STARTTLS is enabled but not required and server does not support it. So we establish a connection without transport level security");
}
}
}
// this worked.
return true;
}
/**
* Switch the connection to using TLS level security, switching to an SSL
* socket.
*/
protected void getConnectedTLSSocket() throws MessagingException {
debugOut("Attempting to negotiate STARTTLS with server " + serverHost);
// tell the server of our intention to start a TLS session
SMTPReply line = sendCommand("STARTTLS");
if (line.getCode() != SMTPReply.SERVICE_READY) {
debugOut("STARTTLS command rejected by SMTP server " + serverHost);
throw new MessagingException("Unable to make TLS server connection");
}
debugOut("STARTTLS command accepted");
// the base class handles the socket switch details
super.getConnectedTLSSocket();
}
/**
* Send the EHLO command to the SMTP server.
*
* @return True if the command was accepted ok, false for any errors.
* @exception SMTPTransportException
* @exception MalformedSMTPReplyException
* @exception MessagingException
*/
protected boolean sendEhlo() throws MessagingException {
sendLine("EHLO " + getLocalHost());
SMTPReply reply = getReply();
// we get a 250 code back. The first line is just a greeting, and
// extensions are identifed on
// continuations. If this fails, then we'll try once more with HELO to
// establish bona fides.
if (reply.getCode() != SMTPReply.COMMAND_ACCEPTED) {
return false;
}
// create a fresh mapping and authentications table
capabilities = new HashMap();
authentications = new ArrayList();
List lines = reply.getLines();
// process all of the continuation lines
for (int i = 1; i < lines.size(); i++) {
// go process the extention
processExtension((String)lines.get(i));
}
return true;
}
/**
* Send the HELO command to the SMTP server.
*
* @exception MessagingException
*/
protected void sendHelo() throws MessagingException {
// create a fresh mapping and authentications table
// these will be empty, but it will prevent NPEs
capabilities = new HashMap();
authentications = new ArrayList();
sendLine("HELO " + getLocalHost());
SMTPReply line = getReply();
// we get a 250 code back. The first line is just a greeting, and
// extensions are identifed on
// continuations. If this fails, then we'll try once more with HELO to
// establish bona fides.
if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
throw new MessagingException("Failure sending HELO command to SMTP server");
}
}
/**
* Return the current startTLS property.
*
* @return The current startTLS property.
*/
public boolean getStartTLS() {
return useTLS;
}
/**
* Set a new value for the startTLS property.
*
* @param start
* The new setting.
*/
public void setStartTLS(boolean start) {
useTLS = start;
}
/**
* Return the current requireTLS property.
*
* @return The current requireTLS property.
*/
public boolean getRequireTLS() {
return requireTLS;
}
/**
* Set a new value for the requireTLS property.
*
* @param require
* The new setting.
*/
public void setRequireTLS(boolean require) {
requireTLS = require;
}
/**
* Process an extension string passed back as the EHLP response.
*
* @param extension
* The string value of the extension (which will be of the form
* "NAME arguments").
*/
protected void processExtension(String extension) {
debugOut("Processing extension " + extension);
String extensionName = extension.toUpperCase();
String argument = "";
int delimiter = extension.indexOf(' ');
// if we have a keyword with arguments, parse them out and add to the
// argument map.
if (delimiter != -1) {
extensionName = extension.substring(0, delimiter).toUpperCase();
argument = extension.substring(delimiter + 1);
}
// add this to the map so it can be tested later.
capabilities.put(extensionName, argument);
// process a few special ones that don't require extra parsing.
// AUTH and AUTH=LOGIN are handled the same
if (extensionName.equals("AUTH")) {
// if we don't have an argument on AUTH, this means LOGIN.
if (argument == null) {
authentications.add("LOGIN");
} else {
// The security mechanisms are blank delimited tokens.
StringTokenizer tokenizer = new StringTokenizer(argument);
while (tokenizer.hasMoreTokens()) {
String mechanism = tokenizer.nextToken().toUpperCase();
authentications.add(mechanism);
}
}
}
// special case for some older servers.
else if (extensionName.equals("AUTH=LOGIN")) {
authentications.add("LOGIN");
}
// does this support transport level security?
else if (extensionName.equals("STARTTLS")) {
// flag this for later
serverTLS = true;
}
}
/**
* Retrieve any argument information associated with a extension reported
* back by the server on the EHLO command.
*
* @param name
* The name of the target server extension.
*
* @return Any argument passed on a server extension. Returns null if the
* extension did not include an argument or the extension was not
* supported.
*/
public String extensionParameter(String name) {
if (capabilities != null) {
return (String)capabilities.get(name);
}
return null;
}
/**
* Tests whether the target server supports a named extension.
*
* @param name
* The target extension name.
*
* @return true if the target server reported on the EHLO command that is
* supports the targer server, false if the extension was not
* supported.
*/
public boolean supportsExtension(String name) {
// this only returns null if we don't have this extension
return extensionParameter(name) != null;
}
/**
* Authenticate with the server, if necessary (or possible).
*
* @return true if we are ok to proceed, false for an authentication
* failures.
*/
protected boolean processAuthentication() throws MessagingException {
// no authentication defined?
if (!props.getBooleanProperty(MAIL_SMTP_AUTH, false)) {
return true;
}
// we need to authenticate, but we don't have userid/password
// information...fail this
// immediately.
if (username == null || password == null) {
return false;
}
// if unable to get an appropriate authenticator, just fail it.
ClientAuthenticator authenticator = getSaslAuthenticator();
if (authenticator == null) {
throw new MessagingException("Unable to obtain SASL authenticator");
}
if (debug) {
debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
}
// if the authenticator has some initial data, we compose a command
// containing the initial data.
if (authenticator.hasInitialResponse()) {
StringBuffer command = new StringBuffer();
// the auth command initiates the handshaking.
command.append("AUTH ");
// and tell the server which mechanism we're using.
command.append(authenticator.getMechanismName());
command.append(" ");
// and append the response data
try {
command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)), "US-ASCII"));
} catch (UnsupportedEncodingException e) {
}
// send the command now
sendLine(command.toString());
}
// we just send an auth command with the command type.
else {
StringBuffer command = new StringBuffer();
// the auth command initiates the handshaking.
command.append("AUTH ");
// and tell the server which mechanism we're using.
command.append(authenticator.getMechanismName());
// send the command now
sendLine(command.toString());
}
// now process the challenge sequence. We get a 235 response back when
// the server accepts the
// authentication, and a 334 indicates we have an additional challenge.
while (true) {
// get the next line, and if it is an error response, return now.
SMTPReply line;
try {
line = new SMTPReply(receiveLine());
} catch (MalformedSMTPReplyException e) {
throw new MessagingException(e.toString());
} catch (MessagingException e) {
throw e;
}
// if we get a completion return, we've passed muster, so give an
// authentication response.
if (line.getCode() == SMTPReply.AUTHENTICATION_COMPLETE) {
debugOut("Successful SMTP authentication");
return true;
}
// we have an additional challenge to process.
else if (line.getCode() == SMTPReply.AUTHENTICATION_CHALLENGE) {
// Does the authenticator think it is finished? We can't answer
// an additional challenge,
// so fail this.
if (authenticator.isComplete()) {
return false;
}
try {
// we're passed back a challenge value, Base64 encoded.
byte[] challenge = Base64.decode(line.getMessage().getBytes("ISO8859-1"));
// have the authenticator evaluate and send back the encoded
// response.
sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)), "US-ASCII"));
} catch (UnsupportedEncodingException e) {
}
}
// completion or challenge are the only responses we know how to
// handle. Anything else must
// be a failure.
else {
if (debug) {
debugOut("Authentication failure " + line);
}
return false;
}
}
}
/**
* Attempt to retrieve a SASL authenticator for this
* protocol.
*
* @return A SASL authenticator, or null if a suitable one
* was not located.
*/
protected ClientAuthenticator getSaslAuthenticator() {
return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
}
/**
* Read the bytes in a stream a test to see if this
* conforms to the RFC 2045 rules for 8bit encoding.
*
* 1) No more than 998 bytes long
* 2) All lines are terminated with CRLF sequences
* 3) CR and LF characters only occur in properly
* formed line separators
* 4) No null characters are allowed.
*
* @param inStream The source input stream.
*
* @return true if this can be transmitted successfully
* using 8bit encoding, false if an alternate encoding
* will be required.
*/
protected boolean isValid8bit(InputStream inStream) {
try {
int ch;
int lineLength = 0;
while ((ch = inStream.read()) >= 0) {
// nulls are decidedly not allowed
if (ch == 0) {
return false;
}
// start of a CRLF sequence (potentially)
else if (ch == '\r') {
// check the next character. There must be one,
// and it must be a LF for this to be value
ch = inStream.read();
if (ch != '\n') {
return false;
}
// reset the line length
lineLength = 0;
}
else {
// a normal character
lineLength++;
// make sure the line is not too long
if (lineLength > 998) {
return false;
}
}
}
} catch (IOException e) {
return false; // can't read this, don't try passing it
}
// this converted ok
return true;
}
/**
* Simple holder class for the address/send status duple, as we can have
* mixed success for a set of addresses and a message
*/
static public class SendStatus {
public final static int SUCCESS = 0;
public final static int INVALID_ADDRESS = 1;
public final static int SEND_FAILURE = 2;
public final static int GENERAL_ERROR = 3;
// the status type of the send operation.
int status;
// the address associated with this status
InternetAddress address;
// the command string send to the server.
String cmd;
// the reply from the server.
SMTPReply reply;
/**
* Constructor for a SendStatus item.
*
* @param s
* The status type.
* @param a
* The address this is the status for.
* @param c
* The command string associated with this status.
* @param r
* The reply information from the server.
*/
public SendStatus(int s, InternetAddress a, String c, SMTPReply r) {
this.cmd = c;
this.status = s;
this.address = a;
this.reply = r;
}
/**
* Get the status information for this item.
*
* @return The current status code.
*/
public int getStatus() {
return this.status;
}
/**
* Retrieve the InternetAddress object associated with this send
* operation.
*
* @return The associated address object.
*/
public InternetAddress getAddress() {
return this.address;
}
/**
* Retrieve the reply information associated with this send operati
*
* @return The SMTPReply object received for the operation.
*/
public SMTPReply getReply() {
return reply;
}
/**
* Get the command string sent for this send operation.
*
* @return The command string for the MAIL TO command sent to the
* server.
*/
public String getCommand() {
return cmd;
}
/**
* Get an exception object associated with this send operation. There is
* a mechanism for reporting send success via a send operation, so this
* will be either a success or failure exception.
*
* @param reportSuccess
* Indicates if we want success operations too.
*
* @return A newly constructed exception object.
*/
public MessagingException getException(boolean reportSuccess) {
if (status != SUCCESS) {
return new SMTPAddressFailedException(address, cmd, reply.getCode(), reply.getMessage());
} else {
if (reportSuccess) {
return new SMTPAddressSucceededException(address, cmd, reply.getCode(), reply.getMessage());
}
}
return null;
}
}
/**
* Reset the server connection after an error.
*
* @exception MessagingException
*/
public void resetConnection() throws MessagingException {
// we want the caller to retrieve the last response responsbile for
// requiring the reset, so save and
// restore that info around the reset.
SMTPReply last = lastServerResponse;
// send a reset command.
SMTPReply line = sendCommand("RSET");
// if this did not reset ok, just close the connection
if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) {
close();
}
// restore this.
lastServerResponse = last;
}
/**
* Return the current reportSuccess property.
*
* @return The current reportSuccess property.
*/
public boolean getReportSuccess() {
return reportSuccess;
}
/**
* Set a new value for the reportSuccess property.
*
* @param report
* The new setting.
*/
public void setReportSuccess(boolean report) {
reportSuccess = report;
}
}