/*********************************************************************************
* 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.util.Arrays;
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.agnitas.util.Const;
import org.agnitas.util.Log;
/**
* Holds all Blocks of a Mailing
*/
public class BlockCollection {
/**
* Reference to configuration
*/
private Data data;
/**
* All blocks in the mailing
*/
public BlockData blocks[];
/**
* Total number of text blocks
*/
protected int totalText = 0;
/**
* Total number of all blocks
*/
protected int totalNumber = 0;
/**
* All dynamic blocks
*/
public DynCollection dynContent;
/**
* Collection of all found dynamic names in blocks
*/
public Vector <String> dynNames;
/**
* Number of all names in dynNames
*/
public int dynCount;
/**
* if we have any attachments
*/
public boolean hasAttachment = false;
/**
* total amount of attachments
*/
public int numberOfAttachments = 0;
/**
* referenced database fields in conditions
*/
public HashSet <String> conditionFields;
public Object mkDynCollection (Object nData) {
return new DynCollection ((Data) nData);
}
public Object mkEMMTag (String tag, boolean isCustom) throws Exception {
EMMTag tg = new EMMTag (data, tag, isCustom);
tg.initialize (data, false);
return tg;
}
/**
* Constructor for this class
*/
public void setupBlockCollection (Object nData, String customText) throws Exception {
data = (Data) nData;
if (customText == null) {
totalText = 0;
totalNumber = 0;
blocks = null;
readBlockdata ();
dynContent = (DynCollection) mkDynCollection (data);
dynContent.collectParts ();
dynNames = new Vector <String> ();
dynCount = 0;
conditionFields = new HashSet <String> ();
} else {
BlockData block = (BlockData) mkBlockData ();
block.id = 0;
block.content = customText;
block.cid = Const.Component.NAME_TEXT;
block.is_parseable = true;
block.is_text = true;
block.type = BlockData.TEXT;
block.media = Media.TYPE_EMAIL;
block.comptype = 0;
totalText = 1;
totalNumber = 1;
blocks = new BlockData[1];
blocks[0] = block;
}
}
/**
* Add a string to the receiver `To:' line in the mailing
* to mark an admin- or testmailing
*
* @return the optional string
*/
public String addTo () {
if (data.isAdminMailing ()) {
return "\"Adminmail\" ";
} else if (data.isTestMailing ()) {
return "\"Testmail\" ";
}
return "";
}
/**
* Add a string to the receiver `Subject:' line
*
* @return the optional string
*/
public String addSubject () {
return "";
}
public Object mkBlockData () {
return new BlockData ();
}
/** Returns the envelope sender address
* @return the address
*/
public String envelopeFrom () {
return data.getEnvelopeFrom ();
}
public String returnPath () {
return null;
}
/** Adds the `From' line to header
* @return the from line
*/
public String headFrom () {
if (data.fromEmail.full != data.fromEmail.pure) {
return "HFrom: " + data.fromEmail.full_puny + data.eol;
} else {
return "HFrom: <" + data.fromEmail.full_puny + ">" + data.eol;
}
}
/** Adds the 'Reply-To' line to head
* @return the reply-to line
*/
public String headReplyTo () {
if ((data.replyTo != null) && data.replyTo.valid (true) && (! data.fromEmail.full.equals (data.replyTo.full))) {
return "HReply-To: " + data.replyTo.full_puny + data.eol;
}
return "";
}
public String headAdditional () {
return "";
}
/**
* Creates the first block holding the header information.
*
* @return the newly created block
*/
public BlockData createBlockZero () {
BlockData b = (BlockData) mkBlockData ();
String head;
if ((data.fromEmail != null) &&
data.fromEmail.valid (false) &&
(data.subject != null)) {
String mfrom = envelopeFrom ();
String rpath = returnPath ();
if (mfrom == null) {
mfrom = "";
}
if (rpath == null) {
rpath = mfrom;
}
head = "T[agnSYSINFO name=\"EPOCH\"]" + data.eol +
"S<" + mfrom + ">" + data.eol +
"R<[agnEMAIL code=\"punycode\"]>" + data.eol +
"H?mP?Return-Path: <" + rpath +">" + data.eol +
"HReceived: by [agnSYSINFO name=\"FQDN\" default=\"" + data.domain + "\"] for <[agnEMAIL]>; [agnSYSINFO name=\"RFCDATE\"]" + data.eol +
"HMessage-ID: <" + EMMTag.internalTag (EMMTag.TI_MESSAGEID) + ">" + data.eol +
"HDate: [agnSYSINFO name=\"RFCDATE\"]" + data.eol;
head += headFrom ();
head += headReplyTo ();
head += "HTo: " + addTo () + "<" + "[agnEMAIL code=\"punycode\"]" + ">" + data.eol +
"HSubject: " + addSubject () + data.subject + data.eol;
head += headAdditional ();
head += "HX-Mailer: " + data.makeMailer () + data.eol +
"HMIME-Version: 1.0" + data.eol;
} else {
head = "- unset -" + data.eol;
}
b.content = head;
b.cid = Const.Component.NAME_HEADER;
b.is_parseable = true;
b.is_text = true;
b.type = BlockData.HEADER;
b.media = Media.TYPE_EMAIL;
b.comptype = 0;
return b;
}
/**
* Retreives the blockdata from a SQL record
*
* @return the filled blockdata
*/
public Object retreiveBlockdata (Map <String, Object> row) {
BlockData tmp;
int comptype;
long urlid;
String compname;
String mtype;
int target_id;
String emmblock;
byte[] binary;
comptype = data.dbase.asInt (row.get ("comptype"));
urlid = data.dbase.asLong (row.get ("url_id"));
compname = data.dbase.asString (row.get ("compname"));
mtype = data.dbase.asString (row.get ("mtype"));
target_id = data.dbase.asInt (row.get ("target_id"));
emmblock = data.dbase.asClob (row.get ("emmblock"));
binary = data.dbase.asBlob (row.get ("binblock"));
tmp = (BlockData) mkBlockData ();
tmp.media = Media.TYPE_UNRELATED;
if (comptype == 0) {
if (compname.equals (Const.Component.NAME_TEXT)) {
tmp.type = BlockData.TEXT;
tmp.media = Media.TYPE_EMAIL;
} else if (compname.equals (Const.Component.NAME_HTML)) {
tmp.type = BlockData.HTML;
tmp.media = Media.TYPE_EMAIL;
} else {
data.logging (Log.WARNING, "collect", "Invalid compname " + compname + " for comptype 0 found");
return null;
}
tmp.is_parseable = true;
tmp.is_text = true;
} else if (comptype == 1) {
tmp.type = BlockData.RELATED_BINARY;
} else if (comptype == 3) {
tmp.type = BlockData.ATTACHMENT_BINARY;
tmp.is_attachment = true;
} else if (comptype == 4) {
tmp.type = BlockData.ATTACHMENT_TEXT;
tmp.is_parseable = true;
tmp.is_text = true;
tmp.is_attachment = true;
} else if (comptype == 5) {
tmp.type = BlockData.RELATED_BINARY;
} else {
data.logging (Log.WARNING, "collect", "Invalid comptype " + comptype + " found");
return null;
}
tmp.comptype = comptype;
tmp.urlID = urlid;
tmp.cid = compname;
tmp.mime = mtype;
tmp.targetID = target_id;
// write to different String, depending on text/binary data
if (emmblock != null) {
if (tmp.is_parseable) {
tmp.content = StringOps.convertOld2New (emmblock);
} else {
tmp.content = emmblock;
}
}
tmp.binary = binary;
if (tmp.binary != null) {
if ((! tmp.is_parseable) && (tmp.content != null) && (tmp.content.length () == 0)) {
tmp.content = null;
}
}
return tmp;
}
/**
* Retreive optional related data for newly created block
*
* @return the optional block
*/
public Object retreiveRelatedBlockdata (Object obd) {
return null;
}
/**
* Return mailing_id related part of the where clause to
* retreive the components
*
* @return the clause part
*/
public String mailingClause () {
return "mailing_id = " + data.mailing_id;
}
public String reduceClause () {
if (data.isPreviewMailing ())
return "AND comptype IN (0, 4) ";
return "";
}
public String componentFields () {
return "comptype, url_id, compname, mtype, target_id, emmblock, binblock";
}
public void cleanupBlockCollection(Vector<BlockData> c) {
}
/**
* Reads the blocks used by this mailing from the database
*/
public void readBlockdata () throws Exception {
String query;
query = "SELECT " + componentFields () + " " +
"FROM component_tbl " +
"WHERE company_id = " + data.company_id + " AND (" + mailingClause () + ") " + reduceClause () +
"ORDER BY component_id";
try {
Vector <BlockData> collect;
List <Map <String, Object>> rq;
int n;
collect = new Vector <BlockData> ();
collect.addElement (createBlockZero ());
totalNumber = 1;
rq = data.dbase.query (query);
for (n = 0; n < rq.size (); ++n) {
Map <String, Object> row = rq.get (n);
BlockData tmp;
tmp = (BlockData) retreiveBlockdata (row);
if (tmp == null) {
continue;
}
if ((tmp.type == BlockData.ATTACHMENT_BINARY) || (tmp.type == BlockData.ATTACHMENT_TEXT)) {
hasAttachment = true;
++numberOfAttachments;
}
collect.addElement (tmp);
++totalNumber;
tmp = (BlockData) retreiveRelatedBlockdata (tmp);
if (tmp != null) {
collect.addElement (tmp);
++totalNumber;
}
}
cleanupBlockCollection (collect);
totalText = 0;
totalNumber = collect.size ();
blocks = collect.toArray (new BlockData[totalNumber]);
for (n = 0; n < totalNumber; ++n) {
BlockData b = blocks[n];
if (b.isEmailText ()) {
++totalText;
}
data.logging (Log.DEBUG, "collect",
"Block " + n + " (" + totalNumber + "): " + b.cid + " [" + b.mime + "]");
}
Arrays.sort ((Object[]) blocks);
for (n = 0; n < totalNumber; ++n) {
BlockData b = blocks[n];
b.id = n;
if (b.targetID != 0) {
Target tgt = data.getTarget (b.targetID);
if ((tgt != null) && tgt.valid ()) {
b.condition = tgt.sql;
}
}
data.logging (Log.DEBUG, "collect",
"Block " + n + " (" + totalNumber + "): " + b.cid + " [" + b.mime + "] " + b.targetID + (b.condition != null ? " SQL: " + b.condition : ""));
}
} catch (Exception e) {
throw new Exception ("Unable to read block: " + e, e);
}
}
/**
* returns the block at the given position
*
* @param pos the index into the block array
* @return the block at the requested position
*/
public BlockData getBlock (int pos) {
return blocks[pos];
}
/**
* Parses a block, collecting all tags in a hashtable
*
* @param cb the block to parse
* @param tag_table the hashtable to collect tag
*/
public void parseBlock (BlockData cb, Hashtable <String, EMMTag> tag_table) throws Exception {
if (cb.is_parseable) {
int tag_counter = 0;
String current_tag;
// get all tags inside the block
while( (current_tag = cb.get_next_tag() ) != null){
try{
// add tag and EMMTag data structure to hashtable
if (! tag_table.containsKey (current_tag)) {
EMMTag ntag = (EMMTag) mkEMMTag (current_tag, false);
String dyName;
if ((ntag.tagType == EMMTag.TAG_INTERNAL) &&
(ntag.tagSpec == EMMTag.TI_DYN) &&
((dyName = ntag.mTagParameters.get ("name")) != null)) {
int n;
for (n = 0; n < dynCount; ++n) {
if (dyName.equals (dynNames.elementAt (n))) {
break;
}
}
if (n == dynCount) {
dynNames.addElement (dyName);
dynCount++;
}
}
tag_table.put(current_tag, ntag);
data.logging (Log.DEBUG, "collect", "Added Tag: " + current_tag);
} else
data.logging (Log.DEBUG, "collect", "Skip existing Tag: " + current_tag);
} catch (Exception e) {
throw new Exception (
"Error while trying to query block " + tag_counter + " :" +e);
}
tag_counter++;
}
// check for tagless blocks
if ( tag_counter == 0 ) {
cb.is_parseable = false; // block contained no tags!
}
}
}
/**
* Substidute parts of a filename using some pattern
*
* @return the replacement string
*/
public String substituteParameter (String mod, String parm, String dflt) {
return dflt;
}
private String parseSubstitution (String src) {
int cur = 0;
int start, end;
StringBuffer res = null;
while ((start = src.indexOf ("%[", cur)) != -1) {
end = src.indexOf ("]%", start);
if (end == -1) {
break;
}
if (res == null)
res = new StringBuffer (src.length ());
res.append (src.substring (cur, start));
String cont = src.substring (start + 2, end);
int parmoffset = cont.indexOf (':');
String mod, parm;
if (parmoffset == -1) {
mod = cont;
parm = null;
} else {
mod = cont.substring (0, parmoffset);
++parmoffset;
while ((parmoffset < cont.length ()) && Character.isWhitespace (cont.charAt (parmoffset))) {
++parmoffset;
}
parm = cont.substring (parmoffset);
}
res.append (substituteParameter (mod, parm, src.substring (start, end + 2)));
cur = end + 2;
}
if ((res != null) && (cur < src.length ())) {
res.append (src.substring (cur));
}
return res == null ? null: res.toString ();
}
/**
* Parses all blocks returning a hashtable with all found
* tags
*
* @return the hashtable with all tags
*/
public Hashtable <String, EMMTag> parseBlocks() throws Exception {
Hashtable <String, EMMTag> tag_table = new Hashtable <String, EMMTag> ();
// first add all custom tags
if (data.customTags != null) {
for (int n = 0; n < data.customTags.size (); ++n) {
String tname = data.customTags.get (n);
if (! tag_table.containsKey (tname)) {
EMMTag ntag = (EMMTag) mkEMMTag (tname, true);
tag_table.put (tname, ntag);
}
}
}
// go through all blocks
for (int count = 0; count < this.totalNumber; count++) {
data.logging (Log.DEBUG, "collect", "Parsing block " + count);
parseBlock (blocks[count], tag_table);
}
if (dynContent != null) {
for (Enumeration<DynName> e = dynContent.names.elements (); e.hasMoreElements (); ) {
DynName tmp = e.nextElement ();
String cname = tmp.getAssignedColumn ();
if (cname != null) {
conditionFields.add (cname);
}
for (int n = 0; n < tmp.clen; ++n) {
DynCont cont = tmp.content.elementAt (n);
if (cont.text != null) {
parseBlock (cont.text, tag_table);
}
if (cont.html != null) {
parseBlock (cont.html, tag_table);
}
}
}
}
for (int count = 0; count < totalNumber; ++count) {
BlockData b = blocks[count];
switch (b.comptype) {
case 3:
case 4:
case 7:
b.cidEmit = parseSubstitution (b.cid);
b.mimeEmit = parseSubstitution (b.mime);
break;
case 5:
boolean match = false;
for (Enumeration <EMMTag> e = tag_table.elements (); e.hasMoreElements (); ) {
EMMTag tag = e.nextElement ();
if ((tag.tagType == EMMTag.TAG_DBASE) && (tag.tagSpec == EMMTag.TDB_IMAGE)) {
String name = tag.mTagParameters.get ("name");
if ((name != null) && name.equals (b.cid)) {
b.cidEmit = tag.mTagValue;
match = true;
}
} else if ((tag.tagType == EMMTag.TAG_INTERNAL) && (tag.tagSpec == EMMTag.TI_IMGLINK)) {
String name = tag.mTagParameters.get ("name");
if ((name != null) && name.equals (b.cid)) {
tag.imageLinkReference (data, b.urlID);
b.cidEmit = tag.ilURL;
match = true;
}
}
}
if (! match) {
if (b.mime.startsWith ("image/")) {
b.cidEmit = data.defaultImageLink (b.cid);
}
}
break;
}
}
return tag_table;
}
/**
* create the corresponding url_string for the tags:
* 1 - Profile
* 2 - Unsubscribe
* 3 - AutoURL
* 4 - Onepixellog
*
* @param tag the tag itself
* @param urlMaker an instance of TagString to create the URLs
* @return the newly created URL
*/
protected String create_url_tag (EMMTag tag, URLMaker urlMaker) throws Exception {
switch (tag.tagSpec) {
case 1: return urlMaker.profileURL ();
case 2: return urlMaker.unsubscribeURL ();
case 3:
long urlid = Long.parseLong (tag.mTagParameters.get ("url"));
if (urlid <= 0) {
data.logging (Log.FATAL, "collect", "Invalid Autourl parameter or parameter not found");
throw new Exception ("Failed due to missing/wrong URL parameter in auto url");
}
return urlMaker.autoURL (urlid);
case 4: return urlMaker.onepixelURL ();
default:
throw new Exception ("Unknown tagSpec " + tag.tagSpec);
}
}
/**
* Already parse and replace tags with fixed value
*
* @param b the block to parse
* @param tagTable the tag collection
*/
public void parse_fixed_block (BlockData b, Hashtable <String, EMMTag> tagTable) {
String cont = b.content != null ? b.content : "";
int clen = cont.length ();
StringBuffer buf = new StringBuffer (clen + 128);
Vector <TagPos> pos = b.tag_position;
int count = pos.size ();
int start = 0;
int offset = 0;
boolean changed = false;
for (int m = 0; m < count; ) {
TagPos tp = pos.get (m);
EMMTag tag = tagTable.get (tp.tagname);
String value = tag.mTagValue;
if ((value == null) && tag.fixedValue) {
tag.fixedValue = false;
}
if (tag.fixedValue) {
offset += value.length () - tag.mTagFullname.length ();
buf.append (cont.substring (start, tp.start) + value);
start = tp.end + 1;
pos.removeElementAt (m);
--count;
changed = true;
} else {
if ((tp.content != null) && tp.content.is_parseable)
parse_fixed_block (tp.content, tagTable);
tp.start += offset;
tp.end += offset;
++m;
}
}
if (changed) {
if (start < clen) {
buf.append (cont.substring (start));
}
b.content = StringOps.convertOld2New (buf.toString ());
if (count == 0) {
b.is_parseable = false;
}
}
}
/**
* Parse and replace all tags with fixed value
*
* @param tagTable the collection of all tags
*/
public void replace_fixed_tags (Hashtable <String, EMMTag> tagTable) {
for (int n = 0; n < totalNumber; ++n) {
if (blocks[n].is_parseable) {
parse_fixed_block (blocks[n], tagTable);
}
}
if (dynContent != null) {
for (Enumeration<DynName> e = dynContent.names.elements (); e.hasMoreElements (); ) {
DynName tmp = e.nextElement ();
for (int n = 0; n < tmp.clen; ++n) {
DynCont cont = tmp.content.elementAt (n);
if (cont.text != null) {
parse_fixed_block (cont.text, tagTable);
}
if (cont.html != null) {
parse_fixed_block (cont.html, tagTable);
}
}
}
}
}
}