--- /dev/null
+package net.jaekl.qd.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SendMail {
+ private static final String RESP_220 = "220"; // server ready
+ private static final String RESP_221 = "221"; // goodbye
+ private static final String RESP_250 = "250"; // success
+ private static final String RESP_251 = "251"; // mail address change (we don't support this)
+ private static final String RESP_354 = "354"; // OK, proceed with DATA transmission
+
+ String m_hostName;
+ String m_boundary;
+
+ String m_smtpHost;
+ int m_smtpPort;
+ String m_from;
+ String m_subject;
+ ArrayList<String> m_to;
+ ArrayList<String> m_cc;
+ ArrayList<String> m_bcc;
+ ArrayList<MimePart> m_part;
+
+ public SendMail() {
+ m_hostName = null;
+ m_boundary = null;
+
+ m_smtpHost = "localhost";
+ m_smtpPort = 25;
+ m_from = "noreply@localhost";
+ m_subject = "";
+ m_to = new ArrayList<String>();
+ m_cc = new ArrayList<String>();
+ m_bcc = new ArrayList<String>();
+ m_part = new ArrayList<MimePart>();
+ }
+
+ public void setSmtpHost(String hostName) { m_smtpHost = hostName; }
+ public void setSmtpPort(int portNum) { m_smtpPort = portNum; }
+ public void setFrom(String addr) { m_from = addr; }
+ public void setSubject(String subject) { m_subject = subject; }
+ public void addTo(String addr) { m_to.add(addr); }
+ public void addCc(String addr) { m_cc.add(addr); }
+ public void addBcc(String addr) { m_bcc.add(addr); }
+ public void addPart(MimePart part) { m_part.add(part); }
+
+ public void send() throws MailException
+ {
+ try (
+ Socket sock = new Socket(m_smtpHost, m_smtpPort);
+ PrintWriter pw = new PrintWriter(sock.getOutputStream(), true);
+ BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
+ )
+ {
+ send(pw, br);
+ }
+ catch (IOException exc) {
+ throw new MailException(exc);
+ }
+ }
+
+ String getHostName() throws UnknownHostException {
+ if (null == m_hostName) {
+ m_hostName = InetAddress.getLocalHost().getHostName();
+ }
+ return m_hostName;
+ }
+
+ void send(PrintWriter pw, BufferedReader br) throws MailException
+ {
+ try {
+ String cmd;
+ String line;
+
+ line = readResponse(br);
+ validateResponse("", RESP_220, line);
+
+ cmd = "HELO " + getHostName();
+ validateResponse(cmd, RESP_250, line);
+
+ cmd = "MAIL FROM: " + m_from;
+ line = sendLine(pw, br, cmd);
+ validateResponse(cmd, RESP_250, line);
+
+ sendRcpt(pw, br, m_to);
+ sendRcpt(pw, br, m_cc);
+ sendRcpt(pw, br, m_bcc);
+
+ cmd = "DATA";
+ line = sendLine(pw, br, cmd);
+ validateResponse(cmd, RESP_354, line);
+
+ line = sendData(pw, br);
+ validateResponse(cmd, RESP_250, line);
+
+ cmd = "QUIT";
+ line = sendLine(pw, br, cmd);
+ validateResponse(cmd, RESP_221, line);
+ }
+ catch (IOException exc) {
+ throw new MailException(exc);
+ }
+ }
+
+ // Send the DATA content of the email
+ String sendData(PrintWriter pw, BufferedReader br) throws MailException, IOException
+ {
+ sendLine(pw, "From: " + m_from);
+ sendAddressee(pw, "To", m_to);
+ sendAddressee(pw, "Cc", m_cc);
+ sendAddressee(pw, "Bcc", m_bcc);
+ sendLine(pw, "Subject: " + m_subject);
+ sendMimeHeaders(pw);
+
+ sendLine(pw, "");
+
+ sendMimeParts(pw);
+
+ String result = sendLine(pw, br, "\r\n.\r\n");
+ return result;
+ }
+
+ void sendMimeHeaders(PrintWriter pw)
+ {
+ sendLine(pw, "MIME-Version: 1.0");
+ sendLine(pw, "Content-Type: multipart/mixed; boundary=" + getBoundary());
+ }
+
+ void sendMimeParts(PrintWriter pw) {
+ for (MimePart part : m_part) {
+ sendLine(pw, "--" + getBoundary());
+ sendLine(pw, "Content-Type: " + part.getMimeType()); // TODO: Add support for encodings
+ sendLine(pw, "");
+ sendLine(pw, part.getContent());
+ }
+ sendLine(pw, "--" + getBoundary() + "--");
+ }
+
+ boolean isBoundaryOk(String candidate) {
+ for (MimePart part : m_part) {
+ if (part.getContent().contains(candidate)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ String getBoundary()
+ {
+ if (null != m_boundary) {
+ return m_boundary;
+ }
+
+ StringBuilder sb = new StringBuilder("snip_snip");
+ if (isBoundaryOk(sb.toString())) {
+ return sb.toString();
+ }
+
+ sb.append('_');
+ char ch = 'Z';
+
+ while (true) {
+ if (isBoundaryOk(sb.toString())) {
+ m_boundary = sb.toString();
+ return m_boundary;
+ }
+ sb.append(ch);
+
+ ch--;
+ if (ch < 'A') {
+ ch = 'Z';
+ }
+ }
+ }
+
+ void sendAddressee(PrintWriter pw, String designator, List<String> addrs) throws MailException
+ {
+ for (String addr : addrs) {
+ sendLine(pw, designator + ": " + addr);
+ }
+ }
+
+ void validateResponse(String cmd, String actual, String expected) throws MailException
+ {
+ if (! actual.startsWith(expected)) {
+ throw new MailException(cmd, expected, actual);
+ }
+ }
+
+ void sendRcpt(PrintWriter pw, BufferedReader br, List<String> addrs) throws IOException, MailException
+ {
+ for (String to : addrs) {
+ String cmd = "RCPT TO: " + to;
+ String line = sendLine(pw, br, cmd);
+ if (line.startsWith(RESP_251)) {
+ throw new MailException("Apology: no support for 251 mail forwarding response to RCPT command. Mail will not be delivered: " + line);
+ }
+ if (!line.startsWith(RESP_250)) {
+ throw new MailException(cmd, RESP_250, line);
+ }
+ }
+
+ }
+
+ // RFC 5321 specifies that all response codes are three digits
+ // at the start of a new line.
+ // These digits may optionally be followed by a space and/or text.
+ // If the response is spread over more than one line, then all but
+ // the last line will have a '-' immediately after the 3rd digit.
+ //
+ // This function returns true iff. this line is a response code
+ // that is not continued on the next line.
+ boolean isFinalResponseCode(String line)
+ {
+ if (null == line) {
+ return false;
+ }
+
+ String trimmed = line.trim();
+
+ if (trimmed.length() < 3) {
+ return false;
+ }
+ if (! ( (Character.isDigit(trimmed.charAt(0)))
+ && (Character.isDigit(trimmed.charAt(1)))
+ && (Character.isDigit(trimmed.charAt(2))) ) )
+ {
+ return false;
+ }
+
+ if (3 == trimmed.length()) {
+ return true;
+ }
+
+ if (' ' == trimmed.charAt(3)) {
+ return true;
+ }
+
+ if ('-' == trimmed.charAt(3)) {
+ return false; // This is a response code, but it's continued on the next line
+ }
+
+ // This doesn't look like a response code
+ return false;
+ }
+
+ int responseCode(String line)
+ {
+ // RFC 5321 specifies that all response codes are three digits
+ // at the start of a new line.
+ // These digits may optionally be followed by a space and/or text.
+
+ if ((null == line) || (line.length() < 3)) {
+ return 0;
+ }
+
+ return Integer.parseInt(line.substring(0, 3));
+ }
+
+ String readResponse(BufferedReader br) throws IOException
+ {
+ String line;
+
+ while (null != (line = br.readLine())) {
+ if (isFinalResponseCode(line)) {
+ return line;
+ }
+ }
+ return "";
+ }
+
+ void sendLine(PrintWriter pw, String line)
+ {
+ pw.write(line + "\r\n");
+ }
+
+ String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
+ {
+ sendLine(pw, line);
+ return readResponse(br);
+ }
+}