DM_DEFAULT_ENCODING: Be explicit that we're using 7-bit ASCII when sending email.
[cfb.git] / prod / net / jaekl / qd / util / SendMail.java
1 package net.jaekl.qd.util;
2
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;
15
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
22         
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";
30         
31         String m_hostName;
32         String m_boundary;
33         Charset m_charset;
34         
35         String m_smtpHost;
36         int m_smtpPort;
37         String m_from;
38         String m_subject;
39         ArrayList<String> m_to;
40         ArrayList<String> m_cc;
41         ArrayList<String> m_bcc;
42         ArrayList<MimePart> m_part;
43         
44         public SendMail() {
45                 m_hostName = null;
46                 m_boundary = null;
47                 
48                 m_smtpHost = "localhost";
49                 m_smtpPort = 25;
50                 m_from = "noreply@localhost";
51                 m_subject = "";
52                 m_to = new ArrayList<String>();
53                 m_cc = new ArrayList<String>();
54                 m_bcc = new ArrayList<String>();
55                 m_part = new ArrayList<MimePart>();
56         }
57         
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); }
66         
67         public void send() throws MailException 
68         {
69                 try (
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()));
73                         ) 
74                 {
75                         send(pw, br);
76                 }
77                 catch (IOException exc) {
78                         throw new MailException(exc);
79                 }
80         }
81         
82         Socket openSocket(String host, int port) throws UnknownHostException, IOException {
83                 return new Socket(host, port);
84         }
85         
86         String getHostName() throws UnknownHostException { 
87                 if (null == m_hostName) {
88                         m_hostName = InetAddress.getLocalHost().getHostName();
89                 }
90                 return m_hostName;
91         }
92         
93         void send(PrintWriter pw, BufferedReader br) throws MailException
94         {
95                 try {
96                         String cmd;
97                         String line;
98                         
99                         line = readResponse(br);
100                         validateResponse("", RESP_220, line);
101                         
102                         cmd = "HELO " + getHostName();
103                         line = sendLine(pw, br, cmd);
104                         validateResponse(cmd, RESP_250, line);
105                         
106                         cmd = "MAIL FROM: " + m_from;
107                         line = sendLine(pw, br, cmd);
108                         validateResponse(cmd, RESP_250, line);
109                         
110                         sendRcpt(pw, br, m_to);
111                         sendRcpt(pw, br, m_cc);
112                         sendRcpt(pw, br, m_bcc);
113                         
114                         cmd = "DATA";
115                         line = sendLine(pw, br, cmd);
116                         validateResponse(cmd, RESP_354, line);
117                         
118                         line = sendData(pw, br);
119                         validateResponse(cmd, RESP_250, line);
120                         
121                         cmd = "QUIT";
122                         line = sendLine(pw, br, cmd);
123                         validateResponse(cmd, RESP_221, line);
124                 }
125                 catch (IOException exc) {
126                         throw new MailException(exc);
127                 }
128         }
129         
130         // Send the DATA content of the email
131         String sendData(PrintWriter pw, BufferedReader br) throws MailException, IOException
132         {
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);
138                 sendMimeHeaders(pw);
139
140                 sendLine(pw, "");
141                 
142                 sendMimeParts(pw);
143                 
144                 String result = sendLine(pw, br, "\r\n.");
145                 return result;
146         }
147         
148         void sendMimeHeaders(PrintWriter pw)
149         {
150                 sendLine(pw, "MIME-Version: 1.0");
151                 sendLine(pw, "Content-Type: multipart/mixed; boundary=\"" + getBoundary() + "\"");
152         }
153         
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
158                         sendLine(pw, "");
159                         sendLine(pw, part.getContent());
160                         sendLine(pw, "");
161                 }
162                 sendLine(pw, "--" + getBoundary() + "--");
163         }
164         
165         boolean isBoundaryOk(String candidate) {
166                 for (MimePart part : m_part) {
167                         if (part.getContent().contains(candidate)) {
168                                 return false;
169                         }
170                 }
171                 return true;
172         }
173         
174         Charset getCharset() {
175                 if (null == m_charset) {
176                         m_charset = Charset.forName(SMTP_CHARSET);
177                 }
178                 return m_charset;
179         }
180         
181         String getBoundary()
182         {
183                 if (null != m_boundary) {
184                         return m_boundary;
185                 }
186                 
187                 StringBuilder sb = new StringBuilder("snip_snip");
188                 if (isBoundaryOk(sb.toString())) {
189                         return sb.toString();
190                 }
191                 
192                 sb.append('_');
193                 char ch = 'Z';
194                 
195                 while (true) {
196                         if (isBoundaryOk(sb.toString())) {
197                                 m_boundary = sb.toString();
198                                 return m_boundary;
199                         }
200                         sb.append(ch);
201                         
202                         ch--;
203                         if (ch < 'A') {
204                                 ch = 'Z';
205                         }
206                 }
207         }
208         
209         void sendAddressee(PrintWriter pw, String designator, List<String> addrs) throws MailException
210         {
211                 for (String addr : addrs) {
212                         sendLine(pw, designator + ": " + addr);
213                 }
214         }
215         
216         void validateResponse(String cmd, String expected, String actual) throws MailException 
217         {
218                 if (! actual.startsWith(expected)) {
219                         throw new MailException(cmd, expected, actual);
220                 }
221         }
222         
223         void sendRcpt(PrintWriter pw, BufferedReader br, List<String> addrs) throws IOException, MailException 
224         {
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);
230                         }
231                         if (!line.startsWith(RESP_250)) {
232                                 throw new MailException(cmd, RESP_250, line);
233                         }
234                 }
235
236         }
237         
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.
243         //
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) 
247         {
248                 if (null == line) {
249                         return false;
250                 }
251                 
252                 String trimmed = line.trim();
253                 
254                 if (trimmed.length() < 3) {
255                         return false;
256                 }
257                 if (! (    (Character.isDigit(trimmed.charAt(0))) 
258                             && (Character.isDigit(trimmed.charAt(1)))
259                             && (Character.isDigit(trimmed.charAt(2))) ) )
260                 {
261                         return false;
262                 }
263                 
264                 if (3 == trimmed.length()) {
265                         return true;
266                 }
267                 
268                 if (' ' == trimmed.charAt(3)) {
269                         return true;
270                 }
271                 
272                 if ('-' == trimmed.charAt(3)) {
273                         return false;   // This is a response code, but it's continued on the next line
274                 }
275
276                 // This doesn't look like a response code
277                 return false;
278         }
279         
280         int responseCode(String line) 
281         {
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.
285                 
286                 if ((null == line) || (line.length() < 3)) {
287                         return 0;
288                 }
289                 
290                 return Integer.parseInt(line.substring(0, 3));
291         }
292         
293         String readResponse(BufferedReader br) throws IOException 
294         {
295                 String line;
296                 
297                 while (null != (line = br.readLine())) {
298                         if (isFinalResponseCode(line)) {
299                                 return line;
300                         }
301                 }
302                 return "";
303         }
304         
305         void sendLine(PrintWriter pw, String line)
306         {
307                 pw.write(line + "\r\n");
308                 pw.flush();
309         }
310
311         String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
312         {
313                 sendLine(pw, line);
314                 return readResponse(br);
315         }       
316 }