From: Chris Jaekl Date: Sat, 31 Oct 2015 13:29:22 +0000 (+0900) Subject: Switch from javax.mail to net.jaekl.qd.SendMail. X-Git-Url: https://jaekl.net/gitweb/?a=commitdiff_plain;h=4173dc72e969e6e3f51c9cb4734b07ec664db975;p=cfb.git Switch from javax.mail to net.jaekl.qd.SendMail. --- diff --git a/prod/net/jaekl/cfb/analyze/Notifier.java b/prod/net/jaekl/cfb/analyze/Notifier.java index 6203727..fbbaabc 100644 --- a/prod/net/jaekl/cfb/analyze/Notifier.java +++ b/prod/net/jaekl/cfb/analyze/Notifier.java @@ -3,20 +3,14 @@ package net.jaekl.cfb.analyze; 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.qd.util.MailException; +import net.jaekl.qd.util.MimePart; +import net.jaekl.qd.util.SendMail; public class Notifier { - private static final String MAIL_SMTP_HOST = "mail.smtp.host"; private static final String TEXT_HTML = "text/html"; CfbBundle m_bundle; @@ -35,9 +29,9 @@ public class Notifier { } 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 recipients = m_config.getNotify(); if (recipients.size() < 1) { @@ -47,27 +41,26 @@ public class Notifier { 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); - 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) { - msg.addRecipient(RecipientType.TO, new InternetAddress(recipient)); + sendMail.addTo(recipient); } 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) { diff --git a/prod/net/jaekl/qd/util/SendMail.java b/prod/net/jaekl/qd/util/SendMail.java index ce21ea3..1f56302 100644 --- a/prod/net/jaekl/qd/util/SendMail.java +++ b/prod/net/jaekl/qd/util/SendMail.java @@ -55,7 +55,7 @@ public class SendMail { 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())); ) @@ -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(); @@ -84,6 +88,7 @@ public class SendMail { validateResponse("", RESP_220, line); cmd = "HELO " + getHostName(); + line = sendLine(pw, br, cmd); 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, ""); } 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); @@ -280,6 +286,7 @@ public class SendMail { void sendLine(PrintWriter pw, String line) { pw.write(line + "\r\n"); + pw.flush(); } 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 index 0000000..9c32266 --- /dev/null +++ b/test/net/jaekl/qd/util/InputStreamMock.java @@ -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 data = new ArrayList(); + 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 index 0000000..9b56329 --- /dev/null +++ b/test/net/jaekl/qd/util/OutputStreamMock.java @@ -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 index 0000000..f6ce469 --- /dev/null +++ b/test/net/jaekl/qd/util/SendMailMock.java @@ -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 index 0000000..5d942cb --- /dev/null +++ b/test/net/jaekl/qd/util/SendMailTest.java @@ -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 ." + }, + { + ".\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 index 0000000..adb5986 --- /dev/null +++ b/test/net/jaekl/qd/util/SmtpConversationMock.java @@ -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 index 0000000..e5c3dfc --- /dev/null +++ b/test/net/jaekl/qd/util/SocketMock.java @@ -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; } + +}