001// Copyright (C) 1999-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
002// All rights reserved.  Use of this class is limited.
003// Please see the LICENSE for more information.
004
005package com.oreilly.servlet;
006
007import java.io.*;
008import java.net.*;
009import java.util.*;
010
011/** 
012 * A class to help send SMTP email.  It can be used by any Java program, not
013 * just servlets.  Servlets are likely to use this class to:
014 * <ul>
015 * <li>Send submitted form data to interested parties
016 * <li>Send an email page to an administrator in case of error
017 * <li>Send the client an order confirmation
018 * </ul>
019 * <p>
020 * This class is an improvement on the sun.net.smtp.SmtpClient class 
021 * found in the JDK.  This version has extra functionality, and can be used
022 * with JVMs that did not extend from the JDK.  It's not as robust as
023 * the JavaMail Standard Extension classes, but it's easier to use and 
024 * easier to install.
025 * <p>
026 * It can be used like this:
027 * <blockquote><pre>
028 * String mailhost = "localhost";  // or another mail host
029 * String from = "Mail Message Servlet <MailMessage@somedomain.com>";
030 * String to = "to@somedomain.com";
031 * String cc1 = "cc1@somedomain.com";
032 * String cc2 = "cc2@somedomain.com";
033 * String bcc = "bcc@somedomain.com";
034 * &nbsp;
035 * MailMessage msg = new MailMessage(mailhost);
036 * msg.from(from);
037 * msg.to(to);
038 * msg.cc(cc1);
039 * msg.cc(cc2);
040 * msg.bcc(bcc);
041 * msg.setSubject("Test subject");
042 * PrintStream out = msg.getPrintStream();
043 * &nbsp;
044 * Enumeration myEnum = req.getParameterNames();
045 * while (myEnum.hasMoreElements()) {
046 *   String name = (String)myEnum.nextElement();
047 *   String value = req.getParameter(name);
048 *   out.println(name + " = " + value);
049 * }
050 * &nbsp;
051 * msg.sendAndClose();
052 * </pre></blockquote>
053 * <p>
054 * Be sure to set the from address, then set the recepient 
055 * addresses, then set the subject and other headers, then get the 
056 * PrintStream, then write the message, and finally send and close.
057 * The class does minimal error checking internally; it counts on the mail
058 * host to complain if there's any malformatted input or out of order 
059 * execution.  
060 * <p>
061 * An attachment mechanism based on RFC 1521 could be implemented on top of
062 * this class.  In the meanwhile, JavaMail is the best solution for sending
063 * email with attachments.
064 * <p>
065 * Still to do:
066 * <ul>
067 * <li>Figure out how to close the connection in case of error
068 * </ul>
069 *
070 * @author <b>Jason Hunter</b>, Copyright &#169; 1999
071 * @version 1.2, 2002/11/01, added logic to suppress CC: header if no CC addrs
072 * @version 1.1, 2000/03/19, added angle brackets to address, helps some servers
073 * @version 1.0, 1999/12/29
074 */
075public class MailMessage {
076
077  String host;
078  String from;
079  Vector to, cc;
080  Hashtable headers;
081  MailPrintStream out;
082  BufferedReader in;
083  Socket socket;
084
085  /**
086   * Constructs a new MailMessage to send an email.
087   * Use localhost as the mail server.
088   *
089   * @exception IOException if there's any problem contacting the mail server
090   */
091  public MailMessage() throws IOException {
092    this("localhost");
093  }
094
095  /**
096   * Constructs a new MailMessage to send an email.
097   * Use the given host as the mail server.
098   *
099   * @param host the mail server to use
100   * @exception IOException if there's any problem contacting the mail server
101   */
102  public MailMessage(String host) throws IOException {
103    this.host = host;
104    to = new Vector();
105    cc = new Vector();
106    headers = new Hashtable();
107    setHeader("X-Mailer", "com.oreilly.servlet.MailMessage (www.servlets.com)");
108    connect();
109    sendHelo();
110  }
111
112  /**
113   * Sets the from address.  Also sets the "From" header.  This method should
114   * be called only once.
115   *
116   * @exception IOException if there's any problem reported by the mail server
117   */
118  public void from(String from) throws IOException {
119    sendFrom(from);
120    this.from = from;
121  }
122
123  /**
124   * Sets the to address.  Also sets the "To" header.  This method may be
125   * called multiple times.
126   *
127   * @exception IOException if there's any problem reported by the mail server
128   */
129  public void to(String to) throws IOException {
130    sendRcpt(to);
131    this.to.addElement(to);
132  }
133
134  /**
135   * Sets the cc address.  Also sets the "Cc" header.  This method may be
136   * called multiple times.
137   *
138   * @exception IOException if there's any problem reported by the mail server
139   */
140  public void cc(String cc) throws IOException {
141    sendRcpt(cc);
142    this.cc.addElement(cc);
143  }
144
145  /**
146   * Sets the bcc address.  Does NOT set any header since it's a *blind* copy.
147   * This method may be called multiple times.
148   *
149   * @exception IOException if there's any problem reported by the mail server
150   */
151  public void bcc(String bcc) throws IOException {
152    sendRcpt(bcc);
153    // No need to keep track of Bcc'd addresses
154  }
155
156  /**
157   * Sets the subject of the mail message.  Actually sets the "Subject" 
158   * header.
159   */
160  public void setSubject(String subj) {
161    headers.put("Subject", subj);
162  }
163
164  /**
165   * Sets the named header to the given value.  RFC 822 provides the rules for
166   * what text may constitute a header name and value.
167   */
168  public void setHeader(String name, String value) {
169    // Blindly trust the user doesn't set any invalid headers
170    headers.put(name, value);
171  }
172
173  /**
174   * Returns a PrintStream that can be used to write the body of the message.
175   * A stream is used since email bodies are byte-oriented.  A writer could 
176   * be wrapped on top if necessary for internationalization.
177   *
178   * @exception IOException if there's any problem reported by the mail server
179   */
180  public PrintStream getPrintStream() throws IOException {
181    setFromHeader();
182    setToHeader();
183    setCcHeader();
184    sendData();
185    flushHeaders();
186    return out;
187  }
188
189  void setFromHeader() {
190    setHeader("From", from);
191  }
192
193  void setToHeader() {
194    setHeader("To", vectorToList(to));
195  }
196
197  void setCcHeader() {
198    if (!cc.isEmpty()) {  // thanks to Patrice, patricek_97@yahoo.com
199      setHeader("Cc", vectorToList(cc));
200    }
201  }
202
203  String vectorToList(Vector v) {
204    StringBuffer buf = new StringBuffer();
205    Enumeration e = v.elements();
206    while (e.hasMoreElements()) {
207      buf.append(e.nextElement());
208      if (e.hasMoreElements()) {
209        buf.append(", ");
210      }
211    }
212    return buf.toString();
213  }
214
215  void flushHeaders() throws IOException {
216    // XXX Should I care about order here?
217    Enumeration e = headers.keys();
218    while (e.hasMoreElements()) {
219      String name = (String) e.nextElement();
220      String value = (String) headers.get(name);
221      out.println(name + ": " + value);
222    }
223    out.println();
224    out.flush();
225  }
226
227  /**
228   * Sends the message and closes the connection to the server.
229   * The MailMessage object cannot be reused.
230   *
231   * @exception IOException if there's any problem reported by the mail server
232   */
233  public void sendAndClose() throws IOException {
234    sendDot();
235    disconnect();
236  }
237
238  // Make a limited attempt to extract a sanitized email address
239  // Prefer text in <brackets>, ignore anything in (parentheses)
240  static String sanitizeAddress(String s) {
241    int paramDepth = 0;
242    int start = 0;
243    int end = 0;
244    int len = s.length();
245
246    for (int i = 0; i < len; i++) {
247      char c = s.charAt(i);
248      if (c == '(') {
249        paramDepth++;
250        if (start == 0) {
251          end = i;  // support "address (name)"
252        }
253      }
254      else if (c == ')') {
255        paramDepth--;
256        if (end == 0) {
257          start = i + 1;  // support "(name) address"
258        }
259      }
260      else if (paramDepth == 0 && c == '<') {
261        start = i + 1;
262      }
263      else if (paramDepth == 0 && c == '>') {
264        end = i;
265      }
266    }
267
268    if (end == 0) {
269      end = len;
270    }
271
272    return s.substring(start, end);
273  }
274
275  // * * * * * Raw protocol methods below here * * * * *
276
277  void connect() throws IOException {
278    socket = new Socket(host, 25);
279    out = new MailPrintStream(
280          new BufferedOutputStream(
281          socket.getOutputStream())); 
282    in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 
283    getReady();
284  }
285
286  void getReady() throws IOException {
287    String response = in.readLine();
288    int[] ok = { 220 };
289    if (!isResponseOK(response, ok)) {
290      throw new IOException(
291        "Didn't get introduction from server: " + response);
292    }
293  }
294
295  void sendHelo() throws IOException {
296    String local = InetAddress.getLocalHost().getHostName();
297    int[] ok = { 250 };
298    send("HELO " + local, ok);
299  }
300
301  void sendFrom(String from) throws IOException {
302    int[] ok = { 250 };
303    send("MAIL FROM: " + "<" + sanitizeAddress(from) + ">", ok);
304  }
305
306  void sendRcpt(String rcpt) throws IOException {
307    int[] ok = { 250, 251 };
308    send("RCPT TO: " + "<" + sanitizeAddress(rcpt) + ">", ok);
309  }
310
311  void sendData() throws IOException {
312    int[] ok = { 354 };
313    send("DATA", ok);
314  }
315
316  void sendDot() throws IOException {
317    int[] ok = { 250 };
318    send("\r\n.", ok);  // make sure dot is on new line
319  }
320
321  void sendQuit() throws IOException {
322    int[] ok = { 221 };
323    send("QUIT", ok);
324  }
325
326  void send(String msg, int[] ok) throws IOException {
327    out.rawPrint(msg + "\r\n");  // raw supports <CRLF>.<CRLF>
328    //System.out.println("S: " + msg);
329    String response = in.readLine();
330    //System.out.println("R: " + response);
331    if (!isResponseOK(response, ok)) {
332      throw new IOException(
333        "Unexpected reply to command: " + msg + ": " + response);
334    }
335  }
336
337  boolean isResponseOK(String response, int[] ok) {
338    // Check that the response is one of the valid codes
339    for (int i = 0; i < ok.length; i++) {
340      if (response.startsWith("" + ok[i])) {
341        return true;
342      }
343    }
344    return false;
345  }
346
347  void disconnect() throws IOException {
348    if (out != null) out.close(); 
349    if (in != null) in.close(); 
350    if (socket != null) socket.close();
351  }
352}
353
354// This PrintStream subclass makes sure that <CRLF>. becomes <CRLF>..
355// per RFC 821.  It also ensures that new lines are always \r\n.
356//
357class MailPrintStream extends PrintStream {
358
359  int lastChar;
360
361  public MailPrintStream(OutputStream out) {
362    super(out, true);  // deprecated, but email is byte-oriented
363  }
364
365  // Mac OS 9 does \r, but that's tough to distinguish from Windows \r\n.
366  // Don't tackle that problem right now.
367  public void write(int b) {
368    if (b == '\n' && lastChar != '\r') {
369      rawWrite('\r');  // ensure always \r\n
370      rawWrite(b);
371    }
372    else if (b == '.' && lastChar == '\n') {
373      rawWrite('.');  // add extra dot
374      rawWrite(b);
375    }
376    else if (b != '\n' && lastChar == '\r') {  // Special Mac OS 9 handling
377      rawWrite('\n');
378      rawWrite(b);
379      if (b == '.') {
380        rawWrite('.'); // add extra dot
381      }
382    }
383    else {
384      rawWrite(b);
385    }
386    lastChar = b;
387  }
388
389  public void write(byte buf[], int off, int len) {
390    for (int i = 0; i < len; i++) {
391      write(buf[off + i]);
392    }
393  }
394
395  void rawWrite(int b) {
396    super.write(b);
397  }
398
399  void rawPrint(String s) {
400    int len = s.length();
401    for (int i = 0; i < len; i++) {
402      rawWrite(s.charAt(i));
403    }
404  }
405}