From 3d04dba4fdac48cf4837cc8a92cf7055d90eabfd Mon Sep 17 00:00:00 2001 From: Ziver Koc Date: Fri, 20 Jan 2017 00:38:47 +0100 Subject: [PATCH] Improved SMTP email robustness and added JUnit test for it --- src/zutil/net/smtp/Email.java | 126 +++++++++++++++++++-------- src/zutil/net/smtp/SMTPClient.java | 44 +++------- test/zutil/net/smtp/EmailTest.java | 135 +++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 66 deletions(-) create mode 100644 test/zutil/net/smtp/EmailTest.java diff --git a/src/zutil/net/smtp/Email.java b/src/zutil/net/smtp/Email.java index ced90be..1376c28 100755 --- a/src/zutil/net/smtp/Email.java +++ b/src/zutil/net/smtp/Email.java @@ -1,11 +1,13 @@ package zutil.net.smtp; import java.io.IOException; -import java.io.PrintStream; +import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.regex.Pattern; + +import static zutil.net.smtp.SMTPClient.NEWLINE; -import sun.net.smtp.SmtpClient; /** * Simplifies sending of a email @@ -13,16 +15,20 @@ import sun.net.smtp.SmtpClient; * @author Ziver */ public class Email { - public enum ContentType{ + + public enum ContentType{ PLAIN, HTML } private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z"); + private static final Pattern PATTERN_NEWLINE = Pattern.compile("(\\r\\n|\\n)"); - private String from; - private String niceFrom = null; - private String to; - private String replyTo = null; + private String fromAddress; + private String fromName = null; + private String toAddress; + private String toName = null; + private String replyToAddress = null; + private Date date = null; private ContentType type = ContentType.PLAIN; private String subject; private String message; @@ -33,33 +39,66 @@ public class Email { public void setFrom(String address){ - from = address; + this.fromAddress = sanitizeParam(address); } public void setFrom(String address, String niceName){ - from = address; - niceFrom = niceName; + fromAddress = sanitizeParam(address); + fromName = sanitizeParam(niceName); } public String getFromAddress(){ - return from; + return fromAddress; } - public void setReplyTo(String rpt){ - replyTo = rpt; + public String getFromName() { + return fromName; + } + + public void setReplyTo(String address){ + this.replyToAddress = sanitizeParam(address); } - public void setTo(String t){ - to = t; + public String getReplyToAddress() { + return replyToAddress; + } + + public void setTo(String address){ + this.toAddress = sanitizeParam(address); } - public String getTo(){ - return to; + public void setTo(String address, String niceName){ + this.toAddress = sanitizeParam(address); + this.toName = sanitizeParam(niceName); + } + public String getToAddress(){ + return toAddress; + } + public String getToName() { + return toName; + } + + public void setDate(Date date){ + this.date = date; } public void setContentType(ContentType t){ type = t; } - public void setSubject(String s){ - subject = s; + + public void setSubject(String subject){ + this.subject = sanitizeParam(subject); } + public String getSubject(){ + return subject; + } + public void setMessage(String msg){ - message = msg; + message = msg.replaceAll("(\\r\\n|\\n)", NEWLINE); + message = message.replaceAll(NEWLINE+"\\.", NEWLINE +".."); } + public String getMessage(){ + return message; + } + + + private String sanitizeParam(String param){ + return PATTERN_NEWLINE.matcher(param).replaceAll(""); + } /** @@ -67,33 +106,48 @@ public class Email { * * @throws IllegalArgumentException if from address and to address has not been set */ - public void write(PrintStream out) throws IOException{ - if(from == null) + public void write(Writer out) throws IOException{ + if(fromAddress == null) throw new IllegalArgumentException("From value cannot be null!"); - if(to == null) + if(toAddress == null) throw new IllegalArgumentException("To value cannot be null!"); //************ Headers - if (niceFrom!=null) - out.println("From: \""+niceFrom+"\" <"+from+">"); + // From + if (fromName !=null) + out.write("From: "+ fromName +" <"+ fromAddress +">"+ NEWLINE); else - out.println("From: <"+from+">"); - if ( replyTo != null ) - out.println("Reply-To: <"+replyTo+">"); - out.println("To: " + to); - out.println("Subject: "+subject); + out.write("From: "+ fromAddress + NEWLINE); + + // Reply-To + if ( replyToAddress != null ) + out.write("Reply-To: <"+ replyToAddress +">"+ NEWLINE); + + // To + if (toName !=null) + out.write("To: "+ toName +" <"+ toAddress +">"+ NEWLINE); + else + out.write("To: "+ toAddress + NEWLINE); + // Date - out.println("Date: "+dateFormatter.format(new Date(System.currentTimeMillis()))); + if (date != null) + out.write("Date: "+dateFormatter.format(date) + NEWLINE); + else + out.write("Date: "+dateFormatter.format(new Date(System.currentTimeMillis())) + NEWLINE); + // Content type switch( type ){ case HTML: - out.println("Content-Type: text/html;"); break; + out.write("Content-Type: text/html;"+ NEWLINE); break; default: - out.println("Content-Type: text/plain;"); break; + out.write("Content-Type: text/plain;"+ NEWLINE); break; } - out.println(); - + + // Subject + out.write("Subject: "+(subject!=null ? subject : "") + NEWLINE); + + out.write(NEWLINE); //*********** Mesasge - out.println( message ); + out.write( message ); } } \ No newline at end of file diff --git a/src/zutil/net/smtp/SMTPClient.java b/src/zutil/net/smtp/SMTPClient.java index 7dd9a82..b8e579a 100755 --- a/src/zutil/net/smtp/SMTPClient.java +++ b/src/zutil/net/smtp/SMTPClient.java @@ -26,10 +26,7 @@ package zutil.net.smtp; import zutil.log.LogUtil; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintStream; +import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.util.logging.Level; @@ -46,7 +43,7 @@ import java.util.logging.Logger; public class SMTPClient { private static final Logger logger = LogUtil.getLogger(); - private static final String NEWLINE = "\r\n"; + protected static final String NEWLINE = "\r\n"; private static final String CMD_HELO = "HELO"; private static final String CMD_FROM = "MAIL FROM"; private static final String CMD_TO = "RCPT TO"; @@ -59,7 +56,7 @@ public class SMTPClient { private Socket socket; private BufferedReader in; - private PrintStream out; + private Writer out; /** @@ -77,7 +74,7 @@ public class SMTPClient { public SMTPClient(String host, int port) throws IOException { socket = new Socket(host, port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - out = new PrintStream(socket.getOutputStream()); + out = new OutputStreamWriter(socket.getOutputStream()); readCommand(); sendCommand(CMD_HELO + " " + InetAddress.getLocalHost().getHostName()); @@ -92,26 +89,12 @@ public class SMTPClient { * @param msg the email body message */ public synchronized void send(String from, String to, String subj, String msg) throws IOException{ - if(from == null) - throw new IllegalArgumentException("From value cannot be null!"); - if(to == null) - throw new IllegalArgumentException("To value cannot be null!"); - try{ - // Pre metadata - sendCommand(CMD_FROM + ":" + from); - sendCommand(CMD_TO + ":" + to); - sendCommand(CMD_DATA); - // Message headers and body - out.println("From: "+from); - out.println("To: "+to); - out.println("Subject: "+subj); - out.println(""); - out.println(msg); - sendCommand(CMD_DATA_END); - reset(); - }catch(IOException e){ - logger.log(Level.SEVERE, null, e); - } + Email email = new Email(); + email.setFrom(from); + email.setTo(to); + email.setSubject(subj); + email.setMessage(msg); + send(email); } /** @@ -122,15 +105,16 @@ public class SMTPClient { public synchronized void send(Email email) throws IOException{ if(email.getFromAddress() == null) throw new IllegalArgumentException("From value cannot be null!"); - if(email.getTo() == null) + if(email.getToAddress() == null) throw new IllegalArgumentException("To value cannot be null!"); try{ // Pre metadata sendCommand(CMD_FROM + ":" + email.getFromAddress()); - sendCommand(CMD_TO + ":" + email.getTo()); + sendCommand(CMD_TO + ":" + email.getToAddress()); sendCommand(CMD_DATA); // Message headers and body email.write(out); + out.write(NEWLINE); sendCommand(CMD_DATA_END); reset(); }catch(IOException e){ @@ -148,7 +132,7 @@ public class SMTPClient { */ public synchronized int sendCommand(String cmd) throws IOException{ logger.finest(">> "+cmd); - out.print(cmd + NEWLINE); + out.write(cmd + NEWLINE); String reply = readCommand(); return parseReturnCode(reply); } diff --git a/test/zutil/net/smtp/EmailTest.java b/test/zutil/net/smtp/EmailTest.java new file mode 100644 index 0000000..887772b --- /dev/null +++ b/test/zutil/net/smtp/EmailTest.java @@ -0,0 +1,135 @@ +package zutil.net.smtp; + +import org.junit.Test; +import zutil.io.StringOutputStream; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Date; +import java.util.GregorianCalendar; + +import static org.junit.Assert.*; +import static zutil.net.smtp.SMTPClient.NEWLINE; + +/** + * Created by Ziver on 2017-01-19. + */ +public class EmailTest { + + @Test + public void sanitizingFrom(){ + Email email = new Email(); + + email.setFrom("aa\n@aa.aa"+NEWLINE); + assertEquals("aa@aa.aa", email.getFromAddress()); + + email.setFrom("aa\n@aa.aa"+NEWLINE, "aa\n bb"+NEWLINE); + assertEquals("aa@aa.aa", email.getFromAddress()); + assertEquals("aa bb", email.getFromName()); + } + + @Test + public void sanitizingReplyTo(){ + Email email = new Email(); + + email.setReplyTo("aa\n@aa.aa"+NEWLINE); + assertEquals("aa@aa.aa", email.getReplyToAddress()); + } + + @Test + public void sanitizingTo(){ + Email email = new Email(); + + email.setTo("aa\n@aa.aa"+NEWLINE); + assertEquals("aa@aa.aa", email.getToAddress()); + + email.setTo("aa\n@aa.aa"+NEWLINE, "aa\n bb"+NEWLINE); + assertEquals("aa@aa.aa", email.getToAddress()); + assertEquals("aa bb", email.getToName()); + } + + @Test + public void sanitizingSubject(){ + Email email = new Email(); + + email.setSubject("aa\n aa aa"+NEWLINE); + assertEquals("aa aa aa", email.getSubject()); + } + + @Test + public void sanitizingMessage(){ + Email email = new Email(); + + email.setMessage("aa\nbb"+ NEWLINE +"cc\n"); + assertEquals("aa"+ NEWLINE +"bb"+ NEWLINE +"cc"+ NEWLINE, + email.getMessage()); + + email.setMessage("aa"+ NEWLINE +"."+ NEWLINE +"bb"); + assertEquals("aa"+ NEWLINE +".."+ NEWLINE +"bb", + email.getMessage()); + + email.setMessage("aa\n.\nbb"); + assertEquals("aa"+ NEWLINE +".."+ NEWLINE +"bb", + email.getMessage()); + } + + + + @Test + public void simpleEmail() throws IOException { + Email email = new Email(); + email.setFrom("test@example.com"); + email.setTo("to@example.com"); + email.setDate(getDate()); + email.setMessage("message"); + + assertEquals( + "From: test@example.com" + NEWLINE + + "To: to@example.com" + NEWLINE + + "Date: on, 22 nov 2000 15:20:55 +0100" + NEWLINE + + "Content-Type: text/plain;" + NEWLINE + + "Subject: " + NEWLINE + + NEWLINE + + "message", + getEmailString(email)); + } + + @Test + public void fullEmail() throws IOException { + Email email = new Email(); + email.setFrom("test@example.com", "Test Tester"); + email.setTo("to@example.com", "To Totter"); + email.setDate(getDate()); + email.setContentType(Email.ContentType.HTML); + email.setReplyTo("mokey@example.org"); + email.setSubject("Title"); + email.setMessage("\n\nmessage\n\n"); + + assertEquals( + "From: Test Tester " + NEWLINE + + "Reply-To: " + NEWLINE + + "To: To Totter " + NEWLINE + + "Date: on, 22 nov 2000 15:20:55 +0100" + NEWLINE + + "Content-Type: text/html;" + NEWLINE + + "Subject: Title" + NEWLINE + + NEWLINE + + ""+NEWLINE+""+NEWLINE+"message"+NEWLINE+""+NEWLINE+"", + getEmailString(email)); + } + + + + + private String getEmailString(Email email) throws IOException { + StringOutputStream buff = new StringOutputStream(); + OutputStreamWriter out = new OutputStreamWriter(buff); + email.write(out); + out.flush(); + return buff.toString(); + } + + private Date getDate(){ + GregorianCalendar date = new GregorianCalendar(2000,10,22, 15,20,55); + return date.getTime(); + } +} \ No newline at end of file