1. /*
  2. * Copyright 2000-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. /*
  18. * The original version of this class was donated by Jason Hunter,
  19. * who wrote the class as part of the com.oreilly.servlet
  20. * package for his book "Java Servlet Programming" (O'Reilly).
  21. * See http://www.servlets.com.
  22. *
  23. */
  24. package org.apache.tools.mail;
  25. import java.io.IOException;
  26. import java.io.PrintStream;
  27. import java.io.BufferedOutputStream;
  28. import java.io.OutputStream;
  29. import java.net.Socket;
  30. import java.net.InetAddress;
  31. import java.util.Vector;
  32. import java.util.Enumeration;
  33. /**
  34. * A class to help send SMTP email.
  35. * This class is an improvement on the sun.net.smtp.SmtpClient class
  36. * found in the JDK. This version has extra functionality, and can be used
  37. * with JVMs that did not extend from the JDK. It's not as robust as
  38. * the JavaMail Standard Extension classes, but it's easier to use and
  39. * easier to install, and has an Open Source license.
  40. * <p>
  41. * It can be used like this:
  42. * <blockquote><pre>
  43. * String mailhost = "localhost"; // or another mail host
  44. * String from = "Mail Message Servlet <MailMessage@server.com>";
  45. * String to = "to@you.com";
  46. * String cc1 = "cc1@you.com";
  47. * String cc2 = "cc2@you.com";
  48. * String bcc = "bcc@you.com";
  49. *  
  50. * MailMessage msg = new MailMessage(mailhost);
  51. * msg.setPort(25);
  52. * msg.from(from);
  53. * msg.to(to);
  54. * msg.cc(cc1);
  55. * msg.cc(cc2);
  56. * msg.bcc(bcc);
  57. * msg.setSubject("Test subject");
  58. * PrintStream out = msg.getPrintStream();
  59. *  
  60. * Enumeration enum = req.getParameterNames();
  61. * while (enum.hasMoreElements()) {
  62. * String name = (String)enum.nextElement();
  63. * String value = req.getParameter(name);
  64. * out.println(name + " = " + value);
  65. * }
  66. *  
  67. * msg.sendAndClose();
  68. * </pre></blockquote>
  69. * <p>
  70. * Be sure to set the from address, then set the recepient
  71. * addresses, then set the subject and other headers, then get the
  72. * PrintStream, then write the message, and finally send and close.
  73. * The class does minimal error checking internally; it counts on the mail
  74. * host to complain if there's any malformatted input or out of order
  75. * execution.
  76. * <p>
  77. * An attachment mechanism based on RFC 1521 could be implemented on top of
  78. * this class. In the meanwhile, JavaMail is the best solution for sending
  79. * email with attachments.
  80. * <p>
  81. * Still to do:
  82. * <ul>
  83. * <li>Figure out how to close the connection in case of error
  84. * </ul>
  85. *
  86. * @version 1.1, 2000/03/19, added angle brackets to address, helps some servers
  87. * version 1.0, 1999/12/29
  88. */
  89. public class MailMessage {
  90. /** default mailhost */
  91. public static final String DEFAULT_HOST = "localhost";
  92. /** default port for SMTP: 25 */
  93. public static final int DEFAULT_PORT = 25;
  94. /** host name for the mail server */
  95. private String host;
  96. /** host port for the mail server */
  97. private int port = DEFAULT_PORT;
  98. /** sender email address */
  99. private String from;
  100. /** list of email addresses to reply to */
  101. private Vector replyto;
  102. /** list of email addresses to send to */
  103. private Vector to;
  104. /** list of email addresses to cc to */
  105. private Vector cc;
  106. /** headers to send in the mail */
  107. private Vector headersKeys;
  108. private Vector headersValues;
  109. private MailPrintStream out;
  110. private SmtpResponseReader in;
  111. private Socket socket;
  112. private static final int OK_READY = 220;
  113. private static final int OK_HELO = 250;
  114. private static final int OK_FROM = 250;
  115. private static final int OK_RCPT_1 = 250;
  116. private static final int OK_RCPT_2 = 251;
  117. private static final int OK_DATA = 354;
  118. private static final int OK_DOT = 250;
  119. private static final int OK_QUIT = 221;
  120. /**
  121. * Constructs a new MailMessage to send an email.
  122. * Use localhost as the mail server with port 25.
  123. *
  124. * @exception IOException if there's any problem contacting the mail server
  125. */
  126. public MailMessage() throws IOException {
  127. this(DEFAULT_HOST, DEFAULT_PORT);
  128. }
  129. /**
  130. * Constructs a new MailMessage to send an email.
  131. * Use the given host as the mail server with port 25.
  132. *
  133. * @param host the mail server to use
  134. * @exception IOException if there's any problem contacting the mail server
  135. */
  136. public MailMessage(String host) throws IOException {
  137. this(host, DEFAULT_PORT);
  138. }
  139. /**
  140. * Constructs a new MailMessage to send an email.
  141. * Use the given host and port as the mail server.
  142. *
  143. * @param host the mail server to use
  144. * @param port the port to connect to
  145. * @exception IOException if there's any problem contacting the mail server
  146. */
  147. public MailMessage(String host, int port) throws IOException {
  148. this.port = port;
  149. this.host = host;
  150. replyto = new Vector();
  151. to = new Vector();
  152. cc = new Vector();
  153. headersKeys = new Vector();
  154. headersValues = new Vector();
  155. connect();
  156. sendHelo();
  157. }
  158. /**
  159. * Set the port to connect to the SMTP host.
  160. * @param port the port to use for connection.
  161. * @see #DEFAULT_PORT
  162. */
  163. public void setPort(int port) {
  164. this.port = port;
  165. }
  166. /**
  167. * Sets the from address. Also sets the "From" header. This method should
  168. * be called only once.
  169. * @param from the from address
  170. * @exception IOException if there's any problem reported by the mail server
  171. */
  172. public void from(String from) throws IOException {
  173. sendFrom(from);
  174. this.from = from;
  175. }
  176. /**
  177. * Sets the replyto address
  178. * This method may be
  179. * called multiple times.
  180. * @param rto the replyto address
  181. *
  182. */
  183. public void replyto(String rto) {
  184. this.replyto.addElement(rto);
  185. }
  186. /**
  187. * Sets the to address. Also sets the "To" header. This method may be
  188. * called multiple times.
  189. *
  190. * @param to the to address
  191. * @exception IOException if there's any problem reported by the mail server
  192. */
  193. public void to(String to) throws IOException {
  194. sendRcpt(to);
  195. this.to.addElement(to);
  196. }
  197. /**
  198. * Sets the cc address. Also sets the "Cc" header. This method may be
  199. * called multiple times.
  200. *
  201. * @param cc the cc address
  202. * @exception IOException if there's any problem reported by the mail server
  203. */
  204. public void cc(String cc) throws IOException {
  205. sendRcpt(cc);
  206. this.cc.addElement(cc);
  207. }
  208. /**
  209. * Sets the bcc address. Does NOT set any header since it's a *blind* copy.
  210. * This method may be called multiple times.
  211. *
  212. * @param bcc the bcc address
  213. * @exception IOException if there's any problem reported by the mail server
  214. */
  215. public void bcc(String bcc) throws IOException {
  216. sendRcpt(bcc);
  217. // No need to keep track of Bcc'd addresses
  218. }
  219. /**
  220. * Sets the subject of the mail message. Actually sets the "Subject"
  221. * header.
  222. * @param subj the subject of the mail message
  223. */
  224. public void setSubject(String subj) {
  225. setHeader("Subject", subj);
  226. }
  227. /**
  228. * Sets the named header to the given value. RFC 822 provides the rules for
  229. * what text may constitute a header name and value.
  230. * @param name name of the header
  231. * @param value contents of the header
  232. */
  233. public void setHeader(String name, String value) {
  234. // Blindly trust the user doesn't set any invalid headers
  235. headersKeys.add(name);
  236. headersValues.add(value);
  237. }
  238. /**
  239. * Returns a PrintStream that can be used to write the body of the message.
  240. * A stream is used since email bodies are byte-oriented. A writer can
  241. * be wrapped on top if necessary for internationalization.
  242. * This is actually done in Message.java
  243. *
  244. * @return a printstream containing the data and the headers of the email
  245. * @exception IOException if there's any problem reported by the mail server
  246. * @see org.apache.tools.ant.taskdefs.email.Message
  247. */
  248. public PrintStream getPrintStream() throws IOException {
  249. setFromHeader();
  250. setReplyToHeader();
  251. setToHeader();
  252. setCcHeader();
  253. setHeader("X-Mailer", "org.apache.tools.mail.MailMessage (ant.apache.org)");
  254. sendData();
  255. flushHeaders();
  256. return out;
  257. }
  258. // RFC 822 s4.1: "From:" header must be sent
  259. // We rely on error checking by the MTA
  260. void setFromHeader() {
  261. setHeader("From", from);
  262. }
  263. // RFC 822 s4.1: "Reply-To:" header is optional
  264. void setReplyToHeader() {
  265. if (!replyto.isEmpty()) {
  266. setHeader("Reply-To", vectorToList(replyto));
  267. }
  268. }
  269. void setToHeader() {
  270. if (!to.isEmpty()) {
  271. setHeader("To", vectorToList(to));
  272. }
  273. }
  274. void setCcHeader() {
  275. if (!cc.isEmpty()) {
  276. setHeader("Cc", vectorToList(cc));
  277. }
  278. }
  279. String vectorToList(Vector v) {
  280. StringBuffer buf = new StringBuffer();
  281. Enumeration e = v.elements();
  282. while (e.hasMoreElements()) {
  283. buf.append(e.nextElement());
  284. if (e.hasMoreElements()) {
  285. buf.append(", ");
  286. }
  287. }
  288. return buf.toString();
  289. }
  290. void flushHeaders() throws IOException {
  291. // RFC 822 s4.1:
  292. // "Header fields are NOT required to occur in any particular order,
  293. // except that the message body MUST occur AFTER the headers"
  294. // (the same section specifies a reccommended order, which we ignore)
  295. for (int i = 0; i < headersKeys.size(); i++) {
  296. String name = (String) headersKeys.elementAt(i);
  297. String value = (String) headersValues.elementAt(i);
  298. out.println(name + ": " + value);
  299. }
  300. out.println();
  301. out.flush();
  302. }
  303. /**
  304. * Sends the message and closes the connection to the server.
  305. * The MailMessage object cannot be reused.
  306. *
  307. * @exception IOException if there's any problem reported by the mail server
  308. */
  309. public void sendAndClose() throws IOException {
  310. try {
  311. sendDot();
  312. sendQuit();
  313. } finally {
  314. disconnect();
  315. }
  316. }
  317. // Make a limited attempt to extract a sanitized email address
  318. // Prefer text in <brackets>, ignore anything in (parentheses)
  319. static String sanitizeAddress(String s) {
  320. int paramDepth = 0;
  321. int start = 0;
  322. int end = 0;
  323. int len = s.length();
  324. for (int i = 0; i < len; i++) {
  325. char c = s.charAt(i);
  326. if (c == '(') {
  327. paramDepth++;
  328. if (start == 0) {
  329. end = i; // support "address (name)"
  330. }
  331. } else if (c == ')') {
  332. paramDepth--;
  333. if (end == 0) {
  334. start = i + 1; // support "(name) address"
  335. }
  336. } else if (paramDepth == 0 && c == '<') {
  337. start = i + 1;
  338. } else if (paramDepth == 0 && c == '>') {
  339. end = i;
  340. }
  341. }
  342. if (end == 0) {
  343. end = len;
  344. }
  345. return s.substring(start, end);
  346. }
  347. // * * * * * Raw protocol methods below here * * * * *
  348. void connect() throws IOException {
  349. socket = new Socket(host, port);
  350. out = new MailPrintStream(
  351. new BufferedOutputStream(
  352. socket.getOutputStream()));
  353. in = new SmtpResponseReader(socket.getInputStream());
  354. getReady();
  355. }
  356. void getReady() throws IOException {
  357. String response = in.getResponse();
  358. int[] ok = {OK_READY};
  359. if (!isResponseOK(response, ok)) {
  360. throw new IOException(
  361. "Didn't get introduction from server: " + response);
  362. }
  363. }
  364. void sendHelo() throws IOException {
  365. String local = InetAddress.getLocalHost().getHostName();
  366. int[] ok = {OK_HELO};
  367. send("HELO " + local, ok);
  368. }
  369. void sendFrom(String from) throws IOException {
  370. int[] ok = {OK_FROM};
  371. send("MAIL FROM: " + "<" + sanitizeAddress(from) + ">", ok);
  372. }
  373. void sendRcpt(String rcpt) throws IOException {
  374. int[] ok = {OK_RCPT_1, OK_RCPT_2};
  375. send("RCPT TO: " + "<" + sanitizeAddress(rcpt) + ">", ok);
  376. }
  377. void sendData() throws IOException {
  378. int[] ok = {OK_DATA};
  379. send("DATA", ok);
  380. }
  381. void sendDot() throws IOException {
  382. int[] ok = {OK_DOT};
  383. send("\r\n.", ok); // make sure dot is on new line
  384. }
  385. void sendQuit() throws IOException {
  386. int[] ok = {OK_QUIT};
  387. try {
  388. send("QUIT", ok);
  389. } catch (IOException e) {
  390. throw new ErrorInQuitException(e);
  391. }
  392. }
  393. void send(String msg, int[] ok) throws IOException {
  394. out.rawPrint(msg + "\r\n"); // raw supports <CRLF>.<CRLF>
  395. String response = in.getResponse();
  396. if (!isResponseOK(response, ok)) {
  397. throw new IOException("Unexpected reply to command: "
  398. + msg + ": " + response);
  399. }
  400. }
  401. boolean isResponseOK(String response, int[] ok) {
  402. // Check that the response is one of the valid codes
  403. for (int i = 0; i < ok.length; i++) {
  404. if (response.startsWith("" + ok[i])) {
  405. return true;
  406. }
  407. }
  408. return false;
  409. }
  410. void disconnect() throws IOException {
  411. if (out != null) {
  412. out.close();
  413. }
  414. if (in != null) {
  415. try {
  416. in.close();
  417. } catch (IOException e) {
  418. // ignore
  419. }
  420. }
  421. if (socket != null) {
  422. try {
  423. socket.close();
  424. } catch (IOException e) {
  425. // ignore
  426. }
  427. }
  428. }
  429. }
  430. /**
  431. * This PrintStream subclass makes sure that <CRLF>. becomes <CRLF>..
  432. * per RFC 821. It also ensures that new lines are always \r\n.
  433. */
  434. class MailPrintStream extends PrintStream {
  435. private int lastChar;
  436. public MailPrintStream(OutputStream out) {
  437. super(out, true); // deprecated, but email is byte-oriented
  438. }
  439. // Mac does \n\r, but that's tough to distinguish from Windows \r\n\r\n.
  440. // Don't tackle that problem right now.
  441. public void write(int b) {
  442. if (b == '\n' && lastChar != '\r') {
  443. rawWrite('\r'); // ensure always \r\n
  444. rawWrite(b);
  445. } else if (b == '.' && lastChar == '\n') {
  446. rawWrite('.'); // add extra dot
  447. rawWrite(b);
  448. } else {
  449. rawWrite(b);
  450. }
  451. lastChar = b;
  452. }
  453. public void write(byte[] buf, int off, int len) {
  454. for (int i = 0; i < len; i++) {
  455. write(buf[off + i]);
  456. }
  457. }
  458. void rawWrite(int b) {
  459. super.write(b);
  460. }
  461. void rawPrint(String s) {
  462. int len = s.length();
  463. for (int i = 0; i < len; i++) {
  464. rawWrite(s.charAt(i));
  465. }
  466. }
  467. }