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:
005: package com.oreilly.servlet;
006:
007: import java.io.*;
008: import java.net.*;
009: import 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 paramenum = req.getParameterNames();
045: * while (paramenum.hasMoreElements()) {
046: * String name = (String)paramenum.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: */
075: public 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",
108: "com.oreilly.servlet.MailMessage (www.servlets.com)");
109: connect();
110: sendHelo();
111: }
112:
113: /**
114: * Sets the from address. Also sets the "From" header. This method should
115: * be called only once.
116: *
117: * @exception IOException if there's any problem reported by the mail server
118: */
119: public void from(String from) throws IOException {
120: sendFrom(from);
121: this .from = from;
122: }
123:
124: /**
125: * Sets the to address. Also sets the "To" header. This method may be
126: * called multiple times.
127: *
128: * @exception IOException if there's any problem reported by the mail server
129: */
130: public void to(String to) throws IOException {
131: sendRcpt(to);
132: this .to.addElement(to);
133: }
134:
135: /**
136: * Sets the cc address. Also sets the "Cc" header. This method may be
137: * called multiple times.
138: *
139: * @exception IOException if there's any problem reported by the mail server
140: */
141: public void cc(String cc) throws IOException {
142: sendRcpt(cc);
143: this .cc.addElement(cc);
144: }
145:
146: /**
147: * Sets the bcc address. Does NOT set any header since it's a *blind* copy.
148: * This method may be called multiple times.
149: *
150: * @exception IOException if there's any problem reported by the mail server
151: */
152: public void bcc(String bcc) throws IOException {
153: sendRcpt(bcc);
154: // No need to keep track of Bcc'd addresses
155: }
156:
157: /**
158: * Sets the subject of the mail message. Actually sets the "Subject"
159: * header.
160: */
161: public void setSubject(String subj) {
162: headers.put("Subject", subj);
163: }
164:
165: /**
166: * Sets the named header to the given value. RFC 822 provides the rules for
167: * what text may constitute a header name and value.
168: */
169: public void setHeader(String name, String value) {
170: // Blindly trust the user doesn't set any invalid headers
171: headers.put(name, value);
172: }
173:
174: /**
175: * Returns a PrintStream that can be used to write the body of the message.
176: * A stream is used since email bodies are byte-oriented. A writer could
177: * be wrapped on top if necessary for internationalization.
178: *
179: * @exception IOException if there's any problem reported by the mail server
180: */
181: public PrintStream getPrintStream() throws IOException {
182: setFromHeader();
183: setToHeader();
184: setCcHeader();
185: sendData();
186: flushHeaders();
187: return out;
188: }
189:
190: void setFromHeader() {
191: setHeader("From", from);
192: }
193:
194: void setToHeader() {
195: setHeader("To", vectorToList(to));
196: }
197:
198: void setCcHeader() {
199: if (!cc.isEmpty()) { // thanks to Patrice, patricek_97@yahoo.com
200: setHeader("Cc", vectorToList(cc));
201: }
202: }
203:
204: String vectorToList(Vector v) {
205: StringBuffer buf = new StringBuffer();
206: Enumeration e = v.elements();
207: while (e.hasMoreElements()) {
208: buf.append(e.nextElement());
209: if (e.hasMoreElements()) {
210: buf.append(", ");
211: }
212: }
213: return buf.toString();
214: }
215:
216: void flushHeaders() throws IOException {
217: // XXX Should I care about order here?
218: Enumeration e = headers.keys();
219: while (e.hasMoreElements()) {
220: String name = (String) e.nextElement();
221: String value = (String) headers.get(name);
222: out.println(name + ": " + value);
223: }
224: out.println();
225: out.flush();
226: }
227:
228: /**
229: * Sends the message and closes the connection to the server.
230: * The MailMessage object cannot be reused.
231: *
232: * @exception IOException if there's any problem reported by the mail server
233: */
234: public void sendAndClose() throws IOException {
235: sendDot();
236: disconnect();
237: }
238:
239: // Make a limited attempt to extract a sanitized email address
240: // Prefer text in <brackets>, ignore anything in (parentheses)
241: static String sanitizeAddress(String s) {
242: int paramDepth = 0;
243: int start = 0;
244: int end = 0;
245: int len = s.length();
246:
247: for (int i = 0; i < len; i++) {
248: char c = s.charAt(i);
249: if (c == '(') {
250: paramDepth++;
251: if (start == 0) {
252: end = i; // support "address (name)"
253: }
254: } else if (c == ')') {
255: paramDepth--;
256: if (end == 0) {
257: start = i + 1; // support "(name) address"
258: }
259: } else if (paramDepth == 0 && c == '<') {
260: start = i + 1;
261: } else if (paramDepth == 0 && c == '>') {
262: end = i;
263: }
264: }
265:
266: if (end == 0) {
267: end = len;
268: }
269:
270: return s.substring(start, end);
271: }
272:
273: // * * * * * Raw protocol methods below here * * * * *
274:
275: void connect() throws IOException {
276: socket = new Socket(host, 25);
277: out = new MailPrintStream(new BufferedOutputStream(socket
278: .getOutputStream()));
279: in = new BufferedReader(new InputStreamReader(socket
280: .getInputStream()));
281: getReady();
282: }
283:
284: void getReady() throws IOException {
285: String response = in.readLine();
286: int[] ok = { 220 };
287: if (!isResponseOK(response, ok)) {
288: throw new IOException(
289: "Didn't get introduction from server: " + response);
290: }
291: }
292:
293: void sendHelo() throws IOException {
294: String local = InetAddress.getLocalHost().getHostName();
295: int[] ok = { 250 };
296: send("HELO " + local, ok);
297: }
298:
299: void sendFrom(String from) throws IOException {
300: int[] ok = { 250 };
301: send("MAIL FROM: " + "<" + sanitizeAddress(from) + ">", ok);
302: }
303:
304: void sendRcpt(String rcpt) throws IOException {
305: int[] ok = { 250, 251 };
306: send("RCPT TO: " + "<" + sanitizeAddress(rcpt) + ">", ok);
307: }
308:
309: void sendData() throws IOException {
310: int[] ok = { 354 };
311: send("DATA", ok);
312: }
313:
314: void sendDot() throws IOException {
315: int[] ok = { 250 };
316: send("\r\n.", ok); // make sure dot is on new line
317: }
318:
319: void sendQuit() throws IOException {
320: int[] ok = { 221 };
321: send("QUIT", ok);
322: }
323:
324: void send(String msg, int[] ok) throws IOException {
325: out.rawPrint(msg + "\r\n"); // raw supports <CRLF>.<CRLF>
326: //System.out.println("S: " + msg);
327: String response = in.readLine();
328: //System.out.println("R: " + response);
329: if (!isResponseOK(response, ok)) {
330: throw new IOException("Unexpected reply to command: " + msg
331: + ": " + response);
332: }
333: }
334:
335: boolean isResponseOK(String response, int[] ok) {
336: // Check that the response is one of the valid codes
337: for (int i = 0; i < ok.length; i++) {
338: if (response.startsWith("" + ok[i])) {
339: return true;
340: }
341: }
342: return false;
343: }
344:
345: void disconnect() throws IOException {
346: if (out != null)
347: out.close();
348: if (in != null)
349: in.close();
350: if (socket != null)
351: socket.close();
352: }
353: }
354:
355: // This PrintStream subclass makes sure that <CRLF>. becomes <CRLF>..
356: // per RFC 821. It also ensures that new lines are always \r\n.
357: //
358: class MailPrintStream extends PrintStream {
359:
360: int lastChar;
361:
362: public MailPrintStream(OutputStream out) {
363: super (out, true); // deprecated, but email is byte-oriented
364: }
365:
366: // Mac OS 9 does \r, but that's tough to distinguish from Windows \r\n.
367: // Don't tackle that problem right now.
368: public void write(int b) {
369: if (b == '\n' && lastChar != '\r') {
370: rawWrite('\r'); // ensure always \r\n
371: rawWrite(b);
372: } else if (b == '.' && lastChar == '\n') {
373: rawWrite('.'); // add extra dot
374: rawWrite(b);
375: } else if (b != '\n' && lastChar == '\r') { // Special Mac OS 9 handling
376: rawWrite('\n');
377: rawWrite(b);
378: if (b == '.') {
379: rawWrite('.'); // add extra dot
380: }
381: } else {
382: rawWrite(b);
383: }
384: lastChar = b;
385: }
386:
387: public void write(byte buf[], int off, int len) {
388: for (int i = 0; i < len; i++) {
389: write(buf[off + i]);
390: }
391: }
392:
393: void rawWrite(int b) {
394: super .write(b);
395: }
396:
397: void rawPrint(String s) {
398: int len = s.length();
399: for (int i = 0; i < len; i++) {
400: rawWrite(s.charAt(i));
401: }
402: }
403: }
|