/*********************************************************************************
* The contents of this file are subject to the Common Public Attribution
* License Version 1.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.openemm.org/cpal1.html. The License is based on the Mozilla
* Public License Version 1.1 but Sections 14 and 15 have been added to cover
* use of software over a computer network and provide for limited attribution
* for the Original Developer. In addition, Exhibit A has been modified to be
* consistent with Exhibit B.
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
* the specific language governing rights and limitations under the License.
*
* The Original Code is OpenEMM.
* The Original Developer is the Initial Developer.
* The Initial Developer of the Original Code is AGNITAS AG. All portions of
* the code written by AGNITAS AG are Copyright (c) 2007 AGNITAS AG. All Rights
* Reserved.
*
* Contributor(s): AGNITAS AG.
********************************************************************************/
package org.agnitas.backend;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.dao.DataAccessException;
import org.agnitas.util.Const;
import org.agnitas.util.Blackdata;
import org.agnitas.util.Blacklist;
import org.agnitas.util.Log;
/** Central control class for generating mails
*/
public class MailgunImpl implements Mailgun {
/** Reference to configuration */
private Data data;
/** All content blocks */
private BlockCollection allBlocks = null;
/** All tags for this mailing */
private Hashtable <String, EMMTag>
tagNames = null;
/** Creator for all URLs */
private URLMaker urlMaker = null;
/** The blacklist information for this mailing */
public Blacklist blist = null;
/** Query for normal selection */
private String selectQuery = null;
/** Query for the world part selection */
private String wSelectQuery = null;
/** Constructor
* must be followed by initializeMailung ()
*/
public MailgunImpl () {
data = null;
}
/** Setter for data
* @param nData new data instance
*/
public void setData (Data nData) {
data = nData;
}
/** Allocate new data instance
* @param status_id the string version of the statusID to use
*/
protected void mkData (String status_id) throws Exception {
setData (new Data ("mailgun", status_id, "meta:xml/gz"));
}
/**
* Initialize internal data
* @param status_id the string version of the statusID to use
*/
public void initializeMailgun (String status_id) throws Exception {
data = null;
try {
mkData (status_id);
} catch (Exception e) {
done ();
throw new Exception ("Error reading ini file: " + e, e);
}
data.suspend ();
}
/** Setup a mailgun without starting generation
* @param opts options to control the setup beyond DB information
*/
public void prepareMailgun (Hashtable <String, Object> opts) throws Exception {
try {
doPrepare (opts);
} catch (Exception e) {
if (data != null) {
data.suspend ();
}
throw e;
}
}
/** Execute an already setup mailgun
* @param opts options to control the execution beyond DB information
*/
public synchronized void
executeMailgun (Hashtable <String, Object> opts) throws Exception {
try {
doExecute (opts);
} catch (Exception e) {
data.suspend ();
throw e;
}
}
/** retreive the current mailing id
* @return the mailing ID
*/
public long mailingID () {
return data.mailing_id;
}
/** Cleanup
*/
public void done () throws Exception {
if (data != null) {
try {
data.done ();
} catch (Exception e) {
data = null;
throw new Exception ("Failed in cleanup: " + e);
}
data = null;
}
}
/** Retreive blacklist from database
*/
public void retreiveBlacklist () throws Exception {
String query = "SELECT company_id, email FROM cust_ban_tbl WHERE company_id = 0 OR company_id = :companyID";
List <Map <String, Object>>
rq = data.dbase.query (query, "companyID", data.company_id);
for (int n = 0; n < rq.size (); ++n) {
Map <String, Object> row = rq.get (n);
int cid = data.dbase.asInt (row.get ("company_id"));
String email = data.dbase.asString (row.get ("email"));
if (email != null) {
blist.add (email, cid == 0);
}
}
}
protected Object mkBlacklist () {
return new Blacklist ();
}
/** Read in the global and local blacklist
*/
private void readBlacklist () throws Exception {
blist = (Blacklist) mkBlacklist ();
if (! data.isPreviewMailing ()) {
try {
retreiveBlacklist ();
} catch (Exception e) {
data.logging (Log.FATAL, "readblist", "Unable to get blacklist: " + e.toString ());
throw new Exception ("Unable to get blacklist: " + e.toString ());
}
data.logging (Log.INFO, "readblist", "Found " + blist.globalCount () + " entr" + Log.exty (blist.globalCount ()) + " in global blacklist, " +
blist.localCount () + " entr" + Log.exty (blist.localCount ()) + " in local blacklist");
}
}
/** Check the sample email receiver if they should receive
* the sample for this mailing
* @param inp the expression to validate
* @return null, if no mail should be sent, otherwise the email address
*/
private String validateSampleEmail (String inp) {
String email;
int n = inp.indexOf (':');
if (n != -1) {
String minsub;
minsub = inp.substring (0, n);
try {
long minsubscriber = Long.parseLong (minsub);
if (minsubscriber < data.totalSubscribers) {
email = inp.substring (n + 1);
} else {
email = null;
}
} catch (NumberFormatException e) {
email = null;
}
} else {
email = inp;
}
return email;
}
protected Object mkBlockCollection () throws Exception {
return new BlockCollection ();
}
protected Object mkURLMaker () throws Exception {
return new URLMaker (data);
}
/** Prepare the mailgun
* @param opts options to control the setup beyond DB information
*/
private void doPrepare (Hashtable <String, Object> opts) throws Exception {
data.resume ();
data.options (opts, 1);
data.logging (Log.DEBUG, "prepare", "Starting firing");
// create new Block collection and store in member var
allBlocks = (BlockCollection) mkBlockCollection ();
allBlocks.setupBlockCollection (data, data.previewInput);
data.logging (Log.DEBUG, "prepare", "Parse blocks");
// read all tag names contained in the blocks into Hashtable
// - read selectvalues and store in EMMTag associated with tag name in Hashtable
tagNames = allBlocks.parseBlocks();
data.setUsedFieldsInLayout (allBlocks.conditionFields, tagNames);
// add default tags to Hastable
try{
String[] preset = {
"agnMAILTYPE",
"agnONEPIXEL"
};
for (int n = 0; n < preset.length; ++n) {
boolean useit;
switch (n) {
default:
useit = true;
break;
case 1:
useit = data.onepixlog != Data.OPL_NONE;
break;
}
if (useit) {
String tn = "[" + preset[n] + "]";
if (! tagNames.containsKey (tn))
tagNames.put (tn, (EMMTag) allBlocks.mkEMMTag (tn, false));
}
}
} catch (Exception e){
data.suspend ();
throw new Exception("Error adding default tags: " + e);
}
allBlocks.replace_fixed_tags (tagNames);
// prepare special url string maker
urlMaker = (URLMaker) mkURLMaker ();
readBlacklist ();
data.suspend ();
}
/** Prepare collection of customers
* @return a hashset for already seen customers
*/
public HashSet <Long> prepareCollection () throws Exception {
return new HashSet <Long> ((int) data.totalReceivers + 1);
}
/** Get new instance for index collection
* @return new instance
*/
public Object mkIndices () {
return new Indices ();
}
public Object mkCustinfo () {
return new Custinfo ();
}
public Object mkMailWriterMeta (Object nData, Object allBlocks, Hashtable tagNames) throws Exception {
return new MailWriterMeta ((Data) nData, (BlockCollection) allBlocks, tagNames);
}
/** Return used mediatypes (currently only email)
* @param cid the customerID to get types for
* @return mediatypes
*/
public String getMediaTypes (long customerID) {
return "email";
}
/** Write final data to database
*/
public void finalizeMailingToDatabase (MailWriter mailer) throws Exception {
data.toMailtrack ();
}
public void skipSetup () throws Exception {
}
public void skipFinalize () {
}
/** if we should not send mail to this recipient
* @param customerID the customerID of the recipient
* @return true, if we should not send mail, false otherwise
*/
public boolean skipRecipient (long customerID) {
return false;
}
class Extractor implements ResultSetExtractor {
private Data data;
private Mailgun mg;
private EMMTag mailtypeTag;
private Blacklist blist;
private URLMaker urlMaker;
private MailWriter mailer;
private boolean needSamples;
private HashSet <Long> seen;
private boolean hasOverwriteData;
private boolean hasVirtualData;
private Vector <EMMTag> emailTags;
private int emailCount;
private ResultSetMetaData meta;
private int metacount;
private Column[] rmap;
private Indices indices;
private Custinfo cinfo;
public Extractor (Data nData, Mailgun nMg, EMMTag nMailtypeTag,
Blacklist nBlist, URLMaker nUrlMaker, MailWriter nMailer,
boolean nNeedSamples, HashSet <Long> nSeen,
boolean nHasOverwriteData, boolean nHasVirtualData,
Vector <EMMTag> nEmailTags, int nEmailCount) {
data = nData;
mg = nMg;
mailtypeTag = nMailtypeTag;
blist = nBlist;
urlMaker = nUrlMaker;
mailer = nMailer;
needSamples = nNeedSamples;
seen = nSeen;
hasOverwriteData = nHasOverwriteData;
hasVirtualData = nHasVirtualData;
emailTags = nEmailTags;
emailCount = nEmailCount;
meta = null;
metacount = 0;
rmap = null;
indices = (Indices) mg.mkIndices ();
cinfo = (Custinfo) mg.mkCustinfo ();
}
private void extractRecord (ResultSet rset) throws SQLException, DataAccessException {
if (meta == null) {
meta = rset.getMetaData ();
metacount = meta.getColumnCount ();
rmap = new Column[metacount];
cinfo.setup (data, urlMaker);
for (int n = 0; n < metacount; ++n) {
String cname = meta.getColumnName (n + 1);
int ctype = meta.getColumnType (n + 1);
if (Column.typeStr (ctype) != null) {
rmap[n] = new Column (cname, ctype);
cname = cname.toLowerCase ();
indices.checkIndex (cname, n);
} else
rmap[n] = null;
}
}
long cid = rset.getLong (1);
Long ocid = new Long (cid);
if (seen.contains (ocid))
return;
seen.add (ocid);
if (mg.skipRecipient (cid))
return;
if (hasVirtualData && (! data.useRecord (ocid)))
return;
String userType = rset.getString (2);
int offset = 1;
int count;
for (count = 0; count < 2; ++count) {
rmap[count].set (rset, count + offset);
}
// get values from this recordset
// store in Emmtag inside Hashtable
// the tags are in the correct order
//
for (EMMTag tmpTag : tagNames.values ()) {
if ((! tmpTag.globalValue) && (! tmpTag.fixedValue) && (! tmpTag.mutableValue) && (tmpTag.tagType == EMMTag.TAG_DBASE)) {
tmpTag.mTagValue = null;
if (rmap[count] != null) {
rmap[count].set (rset, count + offset);
if (! rmap[count].isNull ()) {
tmpTag.mTagValue = rmap[count].get ();
}
}
++count;
}
} // end for
for (int n = count; n < metacount; ++n)
if (rmap[n] != null)
rmap[n].set (rset, n + offset);
if (data.lusecount > 0) {
int m;
m = 0;
for (int n = 0; n < data.lcount; ++n)
if (data.columnUse (n))
data.columnSet (n, rset, count + offset + m++);
}
if (hasOverwriteData || hasVirtualData) {
for (EMMTag tmpTag : tagNames.values ()) {
if (hasOverwriteData && (tmpTag.tagType == EMMTag.TAG_INTERNAL) && (tmpTag.tagSpec == EMMTag.TI_DB)) {
tmpTag.dbOverwrite = data.overwriteData (ocid, tmpTag.mSelectString.toUpperCase ());
} else if (hasVirtualData && (tmpTag.tagType == EMMTag.TAG_INTERNAL) && (tmpTag.tagSpec == EMMTag.TI_DBV)) {
tmpTag.dbOverwrite = data.virtualData (ocid, tmpTag.mSelectString.toUpperCase ());
}
}
}
String mailtype = (mailtypeTag != null ? mailtypeTag.mTagValue : null);
int mtype;
if (data.isPreviewMailing ()) {
mtype = Const.Mailtype.HTML | Const.Mailtype.HTML_MOBILE;
mailtype = Integer.toString (mtype);
} else {
if (mailtype == null) {
data.logging (Log.WARNING, "mailgun", "Unset mailtype for customer_id " + cid + ", using default");
mtype = Const.Mailtype.HTML;
mailtype = Integer.toString (mtype);
} else {
mtype = Integer.parseInt (mailtype);
if (mtype == 2) {
mtype = Const.Mailtype.HTML | Const.Mailtype.HTML_OFFLINE;
}
mtype &= data.masterMailtype;
}
}
cinfo.clear ();
cinfo.setCustomerID (cid);
cinfo.setUserType (userType);
cinfo.setFromDatabase (rmap, indices);
for (int n = 0; n < emailCount; ++n) {
EMMTag etag = emailTags.elementAt (n);
etag.mTagValue = cinfo.email;
}
if (! data.isPreviewMailing ()) {
boolean isblisted = false;
for (int blstate = 0; blstate < cinfo.checkForBlacklist; ++blstate) {
String check = cinfo.blacklistValue (blstate);
String what = cinfo.blacklistName (blstate);
if (check == null)
continue;
Blackdata bl = (Blackdata) blist.isBlackListed (check);
if (bl != null) {
String where;
data.logging (Log.WARNING, "mailgun", "Found " + what + ": " + check + " (" + cid + ") in " + bl.where () + " blacklist, ignored");
blist.writeBounce (data.mailing_id, cid);
isblisted = true;
}
}
if (isblisted)
return;
}
String mediatypes = getMediaTypes (cid);
if (mediatypes == null)
return;
urlMaker.setCustomerID (cid);
try {
mailer.writeMail (cinfo, 0, mtype, cid, mediatypes, tagNames, urlMaker);
} catch (Exception e) {
data.logging (Log.ERROR, "mailgun", "Failed to write mail: " + e.toString ());
}
if (needSamples) {
urlMaker.setCustomerID (0);
Vector v = StringOps.splitString (data.sampleEmails ());
for (int mcount = 0; mcount < v.size (); ++mcount) {
String email = validateSampleEmail ((String) v.elementAt (mcount));
if ((email != null) && (email.length () > 3) && (email.indexOf ('@') != -1)) {
cinfo.setEmail (email);
for (int n = 0; n < emailCount; ++n) {
EMMTag etag = emailTags.elementAt (n);
etag.mTagValue = email;
}
for (int n = 0; n < Const.Mailtype.MAX; ++n) {
if ((n == 0) || ((n & data.masterMailtype) != 0)) {
mailtypeTag.mTagValue = Integer.toString (n);
try {
mailer.writeMail (cinfo, mcount + 1, n, 0, "email", tagNames, urlMaker);
} catch (Exception e) {
data.logging (Log.ERROR, "mailgun", "Failed to write sample mail: " + e.toString ());
}
}
}
}
}
needSamples = false;
}
}
@Override
public Object extractData (ResultSet rset) throws SQLException, DataAccessException {
while (rset.next ()) {
extractRecord (rset);
}
return null;
}
}
/** Execute a prepared mailgun
* @param opts options to control the execution beyond DB information
*/
private void doExecute (Hashtable <String, Object> opts) throws Exception {
data.resume ();
data.options (opts, 2);
data.sanityCheck ();
// get constructed selectvalue based on tag names in Hashtable
data.startExecution ();
selectQuery = getSelectvalue (tagNames, false);
wSelectQuery = getSelectvalue (tagNames, true);
MailWriter mailer = (MailWriter) mkMailWriterMeta (data, allBlocks, tagNames);
int columnCount = 0;
Vector <EMMTag>
email_tags = new Vector <EMMTag> ();
int email_count = 0;
boolean hasOverwriteData = false;
boolean hasVirtualData = false;
for (Enumeration e = tagNames.elements (); e.hasMoreElements (); ) {
EMMTag tag = (EMMTag) e.nextElement ();
if ((! tag.globalValue) && (! tag.fixedValue) && (! tag.mutableValue)) {
if (tag.tagType == EMMTag.TAG_DBASE) {
++columnCount;
} else if ((tag.tagType == EMMTag.TAG_INTERNAL) && (tag.tagSpec == EMMTag.TI_EMAIL)) {
email_tags.add (tag);
email_count++;
} else if ((tag.tagType == EMMTag.TAG_INTERNAL) && (tag.tagSpec == EMMTag.TI_DBV)) {
hasVirtualData = true;
data.initializeVirtualData (tag.mSelectString);
} else if (tag.tagType == EMMTag.TAG_CUSTOM) {
if ((data.customMap != null) && data.customMap.containsKey (tag.mTagFullname))
tag.mTagValue = data.customMap.get (tag.mTagFullname);
else
tag.mTagValue = null;
}
}
}
if (data.lusecount > 0)
columnCount += data.lusecount;
hasOverwriteData = data.overwriteData ();
skipSetup ();
try {
data.logging (Log.INFO, "execute", "Start creation of mails");
boolean needSamples = data.isWorldMailing () && (data.sampleEmails () != null) && ((data.availableMedias & (1 << Media.TYPE_EMAIL)) != 0);
Vector clist = data.generationClauses ();
HashSet <Long> seen = prepareCollection ();
Extractor ex;
data.prefillRecipients (seen);
ex = new Extractor (data, this, tagNames.get ("[agnMAILTYPE]"),
blist, urlMaker, mailer,
needSamples, seen,
hasOverwriteData, hasVirtualData,
email_tags, email_count);
for (int state = 0; state < clist.size (); ++state) {
String clause = (String) clist.get (state);
if (clause == null)
continue;
String query = (state == 0 ? selectQuery : wSelectQuery) + " " + clause;
if ((state == 1) && (seen.size () > 0)) {
data.increaseStartblockForStepping ();
}
if ((mailer.blockSize > 0) && (mailer.inBlockCount > 0))
mailer.checkBlock (true);
data.dbase.op (query).query (query, ex);
}
} catch (SQLException e){
data.updateGenerationState (4);
data.suspend ();
throw new Exception("Error during main query or mail generation:" + e);
}
skipFinalize ();
mailer.done ();
if (! data.isPreviewMailing ()) {
finalizeMailingToDatabase (mailer);
}
data.endExecution ();
data.updateGenerationState ();
data.logging (Log.DEBUG, "execute", "Successful end");
data.suspend ();
}
public Object mkDestroyer (int mailingId) throws Exception {
return new Destroyer (mailingId);
}
/** Full execution of a mail generation
* @param custid optional customer id
* @return Status string
*/
public String fire (String custid) throws Exception {
String str;
str = null;
try {
data.logging (Log.INFO, "mailgun", "Starting up");
Hashtable <String, Object> opts = new Hashtable <String, Object> ();
if (custid != null)
opts.put ("customer-id", custid);
doPrepare (opts);
doExecute (opts);
str = "Success: Mailgun fired.";
} catch (Exception e) {
data.logging (Log.ERROR, "mailgun", "Creation failed: " + e.toString ());
if ((data != null) && (data.mailing_id > 0)) {
Destroyer d = (Destroyer) mkDestroyer ((int) data.mailing_id);
data.logging (Log.INFO, "mailgun", "Try to remove failed mailing: " + e);
str = d.destroy ();
d.done ();
}
try {
done ();
} catch (Exception temp) {
data.logging (Log.ERROR, "mailgun", "Failed to finalize mailgun (after failure): " + temp.toString ());
}
throw e;
}
data.logging (Log.INFO, "mailgun", "Execution done: " + str);
try {
done ();
} catch (Exception e) {
data.logging (Log.ERROR, "mailgun", "Failed to finalize mailgun: " + e.toString ());
}
return str;
}
/** Optional add database hint
* @return the hint
*/
public String getHint () {
return "";
}
/** Build the complete big query
* @param tagNames the tags
* @param allBlocks all content information
* @return the created query
*/
public String getSelectvalue (Hashtable tagNames, boolean hint) throws Exception {
StringBuffer select_string = new StringBuffer();
select_string.append("SELECT ");
if (hint) {
String hstr = getHint ();
if (hstr.length () > 0) {
select_string.append (hstr);
}
}
if (tagNames != null) {
EMMTag current_tag=null;
// append all select string values of all tags
select_string.append ("cust.customer_id, bind.user_type");
for ( Enumeration e = tagNames.elements(); e.hasMoreElements(); ) {
current_tag = (EMMTag) e.nextElement(); // new
if ((! current_tag.globalValue) && (! current_tag.fixedValue) && (! current_tag.mutableValue) && (current_tag.tagType == EMMTag.TAG_DBASE)) {
select_string.append(", " + current_tag.mSelectString);
}
}
if (data.lusecount > 0)
for (int n = 0; n < data.lcount; ++n) {
Column c = data.columnByIndex (n);
if (c.inuse) {
select_string.append (", ");
select_string.append (c.ref == null ? "cust" : c.ref);
select_string.append (".");
select_string.append (c.name);
}
}
// remove last comma
// select_string.deleteCharAt(select_string.length() - 1);
} else
select_string.append ("count(distinct customer_id)");
// turn stringbuffer into string
String result = select_string.toString();
data.logging (Log.DEBUG, "selectvalue", "SQL-String: " + result);
return result;
}
}