Switch from javax.mail to net.jaekl.qd.SendMail.
authorChris Jaekl <cejaekl@yahoo.com>
Sat, 31 Oct 2015 13:29:22 +0000 (22:29 +0900)
committerChris Jaekl <cejaekl@yahoo.com>
Sat, 31 Oct 2015 13:29:22 +0000 (22:29 +0900)
prod/net/jaekl/cfb/analyze/Notifier.java
prod/net/jaekl/qd/util/SendMail.java
test/net/jaekl/qd/util/InputStreamMock.java [new file with mode: 0644]
test/net/jaekl/qd/util/OutputStreamMock.java [new file with mode: 0644]
test/net/jaekl/qd/util/SendMailMock.java [new file with mode: 0644]
test/net/jaekl/qd/util/SendMailTest.java [new file with mode: 0644]
test/net/jaekl/qd/util/SmtpConversationMock.java [new file with mode: 0644]
test/net/jaekl/qd/util/SocketMock.java [new file with mode: 0644]

index 62037279f6005e8b1252a0d13c71955b9146de48..fbbaabc087bcf78cd24c6e26fc84451b8e7b2a1c 100644 (file)
@@ -3,20 +3,14 @@ package net.jaekl.cfb.analyze;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.Properties;
-
-import javax.mail.Message.RecipientType;
-import javax.mail.MessagingException;
-import javax.mail.Session;
-import javax.mail.Transport;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeMessage;
 
 import net.jaekl.cfb.CfbBundle;
 import net.jaekl.cfb.Config;
 
 import net.jaekl.cfb.CfbBundle;
 import net.jaekl.cfb.Config;
+import net.jaekl.qd.util.MailException;
+import net.jaekl.qd.util.MimePart;
+import net.jaekl.qd.util.SendMail;
 
 public class Notifier {
 
 public class Notifier {
-       private static final String MAIL_SMTP_HOST = "mail.smtp.host";
        private static final String TEXT_HTML = "text/html";
        
        CfbBundle m_bundle;
        private static final String TEXT_HTML = "text/html";
        
        CfbBundle m_bundle;
@@ -35,9 +29,9 @@ public class Notifier {
        }
        
        void sendEmail(PrintWriter pw, HtmlReport report) {
        }
        
        void sendEmail(PrintWriter pw, HtmlReport report) {
-               Properties props = System.getProperties();
-               props.setProperty(MAIL_SMTP_HOST, m_config.getMailSmtpHost());
-               Session sess = Session.getDefaultInstance(props);
+               SendMail sendMail = new SendMail();
+               
+               sendMail.setSmtpHost(m_config.getMailSmtpHost());
                
                ArrayList<String> recipients = m_config.getNotify();
                if (recipients.size() < 1) {
                
                ArrayList<String> recipients = m_config.getNotify();
                if (recipients.size() < 1) {
@@ -47,27 +41,26 @@ public class Notifier {
                PrintWriter mailWriter = null;
                
                try {
                PrintWriter mailWriter = null;
                
                try {
-                       MimeMessage msg = new MimeMessage(sess);
-                       
                        String earlier = report.getDelta().getEarlier().constructVersionText(m_bundle);
                        String later   = report.getDelta().getLater().constructVersionText(m_bundle);
 
                        String earlier = report.getDelta().getEarlier().constructVersionText(m_bundle);
                        String later   = report.getDelta().getLater().constructVersionText(m_bundle);
 
-                       msg.setFrom(new InternetAddress(m_config.getMailFrom()));
-                       msg.setSubject(m_bundle.get(CfbBundle.CFB_MAIL_SUBJECT, earlier, later));
+                       sendMail.setFrom(m_config.getMailFrom());
+                       sendMail.setSubject(m_bundle.get(CfbBundle.CFB_MAIL_SUBJECT, earlier, later));
                        
                        for (String recipient : recipients) {
                        
                        for (String recipient : recipients) {
-                               msg.addRecipient(RecipientType.TO, new InternetAddress(recipient));
+                               sendMail.addTo(recipient);
                        }
                        
                        StringWriter sw = new StringWriter();
                        mailWriter = new PrintWriter(sw);
                        report.write(mailWriter);
                        mailWriter.flush();
                        }
                        
                        StringWriter sw = new StringWriter();
                        mailWriter = new PrintWriter(sw);
                        report.write(mailWriter);
                        mailWriter.flush();
-                       
-                       msg.setContent(sw.toString(), TEXT_HTML);
-                       Transport.send(msg);
+
+                       MimePart part = new MimePart(sw.toString(), TEXT_HTML);
+                       sendMail.addPart(part);
+                       sendMail.send();
                }
                }
-               catch (MessagingException exc) {
+               catch (MailException exc) {
                        StringBuilder toList = new StringBuilder();
                        for (String recipient : recipients) {
                                if (toList.length() > 0) {
                        StringBuilder toList = new StringBuilder();
                        for (String recipient : recipients) {
                                if (toList.length() > 0) {
index ce21ea3ba81538694d250b3820b8d98463fa5067..1f563027ce5b247687be8173f95a9b74ca0b63de 100644 (file)
@@ -55,7 +55,7 @@ public class SendMail {
        public void send() throws MailException 
        {
                try (
        public void send() throws MailException 
        {
                try (
-                               Socket sock = new Socket(m_smtpHost, m_smtpPort);
+                               Socket sock = openSocket(m_smtpHost, m_smtpPort);
                                PrintWriter pw = new PrintWriter(sock.getOutputStream(), true);
                                BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
                        ) 
                                PrintWriter pw = new PrintWriter(sock.getOutputStream(), true);
                                BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
                        ) 
@@ -67,6 +67,10 @@ public class SendMail {
                }
        }
        
                }
        }
        
+       Socket openSocket(String host, int port) throws UnknownHostException, IOException {
+               return new Socket(host, port);
+       }
+       
        String getHostName() throws UnknownHostException { 
                if (null == m_hostName) {
                        m_hostName = InetAddress.getLocalHost().getHostName();
        String getHostName() throws UnknownHostException { 
                if (null == m_hostName) {
                        m_hostName = InetAddress.getLocalHost().getHostName();
@@ -84,6 +88,7 @@ public class SendMail {
                        validateResponse("", RESP_220, line);
                        
                        cmd = "HELO " + getHostName();
                        validateResponse("", RESP_220, line);
                        
                        cmd = "HELO " + getHostName();
+                       line = sendLine(pw, br, cmd);
                        validateResponse(cmd, RESP_250, line);
                        
                        cmd = "MAIL FROM: " + m_from;
                        validateResponse(cmd, RESP_250, line);
                        
                        cmd = "MAIL FROM: " + m_from;
@@ -140,6 +145,7 @@ public class SendMail {
                        sendLine(pw, "Content-Type: " + part.getMimeType());    // TODO:  Add support for encodings
                        sendLine(pw, "");
                        sendLine(pw, part.getContent());
                        sendLine(pw, "Content-Type: " + part.getMimeType());    // TODO:  Add support for encodings
                        sendLine(pw, "");
                        sendLine(pw, part.getContent());
+                       sendLine(pw, "");
                }
                sendLine(pw, "--" + getBoundary() + "--");
        }
                }
                sendLine(pw, "--" + getBoundary() + "--");
        }
@@ -188,7 +194,7 @@ public class SendMail {
                }
        }
        
                }
        }
        
-       void validateResponse(String cmd, String actual, String expected) throws MailException 
+       void validateResponse(String cmd, String expected, String actual) throws MailException 
        {
                if (! actual.startsWith(expected)) {
                        throw new MailException(cmd, expected, actual);
        {
                if (! actual.startsWith(expected)) {
                        throw new MailException(cmd, expected, actual);
@@ -280,6 +286,7 @@ public class SendMail {
        void sendLine(PrintWriter pw, String line)
        {
                pw.write(line + "\r\n");
        void sendLine(PrintWriter pw, String line)
        {
                pw.write(line + "\r\n");
+               pw.flush();
        }
 
        String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
        }
 
        String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
diff --git a/test/net/jaekl/qd/util/InputStreamMock.java b/test/net/jaekl/qd/util/InputStreamMock.java
new file mode 100644 (file)
index 0000000..9c32266
--- /dev/null
@@ -0,0 +1,58 @@
+package net.jaekl.qd.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+
+public class InputStreamMock extends InputStream {
+       private ByteArrayInputStream m_bais;
+       
+       public InputStreamMock() { 
+               m_bais = new ByteArrayInputStream(new byte[0]);
+       }
+       
+       @Override
+       public int read() throws IOException {
+               return m_bais.read();
+       }
+
+       public void mock_append(byte[] addition) {
+               byte[] old = readAll();
+               byte[] combined = new byte[old.length + addition.length];
+               for (int idx = 0; idx < old.length; ++idx) {
+                       combined[idx] = old[idx];
+               }
+               for (int idx = 0; idx < addition.length; ++idx) {
+                       combined[old.length + idx] = addition[idx];
+               }
+               
+               mock_reset(combined);
+       }
+       
+       public void mock_append(String addition, Charset charset) 
+       {
+               mock_append(addition.getBytes(charset));
+       }
+       
+       public void mock_reset(byte[] newData) {
+               m_bais = new ByteArrayInputStream(newData);
+       }
+       
+       // Read all remaining bytes in the inputstream
+       private byte[] readAll() {
+               // This is horribly inefficient; don't use in production code
+               ArrayList<Byte> data = new ArrayList<Byte>();
+               int b = m_bais.read();
+               while ((-1) != b) {
+                       data.add(Byte.valueOf((byte)b));
+               }
+               byte[] result = new byte[data.size()];
+               for (int idx = 0; idx < result.length; ++idx) {
+                       result[idx] = data.get(idx).byteValue();
+               }
+               
+               return result;
+       }
+}
diff --git a/test/net/jaekl/qd/util/OutputStreamMock.java b/test/net/jaekl/qd/util/OutputStreamMock.java
new file mode 100644 (file)
index 0000000..9b56329
--- /dev/null
@@ -0,0 +1,48 @@
+package net.jaekl.qd.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+public class OutputStreamMock extends OutputStream {
+       public interface Listener {
+               public void onWrite(int b);
+       }
+       
+       ByteArrayOutputStream m_baos;
+       Listener m_listener;
+
+       public OutputStreamMock() {
+               m_baos = new ByteArrayOutputStream();
+               m_listener = null;
+       }
+       
+       @Override
+       public void write(int b) throws IOException {
+               m_baos.write(b);
+               if (null != m_listener) {
+                       m_listener.onWrite(b);
+               }
+       }
+       
+       public byte[] mock_getContent() {
+               return m_baos.toByteArray();
+       }
+       
+       public String mock_getContent(Charset charset) 
+       {
+               try {
+                       return m_baos.toString(charset.name());
+               }
+               catch (UnsupportedEncodingException exc) {
+                       // Really, this should not happen
+                       throw new RuntimeException(exc);
+               }
+       }
+       
+       public void mock_setListener(Listener listener) {
+               m_listener = listener;
+       }
+}
diff --git a/test/net/jaekl/qd/util/SendMailMock.java b/test/net/jaekl/qd/util/SendMailMock.java
new file mode 100644 (file)
index 0000000..f6ce469
--- /dev/null
@@ -0,0 +1,16 @@
+package net.jaekl.qd.util;
+
+import java.net.Socket;
+
+public class SendMailMock extends SendMail {
+       SmtpConversationMock m_conversat;
+       
+       public SendMailMock(SmtpConversationMock conversat) {
+               m_conversat = conversat;
+       }
+       
+       @Override
+       Socket openSocket(String host, int port) {
+               return new SocketMock(m_conversat.getInputStream(), m_conversat.getOutputStream());
+       }
+}
diff --git a/test/net/jaekl/qd/util/SendMailTest.java b/test/net/jaekl/qd/util/SendMailTest.java
new file mode 100644 (file)
index 0000000..5d942cb
--- /dev/null
@@ -0,0 +1,84 @@
+package net.jaekl.qd.util;
+
+import static org.junit.Assert.*;
+
+import java.nio.charset.Charset;
+
+import org.junit.Test;
+
+public class SendMailTest {
+
+       @Test
+       public void testRfc821Samples() throws MailException {
+               final String TYPICAL_INIT = "220 BBN-UNIX.ARPA Simple Mail Transfer Service Ready"; 
+               final String[][] TYPICAL = {
+                               {
+                                       "HELO ",
+                                       "250 BBN-UNIX.ARPA"
+                               },
+                               {
+                                       "MAIL FROM:",
+                                       "250 OK"
+                               },
+                               {
+                                       "RCPT TO:",
+                                       "250 OK"
+                               },
+                               {
+                                       "DATA",
+                                       "354 Start mail input; end with <CRLF>.<CRLF>"
+                               },
+                               {
+                                       ".\r\n",
+                                       "250 OK"
+                               },
+                               {
+                                       "QUIT",
+                                       "221 BBN-UNIX.ARPA Service closing transmission channel"
+                               }
+                       };
+               final String TYPICAL_SENT = 
+                                 "MAIL FROM: tarzan@jane.net\r\n"
+                               + "RCPT TO: jane@jane.net\r\n"
+                               + "DATA\r\n"
+                               + "From: tarzan@jane.net\r\n"
+                               + "To: jane@jane.net\r\n"
+                               + "Subject: Me Tarzan, you Jane\r\n"
+                               + "MIME-Version: 1.0\r\n"
+                               + "Content-Type: multipart/mixed; boundary=snip_snip\r\n"
+                               + "\r\n"
+                               + "--snip_snip\r\n"
+                               + "Content-Type: text/plain\r\n"
+                               + "\r\n"
+                               + "Tarzan like Jane\n"
+                               + "Tarzan come see Jane soon\n"
+                               + "\n"
+                               + "Tarzan\r\n"
+                               + "\r\n"
+                               + "--snip_snip--\r\n"
+                               + "\r\n.\r\n\r\n"
+                               + "QUIT\r\n";
+
+               SmtpConversationMock conversat = new SmtpConversationMock(TYPICAL_INIT, TYPICAL);
+               SendMailMock smm = new SendMailMock(conversat);
+               
+               smm.setFrom("tarzan@jane.net");
+               smm.addTo("jane@jane.net");
+               smm.setSubject("Me Tarzan, you Jane");
+               
+               MimePart part = new MimePart("text/plain", "Tarzan like Jane\nTarzan come see Jane soon\n\nTarzan");
+               smm.addPart(part);
+               
+               smm.send();
+               
+               OutputStreamMock osm = conversat.getOutputStream();
+               String result = osm.mock_getContent(Charset.forName(FileMock.UTF_8));
+               
+               int pos = result.indexOf("\r\n");
+               assertTrue(pos > 0);
+               String afterHELO = result.substring(pos + 2);
+               
+               assertTrue(result.startsWith("HELO "));
+               assertEquals(TYPICAL_SENT, afterHELO);
+       }
+}
diff --git a/test/net/jaekl/qd/util/SmtpConversationMock.java b/test/net/jaekl/qd/util/SmtpConversationMock.java
new file mode 100644 (file)
index 0000000..adb5986
--- /dev/null
@@ -0,0 +1,56 @@
+package net.jaekl.qd.util;
+
+import java.nio.charset.Charset;
+
+public class SmtpConversationMock {
+       private InputStreamMock m_is;
+       private OutputStreamMock m_os;
+       
+       String[][] m_responses;
+       
+       private class Listener implements OutputStreamMock.Listener {
+               @Override
+               public void onWrite(int b) {
+                       String stream = m_os.mock_getContent(Charset.forName(FileMock.UTF_8));
+                       if (stream.endsWith("\r\n")) {
+                               int pos = stream.lastIndexOf("\r\n", stream.length() - 3);
+                               if (pos < 0) {
+                                       pos = -2;
+                               }
+                               String cmd = stream.substring(pos + 2);
+                               String reply = replyFor(cmd);
+                               if (null != reply) {
+                                       m_is.mock_append("\r\n" + reply + "\r\n", Charset.forName(FileMock.UTF_8));
+                               }
+                       }
+               }
+               
+               private String replyFor(String cmd) {
+                       for (int idx = 0; idx < m_responses.length; ++idx) {
+                               if (cmd.startsWith(m_responses[idx][0])) {
+                                       return m_responses[idx][1];
+                               }
+                       }
+                       return null;    // no canned response for this input
+               }
+       }
+       
+       // responses:  array of 2-tuples of Strings { {input, output}, {input, output}, ... }
+       public SmtpConversationMock(String init, String[][] responses) {
+               assert(null != responses);
+               
+               m_is = new InputStreamMock();
+               m_os = new OutputStreamMock();
+               
+               m_responses = responses.clone();
+               
+               Listener listener = new Listener();
+               m_os.mock_setListener(listener);
+               
+               m_is.mock_reset(init.getBytes(Charset.forName(FileMock.UTF_8)));
+       }
+       
+       public InputStreamMock getInputStream() { return m_is; }
+       public OutputStreamMock getOutputStream() { return m_os; }
+       
+}
diff --git a/test/net/jaekl/qd/util/SocketMock.java b/test/net/jaekl/qd/util/SocketMock.java
new file mode 100644 (file)
index 0000000..e5c3dfc
--- /dev/null
@@ -0,0 +1,22 @@
+package net.jaekl.qd.util;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+public class SocketMock extends Socket {
+       private InputStream m_is;
+       private OutputStream m_os;
+       
+       public SocketMock(InputStream is, OutputStream os) {
+               m_is = is;
+               m_os = os;
+       }
+       
+       @Override
+       public InputStream getInputStream() { return m_is; }
+       
+       @Override
+       public OutputStream getOutputStream() { return m_os; }
+
+}