1 package net.jaekl.qd.util;
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStreamReader;
6 import java.io.OutputStreamWriter;
7 import java.io.PrintWriter;
8 import java.net.InetAddress;
9 import java.net.Socket;
10 import java.net.UnknownHostException;
11 import java.nio.charset.Charset;
12 import java.nio.charset.StandardCharsets;
13 import java.util.ArrayList;
14 import java.util.List;
16 public class SendMail {
17 private static final String RESP_220 = "220"; // server ready
18 private static final String RESP_221 = "221"; // goodbye
19 private static final String RESP_250 = "250"; // success
20 private static final String RESP_251 = "251"; // mail address change (we don't support this)
21 private static final String RESP_354 = "354"; // OK, proceed with DATA transmission
23 // Actually, RFC821 (with which all SMTP should be backwards-compatible)
24 // requires all control transmissions to be done in US-ASCII.
25 // However, there's no point in throwing an exception if the server with which
26 // we're communicating sends bytes with the high bit set. So, let's read from
27 // the socket with the only Charset that Java guarantees will be available that
28 // can also read arbitrary sequences of 8-bit bytes.
29 private static final String SMTP_CHARSET = "ISO-8859-1";
39 ArrayList<String> m_to;
40 ArrayList<String> m_cc;
41 ArrayList<String> m_bcc;
42 ArrayList<MimePart> m_part;
48 m_smtpHost = "localhost";
50 m_from = "noreply@localhost";
52 m_to = new ArrayList<String>();
53 m_cc = new ArrayList<String>();
54 m_bcc = new ArrayList<String>();
55 m_part = new ArrayList<MimePart>();
58 public void setSmtpHost(String hostName) { m_smtpHost = hostName; }
59 public void setSmtpPort(int portNum) { m_smtpPort = portNum; }
60 public void setFrom(String addr) { m_from = addr; }
61 public void setSubject(String subject) { m_subject = subject; }
62 public void addTo(String addr) { m_to.add(addr); }
63 public void addCc(String addr) { m_cc.add(addr); }
64 public void addBcc(String addr) { m_bcc.add(addr); }
65 public void addPart(MimePart part) { m_part.add(part); }
67 public void send() throws MailException
70 Socket sock = openSocket(m_smtpHost, m_smtpPort);
71 PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.US_ASCII), true);
72 BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream(), getCharset()));
77 catch (IOException exc) {
78 throw new MailException(exc);
82 Socket openSocket(String host, int port) throws UnknownHostException, IOException {
83 return new Socket(host, port);
86 String getHostName() throws UnknownHostException {
87 if (null == m_hostName) {
88 m_hostName = InetAddress.getLocalHost().getHostName();
93 void send(PrintWriter pw, BufferedReader br) throws MailException
99 line = readResponse(br);
100 validateResponse("", RESP_220, line);
102 cmd = "HELO " + getHostName();
103 line = sendLine(pw, br, cmd);
104 validateResponse(cmd, RESP_250, line);
106 cmd = "MAIL FROM: " + m_from;
107 line = sendLine(pw, br, cmd);
108 validateResponse(cmd, RESP_250, line);
110 sendRcpt(pw, br, m_to);
111 sendRcpt(pw, br, m_cc);
112 sendRcpt(pw, br, m_bcc);
115 line = sendLine(pw, br, cmd);
116 validateResponse(cmd, RESP_354, line);
118 line = sendData(pw, br);
119 validateResponse(cmd, RESP_250, line);
122 line = sendLine(pw, br, cmd);
123 validateResponse(cmd, RESP_221, line);
125 catch (IOException exc) {
126 throw new MailException(exc);
130 // Send the DATA content of the email
131 String sendData(PrintWriter pw, BufferedReader br) throws MailException, IOException
133 sendLine(pw, "From: " + m_from);
134 sendAddressee(pw, "To", m_to);
135 sendAddressee(pw, "Cc", m_cc);
136 sendAddressee(pw, "Bcc", m_bcc);
137 sendLine(pw, "Subject: " + m_subject);
144 String result = sendLine(pw, br, "\r\n.");
148 void sendMimeHeaders(PrintWriter pw)
150 sendLine(pw, "MIME-Version: 1.0");
151 sendLine(pw, "Content-Type: multipart/mixed; boundary=\"" + getBoundary() + "\"");
154 void sendMimeParts(PrintWriter pw) {
155 for (MimePart part : m_part) {
156 sendLine(pw, "--" + getBoundary());
157 sendLine(pw, "Content-Type: " + part.getMimeType()); // TODO: Add support for encodings
159 sendLine(pw, part.getContent());
162 sendLine(pw, "--" + getBoundary() + "--");
165 boolean isBoundaryOk(String candidate) {
166 for (MimePart part : m_part) {
167 if (part.getContent().contains(candidate)) {
174 Charset getCharset() {
175 if (null == m_charset) {
176 m_charset = Charset.forName(SMTP_CHARSET);
183 if (null != m_boundary) {
187 StringBuilder sb = new StringBuilder("snip_snip");
188 if (isBoundaryOk(sb.toString())) {
189 return sb.toString();
196 if (isBoundaryOk(sb.toString())) {
197 m_boundary = sb.toString();
209 void sendAddressee(PrintWriter pw, String designator, List<String> addrs) throws MailException
211 for (String addr : addrs) {
212 sendLine(pw, designator + ": " + addr);
216 void validateResponse(String cmd, String expected, String actual) throws MailException
218 if (! actual.startsWith(expected)) {
219 throw new MailException(cmd, expected, actual);
223 void sendRcpt(PrintWriter pw, BufferedReader br, List<String> addrs) throws IOException, MailException
225 for (String to : addrs) {
226 String cmd = "RCPT TO: " + to;
227 String line = sendLine(pw, br, cmd);
228 if (line.startsWith(RESP_251)) {
229 throw new MailException("Apology: no support for 251 mail forwarding response to RCPT command. Mail will not be delivered: " + line);
231 if (!line.startsWith(RESP_250)) {
232 throw new MailException(cmd, RESP_250, line);
238 // RFC 5321 specifies that all response codes are three digits
239 // at the start of a new line.
240 // These digits may optionally be followed by a space and/or text.
241 // If the response is spread over more than one line, then all but
242 // the last line will have a '-' immediately after the 3rd digit.
244 // This function returns true iff. this line is a response code
245 // that is not continued on the next line.
246 boolean isFinalResponseCode(String line)
252 String trimmed = line.trim();
254 if (trimmed.length() < 3) {
257 if (! ( (Character.isDigit(trimmed.charAt(0)))
258 && (Character.isDigit(trimmed.charAt(1)))
259 && (Character.isDigit(trimmed.charAt(2))) ) )
264 if (3 == trimmed.length()) {
268 if (' ' == trimmed.charAt(3)) {
272 if ('-' == trimmed.charAt(3)) {
273 return false; // This is a response code, but it's continued on the next line
276 // This doesn't look like a response code
280 int responseCode(String line)
282 // RFC 5321 specifies that all response codes are three digits
283 // at the start of a new line.
284 // These digits may optionally be followed by a space and/or text.
286 if ((null == line) || (line.length() < 3)) {
290 return Integer.parseInt(line.substring(0, 3));
293 String readResponse(BufferedReader br) throws IOException
297 while (null != (line = br.readLine())) {
298 if (isFinalResponseCode(line)) {
305 void sendLine(PrintWriter pw, String line)
307 pw.write(line + "\r\n");
311 String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
314 return readResponse(br);