b4c783488431f86bb53774569ac305e71774a9cb
[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.PrintWriter;
7 import java.net.InetAddress;
8 import java.net.Socket;
9 import java.net.UnknownHostException;
10 import java.nio.charset.Charset;
11 import java.util.ArrayList;
12 import java.util.List;
13
14 public class SendMail {
15         private static final String RESP_220 = "220";   // server ready
16         private static final String RESP_221 = "221";   // goodbye
17         private static final String RESP_250 = "250";   // success
18         private static final String RESP_251 = "251";   // mail address change (we don't support this)
19         private static final String RESP_354 = "354";   // OK, proceed with DATA transmission
20         
21         // Actually, RFC821 (with which all SMTP should be backwards-compatible)
22         // requires all control transmissions to be done in US-ASCII.
23         // However, there's no point in throwing an exception if the server with which 
24         // we're communicating sends bytes with the high bit set.  So, let's read from 
25         // the socket with the only Charset that Java guarantees will be available that 
26         // can also read arbitrary sequences of 8-bit bytes.
27         private static final String SMTP_CHARSET = "ISO-8859-1";
28         
29         String m_hostName;
30         String m_boundary;
31         Charset m_charset;
32         
33         String m_smtpHost;
34         int m_smtpPort;
35         String m_from;
36         String m_subject;
37         ArrayList<String> m_to;
38         ArrayList<String> m_cc;
39         ArrayList<String> m_bcc;
40         ArrayList<MimePart> m_part;
41         
42         public SendMail() {
43                 m_hostName = null;
44                 m_boundary = null;
45                 
46                 m_smtpHost = "localhost";
47                 m_smtpPort = 25;
48                 m_from = "noreply@localhost";
49                 m_subject = "";
50                 m_to = new ArrayList<String>();
51                 m_cc = new ArrayList<String>();
52                 m_bcc = new ArrayList<String>();
53                 m_part = new ArrayList<MimePart>();
54         }
55         
56         public void setSmtpHost(String hostName) { m_smtpHost = hostName; }
57         public void setSmtpPort(int portNum) { m_smtpPort = portNum; }
58         public void setFrom(String addr) { m_from = addr; }
59         public void setSubject(String subject) { m_subject = subject; }
60         public void addTo(String addr) { m_to.add(addr); }
61         public void addCc(String addr) { m_cc.add(addr); }
62         public void addBcc(String addr) { m_bcc.add(addr); }
63         public void addPart(MimePart part) { m_part.add(part); }
64         
65         public void send() throws MailException 
66         {
67                 try (
68                                 Socket sock = openSocket(m_smtpHost, m_smtpPort);
69                                 PrintWriter pw = new PrintWriter(sock.getOutputStream(), true);
70                                 BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream(), getCharset()));
71                         ) 
72                 {
73                         send(pw, br);
74                 }
75                 catch (IOException exc) {
76                         throw new MailException(exc);
77                 }
78         }
79         
80         Socket openSocket(String host, int port) throws UnknownHostException, IOException {
81                 return new Socket(host, port);
82         }
83         
84         String getHostName() throws UnknownHostException { 
85                 if (null == m_hostName) {
86                         m_hostName = InetAddress.getLocalHost().getHostName();
87                 }
88                 return m_hostName;
89         }
90         
91         void send(PrintWriter pw, BufferedReader br) throws MailException
92         {
93                 try {
94                         String cmd;
95                         String line;
96                         
97                         line = readResponse(br);
98                         validateResponse("", RESP_220, line);
99                         
100                         cmd = "HELO " + getHostName();
101                         line = sendLine(pw, br, cmd);
102                         validateResponse(cmd, RESP_250, line);
103                         
104                         cmd = "MAIL FROM: " + m_from;
105                         line = sendLine(pw, br, cmd);
106                         validateResponse(cmd, RESP_250, line);
107                         
108                         sendRcpt(pw, br, m_to);
109                         sendRcpt(pw, br, m_cc);
110                         sendRcpt(pw, br, m_bcc);
111                         
112                         cmd = "DATA";
113                         line = sendLine(pw, br, cmd);
114                         validateResponse(cmd, RESP_354, line);
115                         
116                         line = sendData(pw, br);
117                         validateResponse(cmd, RESP_250, line);
118                         
119                         cmd = "QUIT";
120                         line = sendLine(pw, br, cmd);
121                         validateResponse(cmd, RESP_221, line);
122                 }
123                 catch (IOException exc) {
124                         throw new MailException(exc);
125                 }
126         }
127         
128         // Send the DATA content of the email
129         String sendData(PrintWriter pw, BufferedReader br) throws MailException, IOException
130         {
131                 sendLine(pw, "From: " + m_from);
132                 sendAddressee(pw, "To", m_to);
133                 sendAddressee(pw, "Cc", m_cc);
134                 sendAddressee(pw, "Bcc", m_bcc);
135                 sendLine(pw, "Subject: " + m_subject);
136                 sendMimeHeaders(pw);
137
138                 sendLine(pw, "");
139                 
140                 sendMimeParts(pw);
141                 
142                 String result = sendLine(pw, br, "\r\n.");
143                 return result;
144         }
145         
146         void sendMimeHeaders(PrintWriter pw)
147         {
148                 sendLine(pw, "MIME-Version: 1.0");
149                 sendLine(pw, "Content-Type: multipart/mixed; boundary=\"" + getBoundary() + "\"");
150         }
151         
152         void sendMimeParts(PrintWriter pw) {
153                 for (MimePart part : m_part) {
154                         sendLine(pw, "--" + getBoundary());
155                         sendLine(pw, "Content-Type: " + part.getMimeType());    // TODO:  Add support for encodings
156                         sendLine(pw, "");
157                         sendLine(pw, part.getContent());
158                         sendLine(pw, "");
159                 }
160                 sendLine(pw, "--" + getBoundary() + "--");
161         }
162         
163         boolean isBoundaryOk(String candidate) {
164                 for (MimePart part : m_part) {
165                         if (part.getContent().contains(candidate)) {
166                                 return false;
167                         }
168                 }
169                 return true;
170         }
171         
172         Charset getCharset() {
173                 if (null == m_charset) {
174                         m_charset = Charset.forName(SMTP_CHARSET);
175                 }
176                 return m_charset;
177         }
178         
179         String getBoundary()
180         {
181                 if (null != m_boundary) {
182                         return m_boundary;
183                 }
184                 
185                 StringBuilder sb = new StringBuilder("snip_snip");
186                 if (isBoundaryOk(sb.toString())) {
187                         return sb.toString();
188                 }
189                 
190                 sb.append('_');
191                 char ch = 'Z';
192                 
193                 while (true) {
194                         if (isBoundaryOk(sb.toString())) {
195                                 m_boundary = sb.toString();
196                                 return m_boundary;
197                         }
198                         sb.append(ch);
199                         
200                         ch--;
201                         if (ch < 'A') {
202                                 ch = 'Z';
203                         }
204                 }
205         }
206         
207         void sendAddressee(PrintWriter pw, String designator, List<String> addrs) throws MailException
208         {
209                 for (String addr : addrs) {
210                         sendLine(pw, designator + ": " + addr);
211                 }
212         }
213         
214         void validateResponse(String cmd, String expected, String actual) throws MailException 
215         {
216                 if (! actual.startsWith(expected)) {
217                         throw new MailException(cmd, expected, actual);
218                 }
219         }
220         
221         void sendRcpt(PrintWriter pw, BufferedReader br, List<String> addrs) throws IOException, MailException 
222         {
223                 for (String to : addrs) {                               
224                         String cmd = "RCPT TO: " + to;
225                         String line = sendLine(pw, br, cmd);
226                         if (line.startsWith(RESP_251)) {
227                                 throw new MailException("Apology:  no support for 251 mail forwarding response to RCPT command.  Mail will not be delivered:  " + line);
228                         }
229                         if (!line.startsWith(RESP_250)) {
230                                 throw new MailException(cmd, RESP_250, line);
231                         }
232                 }
233
234         }
235         
236         // RFC 5321 specifies that all response codes are three digits
237         // at the start of a new line.
238         // These digits may optionally be followed by a space and/or text.
239         // If the response is spread over more than one line, then all but 
240         // the last line will have a '-' immediately after the 3rd digit.
241         //
242         // This function returns true iff. this line is a response code
243         // that is not continued on the next line.
244         boolean isFinalResponseCode(String line) 
245         {
246                 if (null == line) {
247                         return false;
248                 }
249                 
250                 String trimmed = line.trim();
251                 
252                 if (trimmed.length() < 3) {
253                         return false;
254                 }
255                 if (! (    (Character.isDigit(trimmed.charAt(0))) 
256                             && (Character.isDigit(trimmed.charAt(1)))
257                             && (Character.isDigit(trimmed.charAt(2))) ) )
258                 {
259                         return false;
260                 }
261                 
262                 if (3 == trimmed.length()) {
263                         return true;
264                 }
265                 
266                 if (' ' == trimmed.charAt(3)) {
267                         return true;
268                 }
269                 
270                 if ('-' == trimmed.charAt(3)) {
271                         return false;   // This is a response code, but it's continued on the next line
272                 }
273
274                 // This doesn't look like a response code
275                 return false;
276         }
277         
278         int responseCode(String line) 
279         {
280                 // RFC 5321 specifies that all response codes are three digits
281                 // at the start of a new line.
282                 // These digits may optionally be followed by a space and/or text.
283                 
284                 if ((null == line) || (line.length() < 3)) {
285                         return 0;
286                 }
287                 
288                 return Integer.parseInt(line.substring(0, 3));
289         }
290         
291         String readResponse(BufferedReader br) throws IOException 
292         {
293                 String line;
294                 
295                 while (null != (line = br.readLine())) {
296                         if (isFinalResponseCode(line)) {
297                                 return line;
298                         }
299                 }
300                 return "";
301         }
302         
303         void sendLine(PrintWriter pw, String line)
304         {
305                 pw.write(line + "\r\n");
306                 pw.flush();
307         }
308
309         String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
310         {
311                 sendLine(pw, line);
312                 return readResponse(br);
313         }       
314 }