/*
* Copyright (C) 2000 - 2015 aw2.0 Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://www.openbd.org/
* $Id: OutgoingMailServer.java 2524 2015-02-22 23:09:07Z alan $
*/
package com.bluedragon.platform.java.smtp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import com.bluedragon.platform.SmtpInterface;
import com.bluedragon.plugin.ObjectCFC;
import com.bluedragon.plugin.PluginManager;
import com.nary.io.FileUtils;
import com.nary.util.HashMapTimed;
import com.nary.util.LogFile;
import com.nary.util.stringtokenizer;
import com.naryx.tagfusion.cfm.application.cfAPPLICATION;
import com.naryx.tagfusion.cfm.application.cfApplicationData;
import com.naryx.tagfusion.cfm.engine.cfArrayData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfStringData;
import com.naryx.tagfusion.cfm.engine.cfmRunTimeException;
import com.naryx.tagfusion.cfm.engine.engineListener;
import com.naryx.tagfusion.cfm.engine.variableStore;
import com.naryx.tagfusion.cfm.mail.cfMAIL;
import com.naryx.tagfusion.xmlConfig.xmlCFML;
/**
* Mail Server. This class monitors all the mail and controls the transmission
*
*/
public class OutgoingMailServer implements engineListener, SmtpInterface {
public static final String DEFAULT_MAILTHREADS = "1";
private static enum SendType {
NONE, SSL, TLS
};
File spoolDirectory, failedDirectory;
private AtomicLong uniqueID = new AtomicLong(System.currentTimeMillis());
private int mailOut = 0;
private long mailSize = 0;
private String smtpServer, smtpPort;
private boolean useSSL, useTLS;
private volatile boolean stayAlive = true;
private String domain;
private Object semaphore = new Object();
private fileFilter emailFileFilter = new fileFilter();
private int mailThreads;
private int timeout;
private InternetAddress[] catchEmails = null;
private String catchEmailList;
private long lastMailSent = 0;
private long delaySendMS = 200;
private Object lastSemaphoreSent = new Object();
private List<Thread> mailThreadList;
// needed in order to create a MimeMessage for reading in the message.
// Its properties should not be populated because it is not used in the
// sending
private Session dummySession = Session.getInstance(new Properties());
private HashMapTimed sessions = new HashMapTimed();
public OutgoingMailServer(xmlCFML config) {
File mainDirectory = new File(cfEngine.thisPlatform.getFileIO().getWorkingDirectory(), "cfmail");
try {
mainDirectory = FileUtils.checkAndCreateDirectory(cfEngine.thisPlatform.getFileIO().getWorkingDirectory(), "cfmail", false);
spoolDirectory = FileUtils.checkAndCreateDirectory(mainDirectory, "spool", false);
failedDirectory = FileUtils.checkAndCreateDirectory(mainDirectory, "undelivered", false);
} catch (Exception E) {
cfEngine.log("OutgoingMailServer failed to create all the CFMAIL spooling directorys: " + mainDirectory);
}
LogFile.open("MAIL", new File(mainDirectory, "mail.log").toString());
LogFile.println("MAIL", "OutgoingMailServer started");
cfEngine.log("OutgoingMailServer started mainDirectory=" + mainDirectory.toString());
mailThreadList = new ArrayList<Thread>();
setMailSettings(config);
if ( catchEmails != null && catchEmails.length > 0 )
cfEngine.log("OutgoingMailServer catchemail=" + catchEmailList );
cfEngine.registerEngineListener(this);
}
private void setMailSettings(xmlCFML config) {
// default value is loopback IP address
smtpServer = config.getString("server.cfmail.smtpserver", DEFAULT_SMTP_SERVER);
domain = config.getString("server.cfmail.domain", "");
smtpPort = config.getString("server.cfmail.smtpport", DEFAULT_SMTP_PORT);
timeout = config.getInt("server.cfmail.timeout", Integer.parseInt(DEFAULT_TIMEOUT));
delaySendMS = config.getLong("server.cfmail.ratedelay", -1);
mailThreads = config.getInt("server.cfmail.threads", Integer.parseInt(DEFAULT_MAILTHREADS));
useSSL = config.getBoolean("server.cfmail.usessl", false);
useTLS = config.getBoolean("server.cfmail.usetls", false);
catchEmailList = config.getString("server.cfmail.catchemail", "");
catchEmails = cfMAIL.getAddresses( catchEmailList, SmtpInterface.DEFAULT_CHARSET );
// check if mailThreads has changed
if (mailThreads > mailThreadList.size()) { // new threads need to be created
int newThreads = mailThreads - mailThreadList.size();
for (int i = 0; i < newThreads; i++) {
Thread t = new MailSender();
mailThreadList.add(t);
t.start();
}
} else if (mailThreads < mailThreadList.size()) { // need to shutdown threads
int shutdownThreads = mailThreadList.size() - mailThreads;
for (int i = 0; i < shutdownThreads; i++) {
MailSender ms = (MailSender) mailThreadList.remove(0);
ms.shutdown();
}
}
}
public int getDefaultTimeout() {
return timeout;
}
public int getTotalMails() {
return mailOut;
}
public long getTotalMailSize() {
return mailSize;
}
public String getDomain() {
return domain;
}
public void engineAdminUpdate(xmlCFML config) {
setMailSettings(config);
cfEngine.log("OutgoingMailServer Configuration Updated");
}
public void engineShutdown() {
stayAlive = false;
for (int i = 0; i < this.mailThreadList.size(); i++) {
((MailSender) mailThreadList.get(i)).shutdown();
}
sessions.destroy();
cfEngine.log("OutgoingMailServer: Shutdown");
}
private void rateLimitSend(){
if ( delaySendMS < 0 )
return;
for (;;){
synchronized(lastSemaphoreSent){
if ( (System.currentTimeMillis() - lastMailSent) >= delaySendMS ){
lastMailSent = System.currentTimeMillis();
return;
}
}
try {
Thread.sleep( delaySendMS >> 2 );
} catch (InterruptedException e) {
return;
}
}
}
public void send(MimeMessage msg) {
if (msg == null)
return;
long msgID = uniqueID.getAndIncrement(); // atomic increment
/*
* Write out the email to a temp file so it won't be picked up by a
* MailSender thread until we are done writing it out. This is the fix for
* bug #3285.
*/
BufferedOutputStream out = null;
FileOutputStream fileOut = null;
File filename = new File(spoolDirectory, msgID + ".spool");
if ( !filename.getParentFile().exists() )
filename.getParentFile().mkdirs();
try {
fileOut = new FileOutputStream(filename);
out = new BufferedOutputStream(fileOut);
msg.writeTo(out);
out.flush();
} catch (Exception E) {
LogFile.println("MAIL", E);
} finally {
try {
if (out != null)
out.close();
} catch (IOException ignoreE) {}
try {
if (fileOut != null)
fileOut.close();
} catch (IOException ignoreE) {}
// Now rename to a file with a .email extension so it will be picked up by
// a MailSender thread. This is the fix for bug #3285.
try {
if (!filename.renameTo(new File(spoolDirectory, msgID + ".email")))
LogFile.println("MAIL", "ERROR - Failed to rename [" + filename.getAbsolutePath() + "]");
} catch (Exception exc) {
LogFile.println("MAIL", "ERROR - Exception occurred while trying to rename [" + filename.getAbsolutePath() + "], exc=" + exc.toString());
}
}
notifySenders();
}
public void notifySenders() {
synchronized (semaphore) {
semaphore.notify();
}
}
class MailSender extends Thread {
private boolean shutdown;
public MailSender() {
super("OpenBD MailSender");
setDaemon(true);
setPriority(Thread.MIN_PRIORITY);
}
public void shutdown() {
shutdown = true;
this.interrupt();
}
private File getMail() {
synchronized (semaphore) {
// Get the files from the spool directory that end with .email
String[] fileArr = spoolDirectory.list(emailFileFilter);
// Keep waiting until we find a file that ends with .email
while ((fileArr == null) || (fileArr.length == 0)) {
try {
semaphore.wait(60000);
fileArr = spoolDirectory.list(emailFileFilter);
} catch (InterruptedException ignored) {
if (shutdown) {
return null;
}
}
}
// Rename from xxx.email to xxx.tmpsend so no other MailSender threads will pick up the file
String oldName = fileArr[0];
String newName = oldName.substring(0, oldName.length() - ".email".length()) + ".tmpsend";
File oldMailFile = new File(spoolDirectory, oldName);
File newMailFile = new File(spoolDirectory, newName);
oldMailFile.renameTo(newMailFile);
return newMailFile;
}
}
public void run() {
File file;
try {
while (stayAlive && !shutdown && (file = getMail()) != null) {
sendMail(file);
}
} catch (Throwable t) {
// handle unexpected exceptions
// e.g. running BD with gcj can result in
// java.lang.NoClassDefFoundError: javax/security/sasl/SaslException
cfEngine.log("mailSender failed due to unexpected exception: " + t.getClass().getName());
}
cfEngine.log("mailSender: thread stop.");
}
}
private Session getSession(String _host, String _port, String _timeout, boolean _auth, String _returnPath, SendType _sendType) {
Properties props = new Properties();
String propPrefix = "mail.smtp";
if (_sendType == SendType.SSL) {
propPrefix += "s";
}
props.put(propPrefix + ".host", _host);
props.put(propPrefix + ".port", _port);
props.put(propPrefix + ".timeout", _timeout);
if (_sendType == SendType.TLS) {
props.put(propPrefix + ".starttls.enable", "true");
props.put(propPrefix + ".starttls.required", "true");
}
if (domain.length() != 0) {
props.put(propPrefix + ".localhost", domain);
}
if (_auth) {
props.put(propPrefix + ".auth", "true");
}
if (_returnPath != null) { // fix for bug #2986
props.put(propPrefix + ".from", _returnPath);
}
String key = props.toString();
Session session = (Session) sessions.get(key);
// -- if session doesn't already exist, create one
if (session == null) {
session = Session.getInstance(props);
sessions.put(key, session);
}
return session;
}
private boolean sendMail(File filename) {
String To = "", From = "", Subject = "";
String callbackCFC = null, appname = null, customData = null;
CustomMessage msg = null;
FileInputStream fileIn = null;
BufferedInputStream in = null;
boolean deleteFile = false;
String origFilename = filename.getName();
origFilename = origFilename.substring(0, origFilename.lastIndexOf(".")) + ".email";
try {
// Load in the file
fileIn = new FileInputStream(filename);
in = new BufferedInputStream(fileIn);
msg = new CustomMessage(dummySession, in);
// Message is now in; run through the servers and attempt to deliver it
String servers[] = msg.getHeader("X-BlueDragon-server");
msg.removeHeader("X-BlueDragon-server");
String timeout = msg.getHeader("X-BlueDragon-timeout", ",");
msg.removeHeader("X-BlueDragon-timeout");
// Callback details
callbackCFC = msg.getHeader("X-BlueDragon-callback", ",");
appname = msg.getHeader("X-BlueDragon-appname", ",");
customData = msg.getHeader("X-BlueDragon-callbackdata", ",");
msg.removeHeader("X-BlueDragon-callback");
msg.removeHeader("X-BlueDragon-appname");
msg.removeHeader("X-BlueDragon-callbackdata");
// SSL details
String ssl = msg.getHeader("X-BlueDragon-SSL", ",");
msg.removeHeader("X-BlueDragon-SSL");
SendType sendType = SendType.NONE;
if ("ssl".equals(ssl)) {
sendType = SendType.SSL;
} else if ("tls".equals(ssl)) {
sendType = SendType.TLS;
}
boolean gotAtLeastOneGoodServerAndPortCombo = false;
for (int x = 0; x < servers.length; x++) {
Transport transport = null;
String catchLog = "";
try {
stringtokenizer st = new stringtokenizer(servers[x], ";");
String username = st.nextToken();
String password = st.nextToken();
String server = st.nextToken();
String port = st.nextToken();
// here is part of the fix for bug #2088
if (server == null || server.trim().equals("") || port == null || port.trim().equals("")) {
continue;
} else
gotAtLeastOneGoodServerAndPortCombo = true;
// Now we need to deliver it
boolean auth = (username.length() != 0 && password.length() != 0);
String[] returnPath = msg.getHeader("Return-Path");
Session realSession = getSession(server, port, timeout, auth, (returnPath == null || returnPath.length == 0 ? null : returnPath[0]), sendType);
// Set the To, From
To = msg.getHeader("To", ",");
From = msg.getHeader("From", ",");
Subject = msg.getSubject();
// Is this catching emails
if ( catchEmails != null ){
redirectEmails( msg );
catchLog = "[CatchEmails:" + catchEmailList + "] ";
}else{
catchLog = "";
}
// Send the message
mailOut++;
mailSize += filename.length();
/*
* previously just used the static method Transport.send() to send
* messages that don't require authentication however we need to use
* the realSession as opposed to the dummySession associated with the
* MimeMessage
*/
if (sendType == SendType.SSL) {
transport = realSession.getTransport("smtps");
} else {
transport = realSession.getTransport("smtp");
}
if (auth) {
transport.connect(server, username, password);
} else {
transport.connect();
}
msg.saveChanges();
rateLimitSend();
transport.sendMessage(msg, msg.getAllRecipients());
LogFile.println("MAIL", catchLog + "MailOut: To=" + To + "; From=" + From + "; Subject=" + Subject + "; Server=" + servers[x] + "; Size=" + filename.length() + " bytes");
if (callbackCFC != null)
onMailSend(callbackCFC, appname, servers[x], customData, msg);
deleteFile = true;
return true;
} catch (Exception E) {
LogFile.println("MAIL", catchLog + "MailOutFail: To=" + To + "; From=" + From + "; Subject=" + Subject + "; Server=" + servers[x] + "; Size=" + filename.length() + " bytes:" + E);
if (callbackCFC != null)
onMailFailed(callbackCFC, appname, servers[x], E.getMessage(), new File(failedDirectory, origFilename), customData, msg);
} finally {
if (transport != null)
transport.close();
}
}// end for-loop
if (!gotAtLeastOneGoodServerAndPortCombo) {
String problemMsg = "Unable to send (" + filename.length() + " byte) message since both SMTP Server & SMTP Port are NOT specified.";
LogFile.println("MAIL", problemMsg);
throw new IllegalStateException(problemMsg);
}
} catch (IllegalStateException e) { // here is part of the fix for bug #2088
throw e;
} catch (Exception E) {
LogFile.println("MAIL", "MailOutFail: Failed to parse " + filename);
} finally {
try {
if (in != null)
in.close();
} catch (IOException ignoreE) {
}
try {
if (fileIn != null)
fileIn.close();
} catch (IOException ignoreE) {
}
if (deleteFile) {
if (!filename.delete())
LogFile.println("MAIL", "MailOutFail: Failed to delete " + filename);
} else {
// sending failed so move file to failed directory
File failedFilename = new File(failedDirectory, origFilename);
if ( !failedFilename.getParentFile().exists() )
failedFilename.getParentFile().mkdirs();
if (!filename.renameTo(failedFilename))
LogFile.println("MAIL", "MailOutFail: Failed to move " + filename + " to " + failedFilename);
}
}
return false;
}
/**
* This is for the main catch email feature to redirect emails to a given box
*
* @param msg
* @throws MessagingException
*/
private void redirectEmails(CustomMessage msg) throws MessagingException {
Address[] emails = msg.getRecipients( RecipientType.TO );
if ( emails != null && emails.length > 0 ){
StringBuilder sb = new StringBuilder();
for ( int x=0; x < emails.length; x++ )
sb.append( emails[x].toString() + ";" );
msg.setHeader("x-openbd-to", sb.toString() );
msg.setRecipients( RecipientType.TO, new Address[0] );
}
emails = msg.getRecipients( RecipientType.CC );
if ( emails != null && emails.length > 0 ){
StringBuilder sb = new StringBuilder();
for ( int x=0; x < emails.length; x++ )
sb.append( emails[x].toString() + ";" );
msg.setHeader("x-openbd-cc", sb.toString() );
msg.setRecipients( RecipientType.CC, new Address[0] );
}
emails = msg.getRecipients( RecipientType.BCC );
if ( emails != null && emails.length > 0 ){
StringBuilder sb = new StringBuilder();
for ( int x=0; x < emails.length; x++ )
sb.append( emails[x].toString() + ";" );
msg.setHeader("x-openbd-bcc", sb.toString() );
msg.setRecipients( RecipientType.BCC, new Address[0] );
}
// finally set the email to where we want it to go
msg.setRecipients( RecipientType.TO, catchEmails );
}
/*
* fileFilter
*
* This FilenameFilter returns true for all files that end with ".email".
*/
class fileFilter implements FilenameFilter {
public boolean accept(File dir, String name) {
return name.endsWith(".email");
}
}
public String getSmtpPort() {
return smtpPort;
}
public String getSmtpServer() {
return smtpServer;
}
public boolean isUseSSL() {
return useSSL;
}
public boolean isUseTLS() {
return useTLS;
}
private class CustomMessage extends MimeMessage {
public CustomMessage(Session session, InputStream is) throws MessagingException {
super(session, is);
}
protected void updateMessageID() throws MessagingException {
}
}
private void onMailFailed(String callbackCFC, String appname, String server, String exception, File filename, String customData, MimeMessage msg) {
onCallback(callbackCFC, "onmailfail", appname, server, exception, filename, customData, msg);
}
private void onMailSend(String callbackCFC, String appname, String server, String customData, MimeMessage msg) {
onCallback(callbackCFC, "onmailsend", appname, server, null, null, customData, msg);
}
private void onCallback(String cfcFilter, final String cfcFilterMethod, String appname, String server, String exception, File file, String customData, MimeMessage msg) {
final cfSession tmpSession = PluginManager.getPlugInManager().createBlankSession();
if (appname != null) {
cfApplicationData appData = cfAPPLICATION.getAppManager().getAppData(tmpSession, appname);
tmpSession.setQualifiedData(variableStore.APPLICATION_SCOPE, appData);
}
try {
final Address[] to = msg.getAllRecipients();
final cfArrayData array = cfArrayData.createArray(1);
for (int x = 0; x < to.length; x++)
array.addElement(new cfStringData(to[x].toString()));
// Create the CFC we want to call
final ObjectCFC cfc = PluginManager.getPlugInManager().createCFC(tmpSession, cfcFilter);
cfc.addArgument("to", array);
cfc.addArgument("from", msg.getHeader("From", ","));
cfc.addArgument("subject", msg.getSubject());
cfc.addArgument("messageid", msg.getMessageID());
cfc.addArgument("mailserver", server);
cfc.addArgument("customdata", customData);
if (exception != null)
cfc.addArgument("error", exception);
if (file != null)
cfc.addArgument("file", file.getAbsolutePath());
new Thread() {
public void run() {
try {
cfc.runMethod(tmpSession, cfcFilterMethod);
} catch (cfmRunTimeException rte) {
rte.handleException(tmpSession);
} catch (Exception e) {
cfEngine.log("OutgoingMailServer.onCallback.thread():" + e.getMessage());
} finally {
tmpSession.pageEnd();
tmpSession.close();
}
}
}.start();
} catch (Exception e) {
cfEngine.log("OutgoingMailServer.onCallback():" + e.getMessage());
}
}
public String getSpoolDirectory() {
return spoolDirectory.getAbsolutePath();
}
public String getUndeliveredDirectory() {
return failedDirectory.getAbsolutePath();
}
}