blob: b89988f122599382d2cb6eb81114bb4fa9f13613 [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.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
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.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import org.apache.geronimo.javamail.util.ProtocolProperties;
import org.apache.geronimo.javamail.transport.smtp.SMTPConnection.SendStatus;
/**
* 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: 995094 $ $Date: 2010-09-08 11:29:50 -0400 (Wed, 08 Sep 2010) $
*/
public class SMTPTransport extends Transport {
/**
* 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_DSN_NOTIFY = "dsn.notify";
protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial";
protected static final String MAIL_SMTP_EXTENSION = "mailextension";
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;
// do we use SSL for our initial connection?
protected boolean sslConnection = false;
// our accessor for protocol properties and the holder of
// protocol-specific information
protected ProtocolProperties props;
// our active connection object
protected SMTPConnection connection;
// the last response line received from the server.
protected SMTPReply lastServerResponse = null;
/**
* 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);
// create the protocol property holder. This gives an abstraction over the different
// flavors of the protocol.
props = new ProtocolProperties(session, protocol, sslConnection, defaultPort);
// the connection manages connection for the transport
connection = new SMTPConnection(props);
}
/**
* 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 {
connection.connect(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 {
// the connection pool handles all of the details here.
return connection.protocolConnect(host, port, username, password);
}
/**
* 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 reportSuccess = getReportSuccess();
// 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 = props.getBooleanProperty(MAIL_SMTP_SENDPARTIAL, false);
}
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 (!connection.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 = connection.getLastServerResponse();
// now send an "uber-exception" to indicate the failure.
throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent,
invalid);
}
// get the additional notification status, if available
String dsn = getDeliveryStatusNotification(message);
// 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 = connection.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) {
// 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.
connection.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
connection.sendData((MimeMessage)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, reportSuccess);
// 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;
}
}
/**
* Determine what delivery status notification should
* be added to the RCPT TO: command.
*
* @param message The message we're sending.
*
* @return The string NOTIFY= value to add to the command.
*/
protected String getDeliveryStatusNotification(Message message) {
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 = props.getProperty(MAIL_SMTP_DSN_NOTIFY);
}
return dsn;
}
/**
* 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 {
// This is done to ensure proper event notification.
super.close();
// NB: We reuse the connection if asked to reconnect
connection.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;
}
/**
* 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]);
}
/**
* Retrieve the local client host name.
*
* @return The string version of the local host name.
* @exception SMTPTransportException
*/
public String getLocalHost() throws MessagingException {
return connection.getLocalHost();
}
/**
* Explicitly set the local host information.
*
* @param localHost
* The new localHost name.
*/
public void setLocalHost(String localHost) {
connection.setLocalHost(localHost);
}
/**
* Return the current reportSuccess property.
*
* @return The current reportSuccess property.
*/
public boolean getReportSuccess() {
return connection.getReportSuccess();
}
/**
* Set a new value for the reportSuccess property.
*
* @param report
* The new setting.
*/
public void setReportSuccess(boolean report) {
connection.setReportSuccess(report);
}
/**
* Return the current startTLS property.
*
* @return The current startTLS property.
*/
public boolean getStartTLS() {
return connection.getStartTLS();
}
/**
* Set a new value for the startTLS property.
*
* @param start
* The new setting.
*/
public void setStartTLS(boolean start) {
connection.setStartTLS(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() {
return connection.getSASLRealm();
}
/**
* Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton.
*
* @param name
* The new realm name.
*/
public void setSASLRealm(String name) {
connection.setSASLRealm(name);
}
}