/**
*
* Copyright 2003-2005 The Apache Software Foundation
*
* Licensed 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.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.URLName;
import javax.mail.Transport;
/**
* 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
*/
private static final char CR = 0x0D;
private static final char LF = 0x0A;
/**
* property key for SMTP server to talk to
*/
private static final String MAIL_HOST = "mail.host";
private static final String MAIL_SMTP_LOCALHOST = "mail.smtp.localhost";
private static final String MAIL_SMTP_PORT = "mail.smtp.port";
private static final int MIN_MILLIS = 1000 * 60;
private static final String DEFAULT_MAIL_HOST = "localhost";
private static final int DEFAULT_MAIL_SMTP_PORT = 25;
/**
* @param session
* @param name
*/
public SMTPTransport(Session session, URLName name) {
super(session, name);
}
public void sendMessage(Message message, Address[] addresses) throws MessagingException {
// do it and ignore the return
sendMessage(addresses, message);
}
public SendStatus[] sendMessage(Address[] addresses, Message message) throws MessagingException {
// don't bother me w/ null messages or no addreses
if (message == null) {
throw new MessagingException("Null message");
}
if (addresses == null || addresses.length == 0) {
throw new MessagingException("Null or empty address array");
}
SendStatus[] stat = new SendStatus[addresses.length];
try {
// create socket and connect to server.
Socket s = getConnectedSocket();
// receive welcoming message
if (!getWelcome(s)) {
throw new MessagingException("Error in getting welcome msg");
}
// say hello
if (!sendHelo(s)) {
throw new MessagingException("Error in saying HELO to server");
}
// send sender
if (!sendMailFrom(s, message.getFrom())) {
throw new MessagingException("Error in setting the MAIL FROM");
}
// send recipients. Only send if not null or "", and just ignore (but log) any errors
for (int i = 0; i < addresses.length; i++) {
String to = addresses[i].toString();
int status = SendStatus.SUCCESS;
if (to != null && !"".equals(to)) {
if (!sendRcptTo(s, to)) {
// this means it didn't like our recipient. I say we keep going
if (this.session.getDebug()) {
this.session.getDebugOut().println("ERROR setting recipient " + to);
}
status = SendStatus.FAIL;
}
} else {
status = SendStatus.FAIL;
}
stat[i] = new SendStatus(status, to);
}
// send data
if (!sendData(s, message)) {
throw new MessagingException("Error sending data");
}
// say goodbye
sendQuit(s);
try {
s.close();
} catch (IOException ignored) {
}
} catch (SMTPTransportException e) {
throw new MessagingException("error", e);
} catch (MalformedSMTPReplyException e) {
throw new MessagingException("error", e);
}
return stat;
}
/**
* 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 boolean sendData(Socket s, Message msg) throws SMTPTransportException, MalformedSMTPReplyException {
if (msg == null) {
throw new SMTPTransportException("invalid message");
}
// send the DATA command
sendLine(s, "DATA");
SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
if (this.session.getDebug()) {
this.session.getDebugOut().println(line);
}
if (line.isError()) {
return false;
}
// now the data... I could look at the type, but
try {
OutputStream os = s.getOutputStream();
// todo - be smarter here and send in chunks to let the other side
// digest easier. There's a 3 min recommended timeout per chunk...
msg.writeTo(os);
os.flush();
} catch (IOException e) {
throw new SMTPTransportException(e);
} catch (MessagingException e) {
throw new SMTPTransportException(e);
}
// now to finish
sendLine(s, "");
sendLine(s, ".");
line = new SMTPReply(receiveLine(s, 10 * MIN_MILLIS));
return !line.isError();
}
/**
* Sends the QUIT message and receieves the response
*/
protected boolean sendQuit(Socket s) throws SMTPTransportException, MalformedSMTPReplyException {
sendLine(s, "QUIT");
SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
return !line.isError();
}
/**
* Sets a receiver address for the current message
*/
protected boolean sendRcptTo(Socket s, String addr) throws SMTPTransportException, MalformedSMTPReplyException {
if (addr == null || "".equals(addr)) {
throw new SMTPTransportException("invalid address");
}
String msg = "RCPT TO: " + fixEmailAddress(addr);
sendLine(s, msg);
SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
return !line.isError();
}
/**
* Set the sender for this mail.
*/
protected boolean sendMailFrom(Socket s, Address[] from) throws SMTPTransportException, MalformedSMTPReplyException {
if (from == null || from.length == 0) {
throw new SMTPTransportException("no FROM address");
}
// TODO - what do we do w/ more than one from???
String msg = "MAIL FROM: " + fixEmailAddress(from[0].toString());
sendLine(s, msg);
SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
return !line.isError();
}
/**
* Sends the initiating "HELO" message. We're keeping it simple, just
* identifying ourselves as we dont' require service extensions, and
* want to keep it simple for now
*
* @param s socket we are talking on. It's assumed to be open and in
* right state for this message
*/
protected boolean sendHelo(Socket s) throws SMTPTransportException, MalformedSMTPReplyException {
String fqdm = null;
try {
fqdm = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
// fine, we're misconfigured - ignore
}
if (fqdm == null) {
fqdm = session.getProperty(MAIL_SMTP_LOCALHOST);
}
if (fqdm == null) {
throw new SMTPTransportException("Can't get local hostname. " +
" Please correctly configure JDK/DNS or set mail.smtp.localhost");
}
sendLine(s, "HELO " + fqdm);
SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
return !line.isError();
}
/**
* Get the servers welcome blob from the wire....
*/
protected boolean getWelcome(Socket s) throws SMTPTransportException, MalformedSMTPReplyException {
SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
return !line.isError();
}
/**
* Sends a message down the socket and terminates with the
* appropriate CRLF
*/
protected void sendLine(Socket s, String data) throws SMTPTransportException {
if (s == null) {
throw new SMTPTransportException("bonehead...");
}
if (!s.isConnected()) {
throw new SMTPTransportException("no connection");
}
try {
OutputStream out = s.getOutputStream();
out.write(data.getBytes());
out.write(CR);
out.write(LF);
out.flush();
if (this.session.getDebug()) {
this.session.getDebugOut().println("sent: " + data);
}
} catch (IOException e) {
throw new SMTPTransportException(e);
}
}
/**
* Receives one line from the server. A line is a sequence of bytes
* terminated by a CRLF
*
* @param s socket to receive from
* @return the line from the server as String
*/
protected String receiveLine(Socket s, int delayMillis) throws SMTPTransportException {
if (s == null) {
throw new SMTPTransportException("bonehead...");
}
if (!s.isConnected()) {
throw new SMTPTransportException("no connection");
}
int timeout = 0;
try {
// for now, read byte for byte, looking for a CRLF
timeout = s.getSoTimeout();
s.setSoTimeout(delayMillis);
InputStream is = s.getInputStream();
StringBuffer buff = new StringBuffer();
int c;
boolean crFound = false, lfFound = false;
while ((c = is.read()) != -1 && crFound == false && lfFound == false) {
buff.append((char) c);
if (c == CR) {
crFound = true;
}
if (c == LF) {
lfFound = true;
}
}
if (this.session.getDebug()) {
this.session.getDebugOut().println("received : " + buff.toString());
}
return buff.toString();
} catch (SocketException e) {
throw new SMTPTransportException(e);
} catch (IOException e) {
throw new SMTPTransportException(e);
} finally {
try {
s.setSoTimeout(timeout);
} catch (SocketException e) {
// ignore - was just trying to do the decent thing...
}
}
}
/**
* Creates and returns a connected socket
*/
protected Socket getConnectedSocket() throws MessagingException {
Socket s = new Socket();
String mail_host = this.session.getProperty(MAIL_HOST);
if (mail_host == null || "".equals(mail_host)) {
mail_host = DEFAULT_MAIL_HOST;
}
String portString = this.session.getProperty(MAIL_SMTP_PORT);
int port = DEFAULT_MAIL_SMTP_PORT;
if (portString != null && !"".equals(portString)) {
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
// ignore - we don't care, leave as default
}
}
try {
if (this.session.getDebug()) {
this.session.getDebugOut().println("connecting to " + mail_host);
}
s.connect(new InetSocketAddress(mail_host, port));
if (this.session.getDebug()) {
this.session.getDebugOut().println("connected to " + mail_host);
}
} catch (IOException e) {
if (this.session.getDebug()) {
this.session.getDebugOut().println("error connecting to " + mail_host);
}
throw new MessagingException("Error connecting to " + mail_host, e);
}
return s;
}
private String fixEmailAddress(String mail) {
if (mail.charAt(0) == '<') {
return mail;
}
return "<" + mail + ">";
}
/**
* 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 FAIL = 1;
final int status;
final String address;
public SendStatus(int s, String a) {
this.status = s;
this.address = a;
}
public int getStatus() {
return this.status;
}
public String getAddress() {
return this.address;
}
}
}