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;
}
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) {
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) {
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()));
)
}
}
+ 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();
validateResponse("", RESP_220, line);
cmd = "HELO " + getHostName();
+ line = sendLine(pw, br, cmd);
validateResponse(cmd, RESP_250, line);
cmd = "MAIL FROM: " + m_from;
sendLine(pw, "Content-Type: " + part.getMimeType()); // TODO: Add support for encodings
sendLine(pw, "");
sendLine(pw, part.getContent());
+ sendLine(pw, "");
}
sendLine(pw, "--" + getBoundary() + "--");
}
}
}
- 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);
void sendLine(PrintWriter pw, String line)
{
pw.write(line + "\r\n");
+ pw.flush();
}
String sendLine(PrintWriter pw, BufferedReader br, String line) throws IOException
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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());
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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; }
+
+}
--- /dev/null
+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; }
+
+}