blob: ea64f36b078b4f33e0d498d719bf267f7c853e5e [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.
*
*/
/*
* The original version of this class was donated by Jason Hunter,
* who wrote the class as part of the com.oreilly.servlet
* package for his book "Java Servlet Programming" (O'Reilly).
* See http://www.servlets.com.
*
*/
package org.apache.tools.mail;
import java.io.IOException;
import java.io.PrintStream;
import java.io.BufferedOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.InetAddress;
import java.util.Vector;
import java.util.Enumeration;
/**
* A class to help send SMTP email.
* This class is an improvement on the sun.net.smtp.SmtpClient class
* found in the JDK. This version has extra functionality, and can be used
* with JVMs that did not extend from the JDK. It's not as robust as
* the JavaMail Standard Extension classes, but it's easier to use and
* easier to install, and has an Open Source license.
* <p>
* It can be used like this:
* <blockquote><pre>
* String mailhost = "localhost"; // or another mail host
* String from = "Mail Message Servlet &lt;MailMessage@server.com&gt;";
* String to = "to@you.com";
* String cc1 = "cc1@you.com";
* String cc2 = "cc2@you.com";
* String bcc = "bcc@you.com";
* &nbsp;
* MailMessage msg = new MailMessage(mailhost);
* msg.setPort(25);
* msg.from(from);
* msg.to(to);
* msg.cc(cc1);
* msg.cc(cc2);
* msg.bcc(bcc);
* msg.setSubject("Test subject");
* PrintStream out = msg.getPrintStream();
* &nbsp;
* Enumeration enum = req.getParameterNames();
* while (enum.hasMoreElements()) {
* String name = (String)enum.nextElement();
* String value = req.getParameter(name);
* out.println(name + " = " + value);
* }
* &nbsp;
* msg.sendAndClose();
* </pre></blockquote>
* <p>
* Be sure to set the from address, then set the recepient
* addresses, then set the subject and other headers, then get the
* PrintStream, then write the message, and finally send and close.
* The class does minimal error checking internally; it counts on the mail
* host to complain if there's any malformatted input or out of order
* execution.
* <p>
* An attachment mechanism based on RFC 1521 could be implemented on top of
* this class. In the meanwhile, JavaMail is the best solution for sending
* email with attachments.
* <p>
* Still to do:
* <ul>
* <li>Figure out how to close the connection in case of error
* </ul>
*
* @version 1.1, 2000/03/19, added angle brackets to address, helps some servers
* version 1.0, 1999/12/29
*/
public class MailMessage {
/** default mailhost */
public static final String DEFAULT_HOST = "localhost";
/** default port for SMTP: 25 */
public static final int DEFAULT_PORT = 25;
/** host name for the mail server */
private String host;
/** host port for the mail server */
private int port = DEFAULT_PORT;
/** sender email address */
private String from;
/** list of email addresses to reply to */
private Vector replyto;
/** list of email addresses to send to */
private Vector to;
/** list of email addresses to cc to */
private Vector cc;
/** headers to send in the mail */
private Vector headersKeys;
private Vector headersValues;
private MailPrintStream out;
private SmtpResponseReader in;
private Socket socket;
private static final int OK_READY = 220;
private static final int OK_HELO = 250;
private static final int OK_FROM = 250;
private static final int OK_RCPT_1 = 250;
private static final int OK_RCPT_2 = 251;
private static final int OK_DATA = 354;
private static final int OK_DOT = 250;
private static final int OK_QUIT = 221;
/**
* Constructs a new MailMessage to send an email.
* Use localhost as the mail server with port 25.
*
* @exception IOException if there's any problem contacting the mail server
*/
public MailMessage() throws IOException {
this(DEFAULT_HOST, DEFAULT_PORT);
}
/**
* Constructs a new MailMessage to send an email.
* Use the given host as the mail server with port 25.
*
* @param host the mail server to use
* @exception IOException if there's any problem contacting the mail server
*/
public MailMessage(String host) throws IOException {
this(host, DEFAULT_PORT);
}
/**
* Constructs a new MailMessage to send an email.
* Use the given host and port as the mail server.
*
* @param host the mail server to use
* @param port the port to connect to
* @exception IOException if there's any problem contacting the mail server
*/
public MailMessage(String host, int port) throws IOException {
this.port = port;
this.host = host;
replyto = new Vector();
to = new Vector();
cc = new Vector();
headersKeys = new Vector();
headersValues = new Vector();
connect();
sendHelo();
}
/**
* Set the port to connect to the SMTP host.
* @param port the port to use for connection.
* @see #DEFAULT_PORT
*/
public void setPort(int port) {
this.port = port;
}
/**
* Sets the from address. Also sets the "From" header. This method should
* be called only once.
* @param from the from address
* @exception IOException if there's any problem reported by the mail server
*/
public void from(String from) throws IOException {
sendFrom(from);
this.from = from;
}
/**
* Sets the replyto address
* This method may be
* called multiple times.
* @param rto the replyto address
*
*/
public void replyto(String rto) {
this.replyto.addElement(rto);
}
/**
* Sets the to address. Also sets the "To" header. This method may be
* called multiple times.
*
* @param to the to address
* @exception IOException if there's any problem reported by the mail server
*/
public void to(String to) throws IOException {
sendRcpt(to);
this.to.addElement(to);
}
/**
* Sets the cc address. Also sets the "Cc" header. This method may be
* called multiple times.
*
* @param cc the cc address
* @exception IOException if there's any problem reported by the mail server
*/
public void cc(String cc) throws IOException {
sendRcpt(cc);
this.cc.addElement(cc);
}
/**
* Sets the bcc address. Does NOT set any header since it's a *blind* copy.
* This method may be called multiple times.
*
* @param bcc the bcc address
* @exception IOException if there's any problem reported by the mail server
*/
public void bcc(String bcc) throws IOException {
sendRcpt(bcc);
// No need to keep track of Bcc'd addresses
}
/**
* Sets the subject of the mail message. Actually sets the "Subject"
* header.
* @param subj the subject of the mail message
*/
public void setSubject(String subj) {
setHeader("Subject", subj);
}
/**
* Sets the named header to the given value. RFC 822 provides the rules for
* what text may constitute a header name and value.
* @param name name of the header
* @param value contents of the header
*/
public void setHeader(String name, String value) {
// Blindly trust the user doesn't set any invalid headers
headersKeys.add(name);
headersValues.add(value);
}
/**
* Returns a PrintStream that can be used to write the body of the message.
* A stream is used since email bodies are byte-oriented. A writer can
* be wrapped on top if necessary for internationalization.
* This is actually done in Message.java
*
* @return a printstream containing the data and the headers of the email
* @exception IOException if there's any problem reported by the mail server
* @see org.apache.tools.ant.taskdefs.email.Message
*/
public PrintStream getPrintStream() throws IOException {
setFromHeader();
setReplyToHeader();
setToHeader();
setCcHeader();
setHeader("X-Mailer", "org.apache.tools.mail.MailMessage (ant.apache.org)");
sendData();
flushHeaders();
return out;
}
// RFC 822 s4.1: "From:" header must be sent
// We rely on error checking by the MTA
void setFromHeader() {
setHeader("From", from);
}
// RFC 822 s4.1: "Reply-To:" header is optional
void setReplyToHeader() {
if (!replyto.isEmpty()) {
setHeader("Reply-To", vectorToList(replyto));
}
}
void setToHeader() {
if (!to.isEmpty()) {
setHeader("To", vectorToList(to));
}
}
void setCcHeader() {
if (!cc.isEmpty()) {
setHeader("Cc", vectorToList(cc));
}
}
String vectorToList(Vector v) {
StringBuffer buf = new StringBuffer();
Enumeration e = v.elements();
while (e.hasMoreElements()) {
buf.append(e.nextElement());
if (e.hasMoreElements()) {
buf.append(", ");
}
}
return buf.toString();
}
void flushHeaders() throws IOException {
// RFC 822 s4.1:
// "Header fields are NOT required to occur in any particular order,
// except that the message body MUST occur AFTER the headers"
// (the same section specifies a reccommended order, which we ignore)
for (int i = 0; i < headersKeys.size(); i++) {
String name = (String) headersKeys.elementAt(i);
String value = (String) headersValues.elementAt(i);
out.println(name + ": " + value);
}
out.println();
out.flush();
}
/**
* Sends the message and closes the connection to the server.
* The MailMessage object cannot be reused.
*
* @exception IOException if there's any problem reported by the mail server
*/
public void sendAndClose() throws IOException {
try {
sendDot();
sendQuit();
} finally {
disconnect();
}
}
// Make a limited attempt to extract a sanitized email address
// Prefer text in <brackets>, ignore anything in (parentheses)
static String sanitizeAddress(String s) {
int paramDepth = 0;
int start = 0;
int end = 0;
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (c == '(') {
paramDepth++;
if (start == 0) {
end = i; // support "address (name)"
}
} else if (c == ')') {
paramDepth--;
if (end == 0) {
start = i + 1; // support "(name) address"
}
} else if (paramDepth == 0 && c == '<') {
start = i + 1;
} else if (paramDepth == 0 && c == '>') {
end = i;
}
}
if (end == 0) {
end = len;
}
return s.substring(start, end);
}
// * * * * * Raw protocol methods below here * * * * *
void connect() throws IOException {
socket = new Socket(host, port);
out = new MailPrintStream(
new BufferedOutputStream(
socket.getOutputStream()));
in = new SmtpResponseReader(socket.getInputStream());
getReady();
}
void getReady() throws IOException {
String response = in.getResponse();
int[] ok = {OK_READY};
if (!isResponseOK(response, ok)) {
throw new IOException(
"Didn't get introduction from server: " + response);
}
}
void sendHelo() throws IOException {
String local = InetAddress.getLocalHost().getHostName();
int[] ok = {OK_HELO};
send("HELO " + local, ok);
}
void sendFrom(String from) throws IOException {
int[] ok = {OK_FROM};
send("MAIL FROM: " + "<" + sanitizeAddress(from) + ">", ok);
}
void sendRcpt(String rcpt) throws IOException {
int[] ok = {OK_RCPT_1, OK_RCPT_2};
send("RCPT TO: " + "<" + sanitizeAddress(rcpt) + ">", ok);
}
void sendData() throws IOException {
int[] ok = {OK_DATA};
send("DATA", ok);
}
void sendDot() throws IOException {
int[] ok = {OK_DOT};
send("\r\n.", ok); // make sure dot is on new line
}
void sendQuit() throws IOException {
int[] ok = {OK_QUIT};
try {
send("QUIT", ok);
} catch (IOException e) {
throw new ErrorInQuitException(e);
}
}
void send(String msg, int[] ok) throws IOException {
out.rawPrint(msg + "\r\n"); // raw supports <CRLF>.<CRLF>
String response = in.getResponse();
if (!isResponseOK(response, ok)) {
throw new IOException("Unexpected reply to command: "
+ msg + ": " + response);
}
}
boolean isResponseOK(String response, int[] ok) {
// Check that the response is one of the valid codes
for (int i = 0; i < ok.length; i++) {
if (response.startsWith("" + ok[i])) {
return true;
}
}
return false;
}
void disconnect() throws IOException {
if (out != null) {
out.close();
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
// ignore
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// ignore
}
}
}
}
/**
* This PrintStream subclass makes sure that <CRLF>. becomes <CRLF>..
* per RFC 821. It also ensures that new lines are always \r\n.
*/
class MailPrintStream extends PrintStream {
private int lastChar;
public MailPrintStream(OutputStream out) {
super(out, true); // deprecated, but email is byte-oriented
}
// Mac does \n\r, but that's tough to distinguish from Windows \r\n\r\n.
// Don't tackle that problem right now.
public void write(int b) {
if (b == '\n' && lastChar != '\r') {
rawWrite('\r'); // ensure always \r\n
rawWrite(b);
} else if (b == '.' && lastChar == '\n') {
rawWrite('.'); // add extra dot
rawWrite(b);
} else {
rawWrite(b);
}
lastChar = b;
}
public void write(byte[] buf, int off, int len) {
for (int i = 0; i < len; i++) {
write(buf[off + i]);
}
}
void rawWrite(int b) {
super.write(b);
}
void rawPrint(String s) {
int len = s.length();
for (int i = 0; i < len; i++) {
rawWrite(s.charAt(i));
}
}
}