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 * 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 * 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 * 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 © 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}