package railo.runtime.net.smtp; import java.io.FileNotFoundException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.TimeZone; import javax.activation.DataHandler; import javax.mail.Authenticator; import javax.mail.BodyPart; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimePart; import org.apache.commons.collections.ReferenceMap; import railo.commons.activation.ResourceDataSource; import railo.commons.digest.MD5; import railo.commons.io.SystemUtil; import railo.commons.io.log.LogAndSource; import railo.commons.io.log.LogUtil; import railo.commons.io.res.Resource; import railo.commons.io.res.util.ResourceUtil; import railo.commons.lang.SerializableObject; import railo.commons.lang.StringUtil; import railo.runtime.config.Config; import railo.runtime.config.ConfigWeb; import railo.runtime.config.ConfigWebImpl; import railo.runtime.engine.ThreadLocalPageContext; import railo.runtime.exp.ExpressionException; import railo.runtime.exp.PageException; import railo.runtime.net.mail.MailException; import railo.runtime.net.mail.MailPart; import railo.runtime.net.mail.MailUtil; import railo.runtime.net.mail.Server; import railo.runtime.net.mail.ServerImpl; import railo.runtime.net.proxy.Proxy; import railo.runtime.net.proxy.ProxyData; import railo.runtime.net.proxy.ProxyDataImpl; import railo.runtime.net.smtp.SMTPConnectionPool.SessionAndTransport; import railo.runtime.op.Caster; import railo.runtime.spooler.mail.MailSpoolerTask; import railo.runtime.type.util.ArrayUtil; import railo.runtime.type.util.ListUtil; import com.sun.mail.smtp.SMTPMessage; public final class SMTPClient implements Serializable { /** * */ private static final long serialVersionUID = 5227282806519740328L; private static final int SPOOL_UNDEFINED=0; private static final int SPOOL_YES=1; private static final int SPOOL_NO=2; private static final int SSL_NONE=0; private static final int SSL_YES=1; private static final int SSL_NO=2; private static final int TLS_NONE=0; private static final int TLS_YES=1; private static final int TLS_NO=2; private static final String TEXT_HTML = "text/html"; private static final String TEXT_PLAIN = "text/plain"; private static final SerializableObject LOCK = new SerializableObject(); private static Map<TimeZone, SimpleDateFormat> formatters=new ReferenceMap(ReferenceMap.SOFT,ReferenceMap.SOFT); //private static final int PORT = 25; private int spool=SPOOL_UNDEFINED; private int timeout=-1; private String plainText; private String plainTextCharset; private String htmlText; private String htmlTextCharset; private Attachment[] attachmentz; private String[] host; private String charset="UTF-8"; private InternetAddress from; private InternetAddress[] tos; private InternetAddress[] bccs; private InternetAddress[] ccs; private InternetAddress[] rts; private InternetAddress[] fts; private String subject=""; private String xmailer="Railo Mail"; private Map<String,String> headers=new HashMap<String,String>(); private int port=-1; private String username; private String password=""; private int ssl=SSL_NONE; private int tls=TLS_NONE; ProxyData proxyData=new ProxyDataImpl(); private ArrayList<MailPart> parts; private TimeZone timeZone; public static String getNow(TimeZone tz){ tz = ThreadLocalPageContext.getTimeZone(tz); SimpleDateFormat df=formatters.get(tz); if(df==null) { df = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z (z)",Locale.US); df.setTimeZone(tz); formatters.put(tz, df); } return df.format(new Date()); } public void setSpoolenable(boolean spoolenable) { spool=spoolenable?SPOOL_YES:SPOOL_NO; } /** * set port of the mailserver * @param port */ public void setPort(int port) { this.port=port; } /** * @param charset the charset to set */ public void setCharset(String charset) { this.charset = charset; } public static ServerImpl toServerImpl(String server,int port, String usr,String pwd) throws MailException { int index; // username/password index=server.indexOf('@'); if(index!=-1) { usr=server.substring(0,index); server=server.substring(index+1); index=usr.indexOf(':'); if(index!=-1) { pwd=usr.substring(index+1); usr=usr.substring(0,index); } } // port index=server.indexOf(':'); if(index!=-1) { try { port=Caster.toIntValue(server.substring(index+1)); } catch (ExpressionException e) { throw new MailException(e.getMessage()); } server=server.substring(0,index); } ServerImpl srv = ServerImpl.getInstance(server, port, usr, pwd, false, false); return srv; } public void setHost(String host) throws PageException { if(!StringUtil.isEmpty(host,true))this.host = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(host, ',')); } /** * @param password the password to set */ public void setPassword(String password) { this.password = password; } /** * @param username the username to set */ public void setUsername(String username) { this.username = username; } public void addHeader(String name, String value) { headers.put(name, value); } public void addTo(InternetAddress to) { tos=add(tos,to); } public void addTo(Object to) throws UnsupportedEncodingException, PageException, MailException { InternetAddress[] tmp = MailUtil.toInternetAddresses(to); for(int i=0;i<tmp.length;i++) { addTo(tmp[i]); } } public void setFrom(InternetAddress from) { this.from=from; } public void setFrom(Object from) throws UnsupportedEncodingException, MailException, PageException { InternetAddress[] addrs = MailUtil.toInternetAddresses(from); if(addrs.length==0) return; setFrom(addrs[0]); } public void addBCC(InternetAddress bcc) { bccs=add(bccs,bcc); } public void addBCC(Object bcc) throws UnsupportedEncodingException, MailException, PageException { InternetAddress[] tmp = MailUtil.toInternetAddresses(bcc); for(int i=0;i<tmp.length;i++) { addBCC(tmp[i]); } } public void addCC(InternetAddress cc) { ccs=add(ccs,cc); } public void addCC(Object cc) throws UnsupportedEncodingException, MailException, PageException { InternetAddress[] tmp = MailUtil.toInternetAddresses(cc); for(int i=0;i<tmp.length;i++) { addCC(tmp[i]); } } public void addReplyTo(InternetAddress rt) { rts=add(rts,rt); } public void addReplyTo(Object rt) throws UnsupportedEncodingException, MailException, PageException { InternetAddress[] tmp = MailUtil.toInternetAddresses(rt); for(int i=0;i<tmp.length;i++) { addReplyTo(tmp[i]); } } public void addFailTo(InternetAddress ft) { fts=add(fts,ft); } public String getHTMLTextAsString() { return htmlText; } public String getPlainTextAsString() { return plainText; } public void addFailTo(Object ft) throws UnsupportedEncodingException, MailException, PageException { InternetAddress[] tmp = MailUtil.toInternetAddresses(ft); for(int i=0;i<tmp.length;i++) { addFailTo(tmp[i]); } } /** * @param timeout the timeout to set */ public void setTimeout(int timeout) { this.timeout = timeout; } public void setSubject(String subject) { this.subject=subject; } public void setXMailer(String xmailer) { this.xmailer=xmailer; } /** * creates a new expanded array and return it; * @param oldArr * @param newValue * @return new expanded array */ protected static InternetAddress[] add(InternetAddress[] oldArr, InternetAddress newValue) { if(oldArr==null) return new InternetAddress[] {newValue}; //else { InternetAddress[] tmp=new InternetAddress[oldArr.length+1]; for(int i=0;i<oldArr.length;i++) { tmp[i]=oldArr[i]; } tmp[oldArr.length]=newValue; return tmp; //} } protected static Attachment[] add(Attachment[] oldArr, Attachment newValue) { if(oldArr==null) return new Attachment[] {newValue}; //else { Attachment[] tmp=new Attachment[oldArr.length+1]; for(int i=0;i<oldArr.length;i++) { tmp[i]=oldArr[i]; } tmp[oldArr.length]=newValue; return tmp; //} } public static class MimeMessageAndSession { public final MimeMessage message; public final SessionAndTransport session; public MimeMessageAndSession(MimeMessage message,SessionAndTransport session){ this.message=message; this.session=session; } } private MimeMessageAndSession createMimeMessage(railo.runtime.config.Config config,String hostName, int port, String username, String password, boolean tls,boolean ssl) throws MessagingException { Properties props = (Properties) System.getProperties().clone(); String strTimeout = Caster.toString(getTimeout(config)); props.put("mail.smtp.host", hostName); props.put("mail.smtp.timeout", strTimeout); props.put("mail.smtp.connectiontimeout", strTimeout); if(port>0){ props.put("mail.smtp.port", Caster.toString(port)); } if(ssl) { props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); props.put("mail.smtp.socketFactory.port", Caster.toString(port)); props.put("mail.smtp.socketFactory.fallback", "false"); } else { props.put("mail.smtp.socketFactory.class", "javax.net.SocketFactory"); props.remove("mail.smtp.socketFactory.port"); props.remove("mail.smtp.socketFactory.fallback"); } Authenticator auth=null; if(!StringUtil.isEmpty(username)) { props.put("mail.smtp.auth", "true"); props.put("mail.smtp.starttls.enable",tls?"true":"false"); props.put("mail.smtp.user", username); props.put("mail.smtp.password", password); props.put("password", password); auth=new SMTPAuthenticator( username, password ); } else { props.put("mail.smtp.auth", "false"); props.remove("mail.smtp.starttls.enable"); props.remove("mail.smtp.user"); props.remove("mail.smtp.password"); props.remove("password"); } SessionAndTransport sat = SMTPConnectionPool.getSessionAndTransport(props,hash(props),auth); // Contacts SMTPMessage msg = new SMTPMessage(sat.session); if(from==null)throw new MessagingException("you have do define the from for the mail"); //if(tos==null)throw new MessagingException("you have do define the to for the mail"); checkAddress(from,charset); //checkAddress(tos,charset); msg.setFrom(from); //msg.setRecipients(Message.RecipientType.TO, tos); if(tos!=null){ checkAddress(tos,charset); msg.setRecipients(Message.RecipientType.TO, tos); } if(ccs!=null){ checkAddress(ccs,charset); msg.setRecipients(Message.RecipientType.CC, ccs); } if(bccs!=null){ checkAddress(bccs,charset); msg.setRecipients(Message.RecipientType.BCC, bccs); } if(rts!=null){ checkAddress(rts,charset); msg.setReplyTo(rts); } if(fts!=null){ checkAddress(fts,charset); msg.setEnvelopeFrom(fts[0].toString()); } // Subject and headers try { msg.setSubject(MailUtil.encode(subject, charset)); } catch (UnsupportedEncodingException e) { throw new MessagingException("the encoding "+charset+" is not supported"); } msg.setHeader("X-Mailer", xmailer); msg.setHeader("Date",getNow(timeZone)); //msg.setSentDate(new Date()); Multipart mp=null; // Only HTML if(plainText==null) { if(ArrayUtil.isEmpty(attachmentz) && ArrayUtil.isEmpty(parts)){ fillHTMLText(msg); setHeaders(msg,headers); return new MimeMessageAndSession(msg,sat); } mp = new MimeMultipart(); mp.addBodyPart(getHTMLText()); } // only Plain else if(htmlText==null) { if(ArrayUtil.isEmpty(attachmentz) && ArrayUtil.isEmpty(parts)){ fillPlainText(msg); setHeaders(msg,headers); return new MimeMessageAndSession(msg,sat); } mp = new MimeMultipart(); mp.addBodyPart(getPlainText()); } // Plain and HTML else { mp=new MimeMultipart("alternative"); mp.addBodyPart(getPlainText()); mp.addBodyPart(getHTMLText()); if(!ArrayUtil.isEmpty(attachmentz) || !ArrayUtil.isEmpty(parts)){ MimeBodyPart content = new MimeBodyPart(); content.setContent(mp); mp = new MimeMultipart(); mp.addBodyPart(content); } } // parts if(!ArrayUtil.isEmpty(parts)){ Iterator<MailPart> it = parts.iterator(); if(mp instanceof MimeMultipart) ((MimeMultipart)mp).setSubType("alternative"); while(it.hasNext()){ mp.addBodyPart(toMimeBodyPart(it.next())); } } // Attachments if(!ArrayUtil.isEmpty(attachmentz)){ for(int i=0;i<attachmentz.length;i++) { mp.addBodyPart(toMimeBodyPart(mp,config,attachmentz[i])); } } msg.setContent(mp); setHeaders(msg,headers); return new MimeMessageAndSession(msg,sat); } private static String hash(Properties props) { Enumeration<?> e = props.propertyNames(); java.util.List<String> names=new ArrayList<String>(); String str; while(e.hasMoreElements()){ str=Caster.toString(e.nextElement(),null); if(!StringUtil.isEmpty(str) && str.startsWith("mail.smtp.")) names.add(str); } Collections.sort(names); StringBuilder sb=new StringBuilder(); Iterator<String> it = names.iterator(); while(it.hasNext()){ str=it.next(); sb.append(str).append(':').append(props.getProperty(str)).append(';'); } str=sb.toString(); return MD5.getDigestAsString(str,str); } private static void setHeaders(SMTPMessage msg, Map<String,String> headers) throws MessagingException { Iterator<Entry<String, String>> it = headers.entrySet().iterator(); Entry<String, String> e; while(it.hasNext()) { e = it.next(); msg.setHeader(e.getKey(),e.getValue()); } } private void checkAddress(InternetAddress[] ias,String charset) { // DIFF 23 for(int i=0;i<ias.length;i++) { checkAddress(ias[i], charset); } } private void checkAddress(InternetAddress ia,String charset) { // DIFF 23 try { if(!StringUtil.isEmpty(ia.getPersonal())) { String personal = MailUtil.encode(ia.getPersonal(), charset); if(!personal.equals(ia.getPersonal())) ia.setPersonal(personal); } } catch (UnsupportedEncodingException e) {} } /** * @param plainText */ public void setPlainText(String plainText) { this.plainText=plainText; this.plainTextCharset=charset; } /** * @param plainText * @param plainTextCharset */ public void setPlainText(String plainText, String plainTextCharset) { this.plainText=plainText; this.plainTextCharset=plainTextCharset; } /** * @param htmlText */ public void setHTMLText(String htmlText) { this.htmlText=htmlText; this.htmlTextCharset=charset; } public boolean hasHTMLText() { return htmlText!=null; } public boolean hasPlainText() { return plainText!=null; } /** * @param htmlText * @param htmlTextCharset */ public void setHTMLText(String htmlText, String htmlTextCharset) { this.htmlText=htmlText; this.htmlTextCharset=htmlTextCharset; } public void addAttachment(URL url) { Attachment mbp = new Attachment(url); attachmentz=add(attachmentz, mbp); } public void addAttachment(Resource resource, String type, String disposition, String contentID,boolean removeAfterSend) { Attachment att = new Attachment(resource, type, disposition, contentID,removeAfterSend); attachmentz=add(attachmentz, att); } public MimeBodyPart toMimeBodyPart(Multipart mp, railo.runtime.config.Config config,Attachment att) throws MessagingException { MimeBodyPart mbp = new MimeBodyPart(); // set Data Source String strRes = att.getAbsolutePath(); if(!StringUtil.isEmpty(strRes)){ mbp.setDataHandler(new DataHandler(new ResourceDataSource(config.getResource(strRes)))); } else mbp.setDataHandler(new DataHandler(new URLDataSource2(att.getURL()))); mbp.setFileName(att.getFileName()); if(!StringUtil.isEmpty(att.getType())) mbp.setHeader("Content-Type", att.getType()); if(!StringUtil.isEmpty(att.getDisposition())){ mbp.setDisposition(att.getDisposition()); if(mp instanceof MimeMultipart) ((MimeMultipart)mp).setSubType("related"); } if(!StringUtil.isEmpty(att.getContentID()))mbp.setContentID(att.getContentID()); return mbp; } /** * @param file * @throws MessagingException * @throws FileNotFoundException */ public void addAttachment(Resource file) throws MessagingException { addAttachment(file,null,null,null,false); } public void send(ConfigWeb config) throws MailException { if(ArrayUtil.isEmpty(config.getMailServers()) && ArrayUtil.isEmpty(host)) throw new MailException("no SMTP Server defined"); if(plainText==null && htmlText==null) throw new MailException("you must define plaintext or htmltext"); ///if(timeout<1)timeout=config.getMailTimeout()*1000; if(spool==SPOOL_YES || (spool==SPOOL_UNDEFINED && config.isMailSpoolEnable())) { config.getSpoolerEngine().add(new MailSpoolerTask(this)); } else _send(config); } public void _send(railo.runtime.config.ConfigWeb config) throws MailException { long start=System.nanoTime(); long _timeout = getTimeout(config); try { Proxy.start(proxyData); LogAndSource log = config.getMailLogger(); // Server Server[] servers = config.getMailServers(); if(host!=null) { int prt; String usr,pwd; ServerImpl[] nServers = new ServerImpl[host.length]; for(int i=0;i<host.length;i++) { usr=null;pwd=null; prt=ServerImpl.DEFAULT_PORT; if(port>0)prt=port; if(!StringUtil.isEmpty(username)) { usr=username; pwd=password; } nServers[i]=toServerImpl(host[i],prt,usr,pwd); if(ssl==SSL_YES) nServers[i].setSSL(true); if(tls==TLS_YES) nServers[i].setTLS(true); } servers=nServers; } if(servers.length==0) { //return; throw new MailException("no SMTP Server defined"); } boolean _ssl,_tls; for(int i=0;i<servers.length;i++) { Server server = servers[i]; String _username=null,_password=""; //int _port; // username/password if(server.hasAuthentication()) { _username=server.getUsername(); _password=server.getPassword(); } // tls if(tls!=TLS_NONE)_tls=tls==TLS_YES; else _tls=((ServerImpl)server).isTLS(); // ssl if(ssl!=SSL_NONE)_ssl=ssl==SSL_YES; else _ssl=((ServerImpl)server).isSSL(); MimeMessageAndSession msgSess; synchronized(LOCK) { try { msgSess = createMimeMessage(config,server.getHostName(),server.getPort(),_username,_password,_tls,_ssl); } catch (MessagingException e) { // listener listener(config,server,log,e,System.nanoTime()-start); MailException me = new MailException(e.getMessage()); me.setStackTrace(e.getStackTrace()); throw me; } try { SerializableObject lock = new SerializableObject(); SMTPSender sender=new SMTPSender(lock,msgSess,server.getHostName(),server.getPort(),_username,_password); sender.start(); SystemUtil.wait(lock, _timeout); if(!sender.hasSended()) { Throwable t = sender.getThrowable(); if(t!=null) throw Caster.toPageException(t); // stop when still running try{ if(sender.isAlive())sender.stop(); } catch(Throwable t2){} // after thread s stopped check send flag again if(!sender.hasSended()){ throw new MessagingException("timeout occurred after "+(_timeout/1000)+" seconds while sending mail message"); } } clean(config,attachmentz); listener(config,server,log,null,System.nanoTime()-start); break; } catch (Exception e) {e.printStackTrace(); if(i+1==servers.length) { listener(config,server,log,e,System.nanoTime()-start); MailException me = new MailException(server.getHostName()+" "+LogUtil.toMessage(e)+":"+i); me.setStackTrace(e.getStackTrace()); throw me; } } } } } finally { Proxy.end(); } } private void listener(ConfigWeb config,Server server, LogAndSource log, Exception e, long exe) { StringBuilder sbTos=new StringBuilder(); for(int i=0;i<tos.length;i++){ if(sbTos.length()>0)sbTos.append(", "); sbTos.append(tos[i].toString()); } if(e==null) log.info("mail","mail sended (from:"+from.toString()+"; to:"+sbTos+" subject:"+subject+")"); else log.error("mail",LogUtil.toMessage(e)); // listener Map<String,Object> props=new HashMap<String,Object>(); props.put("attachments", this.attachmentz); props.put("bccs", this.bccs); props.put("ccs", this.ccs); props.put("charset", this.charset); props.put("from", this.from); props.put("fts", this.fts); props.put("headers", this.headers); props.put("host", server.getHostName()); props.put("htmlText", this.htmlText); props.put("htmlTextCharset", this.htmlTextCharset); props.put("parts", this.parts); props.put("password", this.password); props.put("plainText", this.plainText); props.put("plainTextCharset", this.plainTextCharset); props.put("port", server.getPort()); props.put("proxyData", this.proxyData); props.put("rts", this.rts); props.put("subject", this.subject); props.put("timeout", getTimeout(config)); props.put("timezone", this.timeZone); props.put("tos", this.tos); props.put("username", this.username); props.put("xmailer", this.xmailer); ((ConfigWebImpl)config).getActionMonitorCollector() .log(config, "mail", "Mail", exe, props); } private long getTimeout(Config config) { return timeout>0?timeout:config.getMailTimeout()*1000L; } // remove all atttachements that are marked to remove private static void clean(Config config, Attachment[] attachmentz) { if(attachmentz!=null)for(int i=0;i<attachmentz.length;i++){ if(attachmentz[i].isRemoveAfterSend()){ Resource res = config.getResource(attachmentz[i].getAbsolutePath()); ResourceUtil.removeEL(res,true); } } } private MimeBodyPart getHTMLText() throws MessagingException { MimeBodyPart html = new MimeBodyPart(); fillHTMLText(html); return html; } private void fillHTMLText(MimePart mp) throws MessagingException { mp.setDataHandler(new DataHandler(new StringDataSource(htmlText,TEXT_HTML ,htmlTextCharset))); mp.setHeader("Content-Transfer-Encoding", "7bit"); mp.setHeader("Content-Type", TEXT_HTML+"; charset="+htmlTextCharset); } private MimeBodyPart getPlainText() throws MessagingException { MimeBodyPart plain = new MimeBodyPart(); fillPlainText(plain); return plain; } private void fillPlainText(MimePart mp) throws MessagingException { mp.setDataHandler(new DataHandler(new StringDataSource(plainText,TEXT_PLAIN ,plainTextCharset))); mp.setHeader("Content-Transfer-Encoding", "7bit"); mp.setHeader("Content-Type", TEXT_PLAIN+"; charset="+plainTextCharset); } private BodyPart toMimeBodyPart(MailPart part) throws MessagingException { MimeBodyPart mbp = new MimeBodyPart(); mbp.setDataHandler(new DataHandler(new StringDataSource(part.getBody(),part.getType() ,part.getCharset()))); //mbp.setHeader("Content-Transfer-Encoding", "7bit"); //mbp.setHeader("Content-Type", TEXT_PLAIN+"; charset="+plainTextCharset); return mbp; } /** * @return the proxyData */ public ProxyData getProxyData() { return proxyData; } /** * @param proxyData the proxyData to set */ public void setProxyData(ProxyData proxyData) { this.proxyData = proxyData; } /** * @param ssl the ssl to set */ public void setSSL(boolean ssl) { this.ssl = ssl?SSL_YES:SSL_NO; } /** * @param tls the tls to set */ public void setTLS(boolean tls) { this.tls = tls?TLS_YES:TLS_NO; } /** * @return the subject */ public String getSubject() { return subject; } /** * @return the from */ public InternetAddress getFrom() { return from; } /** * @return the tos */ public InternetAddress[] getTos() { return tos; } /** * @return the bccs */ public InternetAddress[] getBccs() { return bccs; } /** * @return the ccs */ public InternetAddress[] getCcs() { return ccs; } public void setPart(MailPart part) { if(parts==null) parts=new ArrayList<MailPart>(); parts.add(part); } public void setTimeZone(TimeZone timeZone) { this.timeZone=timeZone; } }