ce21ea3ba81538694d250b3820b8d98463fa5067
[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 = new Socket(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         String getHostName() throws UnknownHostException { 
71                 if (null == m_hostName) {
72                         m_hostName = InetAddress.getLocalHost().getHostName();
73                 }
74                 return m_hostName;
75         }
76         
77         void send(PrintWriter pw, BufferedReader br) throws MailException
78         {
79                 try {
80                         String cmd;
81                         String line;
82                         
83                         line = readResponse(br);
84                         validateResponse("", RESP_220, line);
85                         
86                         cmd = "HELO " + getHostName();
87                         validateResponse(cmd, RESP_250, line);
88                         
89                         cmd = "MAIL FROM: " + m_from;
90                         line = sendLine(pw, br, cmd);
91                         validateResponse(cmd, RESP_250, line);
92                         
93                         sendRcpt(pw, br, m_to);
94                         sendRcpt(pw, br, m_cc);
95                         sendRcpt(pw, br, m_bcc);
96                         
97                         cmd = "DATA";
98                         line = sendLine(pw, br, cmd);
99                         validateResponse(cmd, RESP_354, line);
100                         
101                         line = sendData(pw, br);
102                         validateResponse(cmd, RESP_250, line);
103                         
104                         cmd = "QUIT";
105                         line = sendLine(pw, br, cmd);
106                         validateResponse(cmd, RESP_221, line);
107                 }
108                 catch (IOException exc) {
109                         throw new MailException(exc);
110                 }
111         }
112         
113         // Send the DATA content of the email
114         String sendData(PrintWriter pw, BufferedReader br) throws MailException, IOException
115         {
116                 sendLine(pw, "From: " + m_from);
117                 sendAddressee(pw, "To", m_to);
118                 sendAddressee(pw, "Cc", m_cc);
119                 sendAddressee(pw, "Bcc", m_bcc);
120                 sendLine(pw, "Subject: " + m_subject);
121                 sendMimeHeaders(pw);
122
123                 sendLine(pw, "");
124                 
125                 sendMimeParts(pw);
126                 
127                 String result = sendLine(pw, br, "\r\n.\r\n");
128                 return result;
129         }
130         
131         void sendMimeHeaders(PrintWriter pw)
132         {
133                 sendLine(pw, "MIME-Version: 1.0");
134                 sendLine(pw, "Content-Type: multipart/mixed; boundary=" + getBoundary());
135         }
136         
137         void sendMimeParts(PrintWriter pw) {
138                 for (MimePart part : m_part) {
139                         sendLine(pw, "--" + getBoundary());
140                         sendLine(pw, "Content-Type: " + part.getMimeType());    // TODO:  Add support for encodings
141                         sendLine(pw, "");
142                         sendLine(pw, part.getContent());
143                 }
144                 sendLine(pw, "--" + getBoundary() + "--");
145         }
146         
147         boolean isBoundaryOk(String candidate) {
148                 for (MimePart part : m_part) {
149                         if (part.getContent().contains(candidate)) {
150                                 return false;
151                         }
152                 }
153                 return true;
154         }
155         
156         String getBoundary()
157         {
158                 if (null != m_boundary) {
159                         return m_boundary;
160                 }
161                 
162                 StringBuilder sb = new StringBuilder("snip_snip");
163                 if (isBoundaryOk(sb.toString())) {
164                         return sb.toString();
165                 }
166                 
167                 sb.append('_');
168                 char ch = 'Z';
169                 
170                 while (true) {
171                         if (isBoundaryOk(sb.toString())) {
172                                 m_boundary = sb.toString();
173                                 return m_boundary;
174                         }
175                         sb.append(ch);
176                         
177                         ch--;
178                         if (ch < 'A') {
179                                 ch = 'Z';
180                         }
181                 }
182         }
183         
184         void sendAddressee(PrintWriter pw, String designator, List<String> addrs) throws MailException
185         {
186                 for (String addr : addrs) {
187                         sendLine(pw, designator + ": " + addr);
188                 }
189         }
190         
191         void validateResponse(String cmd, String actual, String expected) throws MailException 
192         {
193                 if (! actual.startsWith(expected)) {
194                         throw new MailException(cmd, expected, actual);
195                 }
196         }
197         
198         void sendRcpt(PrintWriter pw, BufferedReader br, List<String> addrs) throws IOException, MailException 
199         {
200                 for (String to : addrs) {                               
201                         String cmd = "RCPT TO: " + to;
202                         String line = sendLine(pw, br, cmd);
203                         if (line.startsWith(RESP_251)) {
204                                 throw new MailException("Apology:  no support for 251 mail forwarding response to RCPT command.  Mail will not be delivered:  " + line);
205                         }
206                         if (!line.startsWith(RESP_250)) {
207                                 throw new MailException(cmd, RESP_250, line);
208                         }
209                 }
210
211         }
212         
213         // RFC 5321 specifies that all response codes are three digits
214         // at the start of a new line.
215         // These digits may optionally be followed by a space and/or text.
216         // If the response is spread over more than one line, then all but 
217         // the last line will have a '-' immediately after the 3rd digit.
218         //
219         // This function returns true iff. this line is a response code
220         // that is not continued on the next line.
221         boolean isFinalResponseCode(String line) 
222         {
223                 if (null == line) {
224                         return false;
225                 }
226                 
227                 String trimmed = line.trim();
228                 
229                 if (trimmed.length() < 3) {
230                         return false;
231                 }
232                 if (! (    (Character.isDigit(trimmed.charAt(0))) 
233                             && (Character.isDigit(trimmed.charAt(1)))
234                             && (Character.isDigit(trimmed.charAt(2))) ) )
235                 {
236                         return false;
237                 }
238                 
239                 if (3 == trimmed.length()) {
240                         return true;
241                 }
242                 
243                 if (' ' == trimmed.charAt(3)) {
244                         return true;
245                 }
246                 
247                 if ('-' == trimmed.charAt(3)) {
248                         return false;   // This is a response code, but it's continued on the next line
249                 }
250
251                 // This doesn't look like a response code
252                 return false;
253         }
254         
255         int responseCode(String line) 
256         {
257                 // RFC 5321 specifies that all response codes are three digits
258                 // at the start of a new line.
259                 // These digits may optionally be followed by a space and/or text.
260                 
261                 if ((null == line) || (line.length() < 3)) {
262                         return 0;
263                 }
264                 
265                 return Integer.parseInt(line.substring(0, 3));
266         }
267         
268         String readResponse(BufferedReader br) throws IOException 
269         {
270                 String line;
271                 
272                 while (null != (line = br.readLine())) {
273                         if (isFinalResponseCode(line)) {
274                                 return line;
275                         }
276                 }
277                 return "";
278         }
279         
280         void sendLine(PrintWriter pw, String line)
281         {
282                 pw.write(line + "\r\n");
283         }
284
285         String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
286         {
287                 sendLine(pw, line);
288                 return readResponse(br);
289         }       
290 }