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