diff --git a/entity/AttributeRelation_entity/AttributeRelation_entity.aod b/entity/AttributeRelation_entity/AttributeRelation_entity.aod
index 5d5c0da96881f87673fcd1d78115da11cc83ce40..69ec8245a8929cad2f06dc06a282f5488290e368 100644
--- a/entity/AttributeRelation_entity/AttributeRelation_entity.aod
+++ b/entity/AttributeRelation_entity/AttributeRelation_entity.aod
@@ -2,7 +2,11 @@
 <entity xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.3.10" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/entity/1.3.10">
   <name>AttributeRelation_entity</name>
   <majorModelMode>DISTRIBUTED</majorModelMode>
-  <title>Attribute</title>
+  <titlePlural>Attributes</ti<grantDeleteProcess>%aditoprj%/entity/AttributeRelation_entity/grantDeleteProcess.js</grantDeleteProcess>
+  tlePlural>
+  <recordContainer>jdito</recordContainer>
+  <entityFields>
+    <entityProvider>
   <titlePlural>Attributes</titlePlural>
   <recordContainer>jdito</recordContainer>
   <entityFields>
diff --git a/entity/AttributeRelation_entity/grantDeleteProcess.js b/entity/AttributeRelation_entity/grantDeleteProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..e407961574cd4f10e420c5b3615f955fa5db8731
--- /dev/null
+++ b/entity/AttributeRelation_entity/grantDeleteProcess.js
@@ -0,0 +1,8 @@
+import("Attribute_lib");
+import("system.result");
+import("system.vars");
+
+var objectType = vars.exists("$param.ObjectType_param") && vars.get("$param.ObjectType_param");
+var rowId = vars.exists("$param.ObjectRowId_param") && vars.get("$param.ObjectRowId_param");
+if (vars.get("$param.GetTree_param") == "true" && rowId)
+    result.object(AttributeRelationUtils.countAttributeRelations(rowId, objectType));
\ No newline at end of file
diff --git a/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js b/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js
index 249ba8308b6fa9ba2b0f912b649a0c43637f3d4a..54e00eb0a624331ee8d633d904a81cfcf5acdb1a 100644
--- a/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js
@@ -1,198 +1,198 @@
-import("system.translate");
-import("Util_lib");
-import("JditoFilter_lib");
-import("KeywordRegistry_basic");
-import("Keyword_lib");
-import("system.db");
-import("system.vars");
-import("system.result");
-import("Sql_lib");
-import("Attribute_lib");
-
-var getGroups = vars.exists("$param.GetGroups_param") && vars.get("$param.GetGroups_param");
-var objectType = vars.exists("$param.ObjectType_param") && vars.get("$param.ObjectType_param");
-var parentType = vars.exists("$param.AttrParentType_param") && vars.get("$param.AttrParentType_param");
-var fetchUsages = true;
-var translateName = false;
-
-var uidTableAlias = "UIDROW";
-var sqlSelect = "select UIDROW.AB_ATTRIBUTEID, UIDROW.ATTRIBUTE_PARENT_ID, UIDROW.ATTRIBUTE_ACTIVE, UIDROW.DROPDOWNDEFINITION, UIDROW.SORTING, UIDROW.ATTRIBUTE_TYPE, " 
-    + KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.attributeType(), "UIDROW.ATTRIBUTE_TYPE") //3
-    + ", '', UIDROW.ATTRIBUTE_NAME, PARENT1.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_NAME, PARENT3.ATTRIBUTE_NAME, PARENT3.ATTRIBUTE_PARENT_ID \n\
-    from AB_ATTRIBUTE UIDROW \n\
-    left join AB_ATTRIBUTE PARENT1 on UIDROW.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID \n\
-    left join AB_ATTRIBUTE PARENT2 ON PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID \n\
-    left join AB_ATTRIBUTE PARENT3 ON PARENT2.ATTRIBUTE_PARENT_ID = PARENT3.AB_ATTRIBUTEID"; 
-
-/* always select the names of the next 3 parents so that less queries
-   are required later when buildung the full name */
-
-var sqlOrder = " order by UIDROW.ATTRIBUTE_PARENT_ID asc, UIDROW.SORTING asc";
-
-var condition = SqlCondition.begin();
-
-if (vars.exists("$local.idvalues") && vars.get("$local.idvalues"))
-{
-    condition.and("UIDROW.AB_ATTRIBUTEID in ('" + vars.get("$local.idvalues").join("','") + "')");
-}
-else if (getGroups) //if getGroups == true, it is the lookup for selecting the superordinate attribute
-{
-    //this condition filters out the own id and the children to prevent loops
-    
-    var isGroupCondition = SqlCondition.begin();
-    for (let type in $AttributeTypes)
-        if ($AttributeTypes[type].isGroup)
-        {
-            isGroupCondition.orPrepare(["AB_ATTRIBUTE", "ATTRIBUTE_TYPE", uidTableAlias], $AttributeTypes[type]);
-        }
-    
-    condition.andSqlCondition(SqlCondition.begin()
-        .andSqlCondition(isGroupCondition)
-        .andPrepareVars(["AB_ATTRIBUTE", "AB_ATTRIBUTEID", uidTableAlias], "$param.AttrParentId_param", "# != ?")
-        .and("UIDROW.AB_ATTRIBUTEID not in ('" + AttributeUtil.getAllChildren(vars.getString("$param.AttrParentId_param")).join("','") + "')")
-    );
-}
-else if (objectType)  //if there's an objectType, it comes from the AttributeRelation entity (lookup for the attribute selection)
-{
-    var filteredAttributes = null;
-    
-    if (vars.exists("$param.FilteredAttributeIds_param") && vars.getString("$param.FilteredAttributeIds_param")) {
-        filteredAttributes = JSON.parse(vars.getString("$param.FilteredAttributeIds_param"));
-    }
-    
-    var attributeCount;
-    if (vars.exists("$param.AttributeCount_param") && vars.get("$param.AttributeCount_param"))
-        attributeCount = JSON.parse(vars.getString("$param.AttributeCount_param"));
-    var ids = AttributeUtil.getPossibleAttributes(objectType, false, filteredAttributes, attributeCount);
-
-    if (ids.length > 0)
-        condition.and("UIDROW.AB_ATTRIBUTEID in ('" + ids.join("','") + "')");
-    else  // do not return anything, if parameter is there but an empty array
-        condition.and("1=2");
-
-     fetchUsages = false;
-     translateName = true;
-}
-else if (parentType) //condition for all subordinate attributes of an attribute (for the tree of subordinate attributes in an attribute)
-{
-    if (AttributeTypeUtil.isGroupType(parentType))
-    {
-        var parentId = vars.exists("$param.AttrParentId_param") && vars.get("$param.AttrParentId_param");
-        if (parentId)
-            condition.and("UIDROW.AB_ATTRIBUTEID in ('" + AttributeUtil.getAllChildren(vars.getString("$param.AttrParentId_param")).join("','") + "')");
-    }
-    else
-        condition.and("1=2");
-    
-    translateName = true;
-}
-
-//when there are filters selected, add them to the conditon
-if (vars.exists("$local.filter") && vars.get("$local.filter"))
-{
-    var filter = vars.get("$local.filter");
-    if (filter.filter)
-        condition.andSqlCondition(JditoFilterUtils.getSqlCondition(filter.filter, "AB_ATTRIBUTE", uidTableAlias));
-}
-
-var usages;
-if (fetchUsages) //this query is only necessary in Attribute, not in AttributeRelation
-{
-    var usagesSelect = "select AB_ATTRIBUTE_ID, OBJECT_TYPE from AB_ATTRIBUTEUSAGE \n\
-                        join AB_ATTRIBUTE UIDROW on AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = UIDROW.AB_ATTRIBUTEID";
-    var usageTbl = db.table(condition.buildSql(usagesSelect, "1=1"));
-    usages = {};
-    for (let i = 0, l = usageTbl.length; i < l; i++)
-    {
-        let attrId = usageTbl[i][0];
-        if (attrId in usages)
-            usages[attrId].push(usageTbl[i][1]);
-        else
-            usages[attrId] = [usageTbl[i][1]];
-    }
-}
-
-var attributes = db.table(condition.buildSql(sqlSelect, "1=1", sqlOrder));
-
-var nameCache = {};
-result.object(_buildAttributeTable(attributes, usages, translateName));
-
-
-//sorts the records in a way that a tree can be built and adds values
-function _buildAttributeTable (pAttributes, pUsages, pTranslate) 
-{
-    var rows = {};
-    var allIds = {};
-    
-    //fills the allIds object, the object is used for checking if a parent exists in the array
-    for (let i = 0, l = pAttributes.length; i < l; i++)
-        allIds[pAttributes[i][0]] = true;
-    
-    var arrayIndex = 0;
-    
-    do {
-        var oldIndex = arrayIndex;
-        pAttributes.forEach(function (row)
-        {   
-            //item will be added if the id is not already in the object and
-            //the parent is already added (or the parent is not in the array)
-            if (!(row[0] in this) && (row[1] in this || !allIds[row[1]]))
-                this[row[0]] = {
-                    data : row,
-                    index : arrayIndex++
-                };
-        }, rows);
-    } while (oldIndex != arrayIndex); //stops the loop when no new items were added so that recursive relations between attributes don't cause an infinite loop
-    
-    var displaySimpleName = vars.exists("$param.DisplaySimpleName_param") && vars.get("$param.DisplaySimpleName_param");
-    var sortedArray = new Array(Object.keys(rows).length);
-    for (let i in rows)
-    {
-        let rowData = rows[i].data;
-        if (pUsages && rowData[5].trim() != $AttributeTypes.COMBOVALUE && i in pUsages)
-        {
-            rowData[7] = pUsages[i].map(function (usage)
-            {
-                return translate.text(usage);
-            }).join(", ");
-        }
-        var fullName;
-        if (displaySimpleName) 
-            fullName = pTranslate 
-                ? translate.text(rowData[8])
-                : rowData[8];
-        else
-            fullName = _getFullName(rowData[8], rowData[9], rowData[10], rowData[11], rowData[12], pTranslate);
-        rowData.splice(10, 3, fullName);
-        sortedArray[rows[i].index] = rowData;
-    }
-    
-    return sortedArray;
-    
-
-    /**
-     * builds the full attribute name from the pre-loaded parent names and adds all parent names
-     * if required
-     */
-    function _getFullName (pAttributeName, pParent1Name, pParent2Name, pParent3Name, pParent4Id)
-    {
-        var parent4FullName;
-        if (pParent4Id && pParent4Id in nameCache)
-            parent4FullName = nameCache[pParent4Id];
-        else
-        {
-            parent4FullName = pParent4Id ? AttributeUtil.getFullAttributeName(pParent4Id, false, pTranslate) : null;
-            nameCache[pParent4Id] = parent4FullName;
-        }
-        if (pTranslate)
-        {
-            pAttributeName = translate.text(pAttributeName);
-            pParent1Name = translate.text(pParent1Name);
-            pParent2Name = translate.text(pParent2Name);
-            pParent3Name = translate.text(pParent3Name);
-        }
-        pAttributeName = ArrayUtils.joinNonEmptyFields([parent4FullName, pParent3Name, pParent2Name, pParent1Name, pAttributeName], " / ");
-
-        return pAttributeName;
-    }
-}
+import("system.translate");
+import("Util_lib");
+import("JditoFilter_lib");
+import("KeywordRegistry_basic");
+import("Keyword_lib");
+import("system.db");
+import("system.vars");
+import("system.result");
+import("Sql_lib");
+import("Attribute_lib");
+
+var getGroups = vars.exists("$param.GetGroups_param") && vars.get("$param.GetGroups_param");
+var objectType = vars.exists("$param.ObjectType_param") && vars.get("$param.ObjectType_param");
+var parentType = vars.exists("$param.AttrParentType_param") && vars.get("$param.AttrParentType_param");
+var fetchUsages = true;
+var translateName = false;
+
+var uidTableAlias = "UIDROW";
+var sqlSelect = "select UIDROW.AB_ATTRIBUTEID, UIDROW.ATTRIBUTE_PARENT_ID, UIDROW.ATTRIBUTE_ACTIVE, UIDROW.DROPDOWNDEFINITION, UIDROW.SORTING, UIDROW.ATTRIBUTE_TYPE, " 
+    + KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.attributeType(), "UIDROW.ATTRIBUTE_TYPE") //3
+    + ", '', UIDROW.ATTRIBUTE_NAME, PARENT1.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_NAME, PARENT3.ATTRIBUTE_NAME, PARENT3.ATTRIBUTE_PARENT_ID \n\
+    from AB_ATTRIBUTE UIDROW \n\
+    left join AB_ATTRIBUTE PARENT1 on UIDROW.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID \n\
+    left join AB_ATTRIBUTE PARENT2 ON PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID \n\
+    left join AB_ATTRIBUTE PARENT3 ON PARENT2.ATTRIBUTE_PARENT_ID = PARENT3.AB_ATTRIBUTEID"; 
+
+/* always select the names of the next 3 parents so that less queries
+   are required later when buildung the full name */
+
+var sqlOrder = " order by UIDROW.ATTRIBUTE_PARENT_ID asc, UIDROW.SORTING asc";
+
+var condition = SqlCondition.begin();
+
+if (vars.exists("$local.idvalues") && vars.get("$local.idvalues"))
+{
+    condition.and("UIDROW.AB_ATTRIBUTEID in ('" + vars.get("$local.idvalues").join("','") + "')");
+}
+else if (getGroups) //if getGroups == true, it is the lookup for selecting the superordinate attribute
+{
+    //this condition filters out the own id and the children to prevent loops
+    
+    var isGroupCondition = SqlCondition.begin();
+    for (let type in $AttributeTypes)
+        if ($AttributeTypes[type].isGroup)
+        {
+            isGroupCondition.orPrepare(["AB_ATTRIBUTE", "ATTRIBUTE_TYPE", uidTableAlias], $AttributeTypes[type]);
+        }
+    
+    condition.andSqlCondition(SqlCondition.begin()
+        .andSqlCondition(isGroupCondition)
+        .andPrepareVars(["AB_ATTRIBUTE", "AB_ATTRIBUTEID", uidTableAlias], "$param.AttrParentId_param", "# != ?")
+        .and("UIDROW.AB_ATTRIBUTEID not in ('" + AttributeUtil.getAllChildren(vars.getString("$param.AttrParentId_param")).join("','") + "')")
+    );
+}
+else if (objectType)  //if there's an objectType, it comes from the AttributeRelation entity (lookup for the attribute selection)
+{
+    var filteredAttributes = null;
+    
+    if (vars.exists("$param.FilteredAttributeIds_param") && vars.getString("$param.FilteredAttributeIds_param")) {
+        filteredAttributes = JSON.parse(vars.getString("$param.FilteredAttributeIds_param"));
+    }
+    
+    var attributeCount;
+    if (vars.exists("$param.AttributeCount_param") && vars.get("$param.AttributeCount_param"))
+        attributeCount = JSON.parse(vars.getString("$param.AttributeCount_param"));
+    var ids = AttributeUtil.getPossibleAttributes(objectType, false, filteredAttributes, attributeCount);
+
+    if (ids.length > 0)
+        condition.and("UIDROW.AB_ATTRIBUTEID in ('" + ids.join("','") + "')");
+    else  // do not return anything, if parameter is there but an empty array
+        condition.and("1=2");
+
+     fetchUsages = false;
+     translateName = true;
+}
+else if (parentType) //condition for all subordinate attributes of an attribute (for the tree of subordinate attributes in an attribute)
+{
+    if (AttributeTypeUtil.isGroupType(parentType))
+    {
+        var parentId = vars.exists("$param.AttrParentId_param") && vars.get("$param.AttrParentId_param");
+        if (parentId)
+            condition.and("UIDROW.AB_ATTRIBUTEID in ('" + AttributeUtil.getAllChildren(vars.getString("$param.AttrParentId_param")).join("','") + "')");
+    }
+    else
+        condition.and("1=2");
+    
+    translateName = true;
+}
+
+//when there are filters selected, add them to the conditon
+if (vars.exists("$local.filter") && vars.get("$local.filter"))
+{
+    var filter = vars.get("$local.filter");
+    if (filter.filter)
+        condition.andSqlCondition(JditoFilterUtils.getSqlCondition(filter.filter, "AB_ATTRIBUTE", uidTableAlias));
+}
+
+var usages;
+if (fetchUsages) //this query is only necessary in Attribute, not in AttributeRelation
+{
+    var usagesSelect = "select AB_ATTRIBUTE_ID, OBJECT_TYPE from AB_ATTRIBUTEUSAGE \n\
+                        join AB_ATTRIBUTE UIDROW on AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = UIDROW.AB_ATTRIBUTEID";
+    var usageTbl = db.table(condition.buildSql(usagesSelect, "1=1"));
+    usages = {};
+    for (let i = 0, l = usageTbl.length; i < l; i++)
+    {
+        let attrId = usageTbl[i][0];
+        if (attrId in usages)
+            usages[attrId].push(usageTbl[i][1]);
+        else
+            usages[attrId] = [usageTbl[i][1]];
+    }
+}
+
+var attributes = db.table(condition.buildSql(sqlSelect, "1=1", sqlOrder));
+
+var nameCache = {};
+result.object(_buildAttributeTable(attributes, usages, translateName));
+
+
+//sorts the records in a way that a tree can be built and adds values
+function _buildAttributeTable (pAttributes, pUsages, pTranslate) 
+{
+    var rows = {};
+    var allIds = {};
+    
+    //fills the allIds object, the object is used for checking if a parent exists in the array
+    for (let i = 0, l = pAttributes.length; i < l; i++)
+        allIds[pAttributes[i][0]] = true;
+    
+    var arrayIndex = 0;
+    
+    do {
+        var oldIndex = arrayIndex;
+        pAttributes.forEach(function (row)
+        {   
+            //item will be added if the id is not already in the object and
+            //the parent is already added (or the parent is not in the array)
+            if (!(row[0] in this) && (row[1] in this || !allIds[row[1]]))
+                this[row[0]] = {
+                    data : row,
+                    index : arrayIndex++
+                };
+        }, rows);
+    } while (oldIndex != arrayIndex); //stops the loop when no new items were added so that recursive relations between attributes don't cause an infinite loop
+    
+    var displaySimpleName = vars.exists("$param.DisplaySimpleName_param") && vars.get("$param.DisplaySimpleName_param");
+    var sortedArray = new Array(Object.keys(rows).length);
+    for (let i in rows)
+    {
+        let rowData = rows[i].data;
+        if (pUsages && rowData[5].trim() != $AttributeTypes.COMBOVALUE && i in pUsages)
+        {
+            rowData[7] = pUsages[i].map(function (usage)
+            {
+                return translate.text(usage);
+            }).join(", ");
+        }
+        var fullName;
+        if (displaySimpleName) 
+            fullName = pTranslate 
+                ? translate.text(rowData[8])
+                : rowData[8];
+        else
+            fullName = _getFullName(rowData[8], rowData[9], rowData[10], rowData[11], rowData[12], pTranslate);
+        rowData.splice(10, 3, fullName);
+        sortedArray[rows[i].index] = rowData;
+    }
+    
+    return sortedArray;
+    
+
+    /**
+     * builds the full attribute name from the pre-loaded parent names and adds all parent names
+     * if required
+     */
+    function _getFullName (pAttributeName, pParent1Name, pParent2Name, pParent3Name, pParent4Id)
+    {
+        var parent4FullName;
+        if (pParent4Id && pParent4Id in nameCache)
+            parent4FullName = nameCache[pParent4Id];
+        else
+        {
+            parent4FullName = pParent4Id ? AttributeUtil.getFullAttributeName(pParent4Id, false, pTranslate) : null;
+            nameCache[pParent4Id] = parent4FullName;
+        }
+        if (pTranslate)
+        {
+            pAttributeName = translate.text(pAttributeName);
+            pParent1Name = translate.text(pParent1Name);
+            pParent2Name = translate.text(pParent2Name);
+            pParent3Name = translate.text(pParent3Name);
+        }
+        pAttributeName = ArrayUtils.joinNonEmptyFields([parent4FullName, pParent3Name, pParent2Name, pParent1Name, pAttributeName], " / ");
+
+        return pAttributeName;
+    }
+}
diff --git a/entity/BulkMail_entity/BulkMail_entity.aod b/entity/BulkMail_entity/BulkMail_entity.aod
index 14908fdd93218c7bbf3d723cd28d32e128402702..39f2deaae84dbb1f3a921d8dec0e5bf591fb8109 100644
--- a/entity/BulkMail_entity/BulkMail_entity.aod
+++ b/entity/BulkMail_entity/BulkMail_entity.aod
@@ -84,6 +84,7 @@
       <name>SENDER</name>
       <title>Sender address</title>
       <mandatory v="true" />
+      <onValidation>%aditoprj%/entity/BulkMail_entity/entityfields/sender/onValidation.js</onValidation>
     </entityField>
     <entityActionField>
       <name>sendMail</name>
@@ -95,11 +96,13 @@
     <entityField>
       <name>ICON</name>
       <contentType>IMAGE</contentType>
+      <searchable v="false" />
       <valueProcess>%aditoprj%/entity/BulkMail_entity/entityfields/icon/valueProcess.js</valueProcess>
     </entityField>
     <entityField>
       <name>preview</name>
       <contentType>HTML</contentType>
+      <searchable v="false" />
       <state>READONLY</state>
       <displayValueProcess>%aditoprj%/entity/BulkMail_entity/entityfields/preview/displayValueProcess.js</displayValueProcess>
     </entityField>
@@ -120,6 +123,7 @@
     <entityField>
       <name>BINDATA</name>
       <contentType>FILE</contentType>
+      <searchable v="false" />
       <stateProcess>%aditoprj%/entity/BulkMail_entity/entityfields/bindata/stateProcess.js</stateProcess>
     </entityField>
     <entityFieldGroup>
@@ -143,6 +147,7 @@
       <alias>Data_alias</alias>
       <fromClauseProcess>%aditoprj%/entity/BulkMail_entity/recordcontainers/db/fromClauseProcess.js</fromClauseProcess>
       <onDBInsert>%aditoprj%/entity/BulkMail_entity/recordcontainers/db/onDBInsert.js</onDBInsert>
+      <onDBUpdate>%aditoprj%/entity/BulkMail_entity/recordcontainers/db/onDBUpdate.js</onDBUpdate>
       <onDBDelete>%aditoprj%/entity/BulkMail_entity/recordcontainers/db/onDBDelete.js</onDBDelete>
       <linkInformation>
         <linkInformation>
diff --git a/entity/BulkMail_entity/entityfields/sender/onValidation.js b/entity/BulkMail_entity/entityfields/sender/onValidation.js
new file mode 100644
index 0000000000000000000000000000000000000000..a07daba57f94e4328ff3907479205d93e38a93db
--- /dev/null
+++ b/entity/BulkMail_entity/entityfields/sender/onValidation.js
@@ -0,0 +1,11 @@
+import("system.result");
+import("Communication_lib");
+import("Entity_lib");
+
+var message;
+var sender = ProcessHandlingUtils.getOnValidationValue();
+var fn = CommValidationUtil.makeValidationFn("EMAIL");
+if (fn !== null)
+    message = fn.call(null, sender);
+if (message)
+    result.string(message);
\ No newline at end of file
diff --git a/entity/BulkMail_entity/recordcontainers/db/onDBUpdate.js b/entity/BulkMail_entity/recordcontainers/db/onDBUpdate.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b8b28c49a9c31968b3cc5b34883d746bdc50193
--- /dev/null
+++ b/entity/BulkMail_entity/recordcontainers/db/onDBUpdate.js
@@ -0,0 +1,28 @@
+import("Sql_lib");
+import("system.result");
+import("system.vars");
+import("system.db");
+import("system.util");
+import("Document_lib");
+
+//TODO - Function
+
+var upload = vars.get("$field.BINDATA");
+var bindataUpload = DocumentUtil.getBindataFromUpload(upload);
+var filename = "";
+var bindata = "";
+
+if(bindataUpload != "")
+{
+    filename = DocumentUtil.getFilenameFromUpload(upload);
+    bindata  = bindataUpload;
+}
+
+if(bindata != "" && filename != "")
+{
+    let sysAlias = "_____SYSTEMALIAS";
+    var binaryId = db.cell(SqlCondition.begin(sysAlias)
+        .andPrepareVars("ASYS_BINARIES.ROW_ID", "$field.BULKMAILID")
+        .buildSql("select ID from ASYS_BINARIES", "1=2"), sysAlias);
+    db.updateBinary(binaryId, "", bindata, filename, "", "", sysAlias);
+}
\ No newline at end of file
diff --git a/entity/SerialLetter_entity/entityfields/downloadletter/onActionProcess.js b/entity/SerialLetter_entity/entityfields/downloadletter/onActionProcess.js
index 0b4d4abfc7f03b415d5494e33188d75a1a5384d4..bdf5e858524fedc80fa17146b801349638643407 100644
--- a/entity/SerialLetter_entity/entityfields/downloadletter/onActionProcess.js
+++ b/entity/SerialLetter_entity/entityfields/downloadletter/onActionProcess.js
@@ -1,17 +1,6 @@
-import("KeywordRegistry_basic");
-import("Contact_lib");
-import("Sql_lib");
-import("system.db");
-import("system.neon");
-import("system.vars");
-import("DocumentTemplate_lib");
-
-var template = DocumentTemplate.loadTemplate(vars.get("$field.DOCUMENTTEMPLATE_ID"));
-var contactIds = db.table(SqlCondition.begin()
-    .andPrepareVars("LETTERRECIPIENT.SERIALLETTER_ID", "$field.SERIALLETTERID")
-    .andSqlCondition(ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$letter(), true))
-    .buildSql("select CONTACT_ID from LETTERRECIPIENT join CONTACT on LETTERRECIPIENT.CONTACT_ID = CONTACT.CONTACTID", "1=2")
-);
-var document = template.getSerialLetterByContactIds(contactIds);
-if (document)
-    neon.download(document, template.filename);
\ No newline at end of file
+import("DocumentTemplate_lib");
+import("system.neon");
+import("Bulkmail_lib");
+import("system.vars");
+
+var document = SerialLetterUtils.buildSerialLetter(vars.get("$field.SERIALLETTERID"));
\ No newline at end of file
diff --git a/neonNotificationType/BulkMailSent/BulkMailSent.aod b/neonNotificationType/BulkMailSent/BulkMailSent.aod
index 63b6ad9c3e2da370cdd6b33a807b1852aece2ed8..77a69c9942425bf9543abff0c2a3d63df5db0fcc 100644
--- a/neonNotificationType/BulkMailSent/BulkMailSent.aod
+++ b/neonNotificationType/BulkMailSent/BulkMailSent.aod
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<neonNotificationType xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.0" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonNotificationType/1.1.0">
-  <name>BulkMailSent</name>
-  <title>Bulk mail sent</title>
-  <majorModelMode>DISTRIBUTED</majorModelMode>
-  <icon>VAADIN:ENVELOPES</icon>
-</neonNotificationType>
+<?xml version="1.0" encoding="UTF-8"?>
+<neonNotificationType xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.0" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonNotificationType/1.1.0">
+  <name>BulkMailSent</name>
+  <title>Bulk mail sent</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:ENVELOPES</icon>
+</neonNotificationType>
diff --git a/neonNotificationType/DownloadReady/DownloadReady.aod b/neonNotificationType/DownloadReady/DownloadReady.aod
new file mode 100644
index 0000000000000000000000000000000000000000..b8b50011b3a3fb6778905aecff612ab1a8446d40
--- /dev/null
+++ b/neonNotificationType/DownloadReady/DownloadReady.aod
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<neonNotificationType xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.0" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonNotificationType/1.1.0">
+  <name>DownloadReady</name>
+  <title>Download ready</title>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <icon>VAADIN:DOWNLOAD_ALT</icon>
+  <archivable v="false" />
+  <onResultOpen>%aditoprj%/neonNotificationType/DownloadReady/onResultOpen.js</onResultOpen>
+</neonNotificationType>
diff --git a/neonNotificationType/DownloadReady/onResultOpen.js b/neonNotificationType/DownloadReady/onResultOpen.js
new file mode 100644
index 0000000000000000000000000000000000000000..05f211c5511cd30430f3c0141712fe4a7a903367
--- /dev/null
+++ b/neonNotificationType/DownloadReady/onResultOpen.js
@@ -0,0 +1,10 @@
+import("system.logging");
+import("system.vars");
+
+var varses = ["$"];
+var existses = {};
+varses.forEach(function (v) {
+    this[v] = vars.exists(v);
+}, existses);
+
+logging.log(JSON.stringify(existses, null, "\t"))
\ No newline at end of file
diff --git a/neonView/BulkMailAddRecipientsEdit_view/BulkMailAddRecipientsEdit_view.aod b/neonView/BulkMailAddRecipientsEdit_view/BulkMailAddRecipientsEdit_view.aod
index 4407b8799f8672d03560f4ae529b57d6819d1385..e8b7469a6fc7e2208409757a38a938ce83efbe2d 100644
--- a/neonView/BulkMailAddRecipientsEdit_view/BulkMailAddRecipientsEdit_view.aod
+++ b/neonView/BulkMailAddRecipientsEdit_view/BulkMailAddRecipientsEdit_view.aod
@@ -1,35 +1,35 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.1">
-  <name>BulkMailAddRecipientsEdit_view</name>
-  <majorModelMode>DISTRIBUTED</majorModelMode>
-  <isSmall v="true" />
-  <layout>
-    <noneLayout>
-      <name>layout</name>
-    </noneLayout>
-  </layout>
-  <children>
-    <genericViewTemplate>
-      <name>Generic</name>
-      <editMode v="true" />
-      <entityField>#ENTITY</entityField>
-      <fields>
-        <entityFieldLink>
-          <name>96932894-43f2-471f-b511-3b38bd5f93cb</name>
-          <entityField>BULKMAIL_ID</entityField>
-        </entityFieldLink>
-      </fields>
-    </genericViewTemplate>
-    <genericViewTemplate>
-      <name>Message</name>
-      <hideLabels v="true" />
-      <entityField>#ENTITY</entityField>
-      <fields>
-        <entityFieldLink>
-          <name>e29ba637-3638-4c72-bdb7-65034636a882</name>
-          <entityField>recipientCountMessage</entityField>
-        </entityFieldLink>
-      </fields>
-    </genericViewTemplate>
-  </children>
-</neonView>
+<?xml version="1.0" encoding="UTF-8"?>
+<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.1">
+  <name>BulkMailAddRecipientsEdit_view</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <isSmall v="true" />
+  <layout>
+    <noneLayout>
+      <name>layout</name>
+    </noneLayout>
+  </layout>
+  <children>
+    <genericViewTemplate>
+      <name>Generic</name>
+      <editMode v="true" />
+      <entityField>#ENTITY</entityField>
+      <fields>
+        <entityFieldLink>
+          <name>96932894-43f2-471f-b511-3b38bd5f93cb</name>
+          <entityField>BULKMAIL_ID</entityField>
+        </entityFieldLink>
+      </fields>
+    </genericViewTemplate>
+    <genericViewTemplate>
+      <name>Message</name>
+      <hideLabels v="true" />
+      <entityField>#ENTITY</entityField>
+      <fields>
+        <entityFieldLink>
+          <name>e29ba637-3638-4c72-bdb7-65034636a882</name>
+          <entityField>recipientCountMessage</entityField>
+        </entityFieldLink>
+      </fields>
+    </genericViewTemplate>
+  </children>
+</neonView>
diff --git a/neonView/BulkMailEdit_view/BulkMailEdit_view.aod b/neonView/BulkMailEdit_view/BulkMailEdit_view.aod
index fa4ece93b65ff37c3fb38c7105bbe9d5db336d36..0f2c9009d08069b40cd8082687e39250ec8c88bc 100644
--- a/neonView/BulkMailEdit_view/BulkMailEdit_view.aod
+++ b/neonView/BulkMailEdit_view/BulkMailEdit_view.aod
@@ -1,43 +1,43 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.1">
-  <name>BulkMailEdit_view</name>
-  <majorModelMode>DISTRIBUTED</majorModelMode>
-  <layout>
-    <boxLayout>
-      <name>layout</name>
-    </boxLayout>
-  </layout>
-  <children>
-    <genericViewTemplate>
-      <name>BulkMail</name>
-      <editMode v="true" />
-      <entityField>#ENTITY</entityField>
-      <fields>
-        <entityFieldLink>
-          <name>b68c65de-4ecd-4a23-9242-f85e7b708b1e</name>
-          <entityField>DOCUMENTTEMPLATE_ID</entityField>
-        </entityFieldLink>
-        <entityFieldLink>
-          <name>f040d032-823c-4199-8314-01d784fdc167</name>
-          <entityField>BINDATA</entityField>
-        </entityFieldLink>
-        <entityFieldLink>
-          <name>e363bda2-d8bf-456e-bcae-d1870408022a</name>
-          <entityField>NAME</entityField>
-        </entityFieldLink>
-        <entityFieldLink>
-          <name>06f08869-5a81-41cb-8c7e-51be6a7041a7</name>
-          <entityField>DESCRIPTION</entityField>
-        </entityFieldLink>
-        <entityFieldLink>
-          <name>c73d0fb7-b740-48ac-8f3e-fd4199f169da</name>
-          <entityField>SUBJECT</entityField>
-        </entityFieldLink>
-        <entityFieldLink>
-          <name>e4ec09c2-3815-4a3b-bce8-c12d5b919b04</name>
-          <entityField>SENDER</entityField>
-        </entityFieldLink>
-      </fields>
-    </genericViewTemplate>
-  </children>
-</neonView>
+<?xml version="1.0" encoding="UTF-8"?>
+<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.1">
+  <name>BulkMailEdit_view</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <layout>
+    <boxLayout>
+      <name>layout</name>
+    </boxLayout>
+  </layout>
+  <children>
+    <genericViewTemplate>
+      <name>BulkMail</name>
+      <editMode v="true" />
+      <entityField>#ENTITY</entityField>
+      <fields>
+        <entityFieldLink>
+          <name>b68c65de-4ecd-4a23-9242-f85e7b708b1e</name>
+          <entityField>DOCUMENTTEMPLATE_ID</entityField>
+        </entityFieldLink>
+        <entityFieldLink>
+          <name>f040d032-823c-4199-8314-01d784fdc167</name>
+          <entityField>BINDATA</entityField>
+        </entityFieldLink>
+        <entityFieldLink>
+          <name>e363bda2-d8bf-456e-bcae-d1870408022a</name>
+          <entityField>NAME</entityField>
+        </entityFieldLink>
+        <entityFieldLink>
+          <name>06f08869-5a81-41cb-8c7e-51be6a7041a7</name>
+          <entityField>DESCRIPTION</entityField>
+        </entityFieldLink>
+        <entityFieldLink>
+          <name>c73d0fb7-b740-48ac-8f3e-fd4199f169da</name>
+          <entityField>SUBJECT</entityField>
+        </entityFieldLink>
+        <entityFieldLink>
+          <name>e4ec09c2-3815-4a3b-bce8-c12d5b919b04</name>
+          <entityField>SENDER</entityField>
+        </entityFieldLink>
+      </fields>
+    </genericViewTemplate>
+  </children>
+</neonView>
diff --git a/process/Bulkmail_lib/process.js b/process/Bulkmail_lib/process.js
index 48a86f98145d53a3fcc52d5fd774554ad5dc9c6a..aa9a59ff63531e8ea617a175ed1493335ae5b8df 100644
--- a/process/Bulkmail_lib/process.js
+++ b/process/Bulkmail_lib/process.js
@@ -1,183 +1,189 @@
-import("system.util");
-import("Contact_lib");
-import("system.datetime");
-import("system.neon");
-import("Employee_lib");
-import("system.vars");
-import("KeywordRegistry_basic");
-import("Sql_lib");
-import("system.db");
-import("DocumentTemplate_lib");
-import("Email_lib");
-import("system.process");
-
-/**
- * functions for bulk mails
- */
-function BulkMailUtils () {}
-
-/**
- * Executes a process to send bulk mails on the server and creates a notification when finished.
- * 
- * @param {String} pBulkMailId id of the bulk mail
- * @param {String} [pUser=currentUser] User that will get the notification, if null (not undefined!), no notification
- *                                      will be created.
- */
-BulkMailUtils.sendBulkMailOnServer = function (pBulkMailId, pUser)
-{
-    if (pUser === undefined)
-        pUser = EmployeeUtils.getCurrentUserId();
-    process.execute("sendBulkMail_serverProcess", 
-        {
-            bulkMailId : pBulkMailId,
-            user : pUser || ""
-        }
-    );
-}
-
-/**
- * Sends a bulk mail. You should only call this function on the server because it
- * can take some time to execute, use BulkMailUtils.sendBulkMailOnServer instead.
- * 
- * @param {String} pBulkMailId id of the bulk mail 
- * 
- * @return {Object} count of sucessful and failed mails 
- */
-BulkMailUtils.sendBulkMail = function (pBulkMailId)
-{
-    //TODO: Mailbridge, Werbesperre beachten
-    
-    var [templateId, subject, sender] = db.array(db.ROW, SqlCondition.begin()
-        .andPrepare("BULKMAIL.BULKMAILID", pBulkMailId)
-        .buildSql("select DOCUMENTTEMPLATE_ID, SUBJECT, SENDER from BULKMAIL", "1=2")
-    );
-    var template;
-    if (templateId)
-        template = DocumentTemplate.loadTemplate(templateId);
-    else
-        template = DocumentTemplate.loadTemplate(pBulkMailId, "BULKMAIL");
-    var emailSender;
-    
-    var recipientData = db.table(SqlCondition.begin()
-        .andPrepare("BULKMAILRECIPIENT.BULKMAIL_ID", pBulkMailId)
-        .andPrepare("BULKMAILRECIPIENT.STATUS", $KeywordRegistry.bulkMailRecipientStatus$sent(), "# != ?")
-        .andSqlCondition(ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail(), true))
-        .buildSql("select BULKMAILRECIPIENTID, BULKMAILRECIPIENT.CONTACT_ID, '' from BULKMAILRECIPIENT \n\
-            join CONTACT on BULKMAILRECIPIENT.CONTACT_ID = CONTACT.CONTACTID", "1=2")
-    );
-    var contactIds = recipientData.map(function (e) {return e[1];});
-    var successIds = [];
-    var failedIds = [];
-    var sentDate = vars.get("$sys.date");
-    var mails = template.getReplacedEmailsByContactIds(contactIds);
-    
-    var subjectTemplate = new DocumentTemplate(subject, DocumentTemplate.types.PLAIN);
-    var subjects = subjectTemplate.getReplacedContentByContactIds(contactIds);
-    
-    for (let i = 0, l = recipientData.length; i < l; i++)
-    {
-        let isSuccess = false;
-        let contactId = recipientData[i][1];
-        let email = mails[contactId];
-        if (email !== undefined && recipientData[i][2])
-        {
-            email.toRecipients = [recipientData[i][2]]; //TODO: email address missing
-            email.sender = emailSender;
-            email.subject = subjects[contactId];
-
-            isSuccess = email.send();
-        }
-        if (isSuccess)
-            successIds.push(recipientData[i][0]); //set the recipient status to 'sent'
-        else
-            failedIds.push(recipientData[i][0]); //set the recipient status to 'failed'
-    }
-    db.updateData("BULKMAILRECIPIENT", ["STATUS", "SENTDATE"], null, [$KeywordRegistry.bulkMailRecipientStatus$sent(), sentDate], 
-        SqlCondition.begin()
-            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", successIds)
-            .build("1=2")
-    );
-    db.updateData("BULKMAILRECIPIENT", ["STATUS", "SENTDATE"], null, [$KeywordRegistry.bulkMailRecipientStatus$failed(), sentDate], 
-        SqlCondition.begin()
-            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", failedIds)
-            .build("1=2")
-    );
-        
-    db.updateData("BULKMAIL", ["STATUS"], null, [$KeywordRegistry.bulkMailStatus$sent()], 
-        SqlCondition.equals("BULKMAIL.BULKMAILID", pBulkMailId, "1=2"));
-        
-    return {
-        sucessful : successIds.length,
-        failed : failedIds.length
-    };
-}
-
-BulkMailUtils.openAddRecipientView = function (pContactIds)
-{
-    var params = {
-        "ContactIds_param" : pContactIds
-    };
-    neon.openContext("BulkMailAddRecipients", "BulkMailAddRecipientsEdit_view", null, neon.OPERATINGSTATE_NEW, params);
-}
-
-BulkMailUtils.removeCommRestrictionRecipients = function (pBulkMailId)
-{
-    var recipientIds = db.array(db.COLUMN, SqlBuilder.begin()
-        .select("BULKMAILRECIPIENTID")
-        .from("BULKMAILRECIPIENT")
-        .join("CONTACT", SqlCondition.begin()
-            .and("BULKMAILRECIPIENT.CONTACT_ID = CONTACT.CONTACTID")
-            .andSqlCondition(ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail())))
-        .where(SqlCondition.begin()
-            .andPrepare("BULKMAILRECIPIENT.BULKMAIL_ID", pBulkMailId))
-        .build());
-
-    if (recipientIds.length)
-    {
-        db.deleteData("BULKMAILRECIPIENT", SqlCondition.begin()
-            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", recipientIds)
-            .build("1=2"));
-    }
-}
-
-BulkMailUtils.addRecipients = function (pBulkMailId, pContactIds)
-{
-    var columns = [
-        "BULKMAILRECIPIENTID",
-        "BULKMAIL_ID",
-        "CONTACT_ID",
-        "STATUS"
-    ];
-    var inserts = [];
-    for (let i = 0, l = pContactIds.length; i < l; i++)
-    {
-        inserts.push(["BULKMAILRECIPIENT", columns, null, [util.getNewUUID(), pBulkMailId, pContactIds[i], $KeywordRegistry.bulkMailRecipientStatus$pending()]]);
-    }
-    db.inserts(inserts);
-}
-
-function SerialLetterUtils () {}
-
-SerialLetterUtils.addRecipients = function (pBulkMailId, pContactIds)
-{
-    var columns = [
-        "LETTERRECIPIENTID",
-        "SERIALLETTER_ID",
-        "CONTACT_ID"
-    ];
-    var inserts = [];
-    for (let i = 0, l = pContactIds.length; i < l; i++)
-    {
-        inserts.push(["LETTERRECIPIENT", columns, null, [util.getNewUUID(), pBulkMailId, pContactIds[i]]]);
-    }
-    db.inserts(inserts);
-}
-
-
-SerialLetterUtils.openAddRecipientView = function (pContactIds)
-{
-    var params = {
-        "ContactIds_param" : pContactIds
-    };
-    neon.openContext("SerialLetterAddRecipients", "SerialLetterAddRecipientsEdit_view", null, neon.OPERATINGSTATE_NEW, params);
+import("system.util");
+import("Contact_lib");
+import("system.datetime");
+import("system.neon");
+import("Employee_lib");
+import("system.vars");
+import("KeywordRegistry_basic");
+import("Sql_lib");
+import("system.db");
+import("DocumentTemplate_lib");
+import("Email_lib");
+import("system.process");
+
+/**
+ * functions for bulk mails
+ */
+function BulkMailUtils () {}
+
+/**
+ * Executes a process to send bulk mails on the server and creates a notification when finished.
+ * 
+ * @param {String} pBulkMailId id of the bulk mail
+ * @param {String} [pUser=currentUser] User that will get the notification, if null (not undefined!), no notification
+ *                                      will be created.
+ */
+BulkMailUtils.sendBulkMailOnServer = function (pBulkMailId, pUser)
+{
+    if (pUser === undefined)
+        pUser = EmployeeUtils.getCurrentUserId();
+    process.execute("sendBulkMail_serverProcess", 
+        {
+            bulkMailId : pBulkMailId,
+            user : pUser || ""
+        }
+    );
+}
+
+/**
+ * Sends a bulk mail. You should only call this function on the server because it
+ * can take some time to execute, use BulkMailUtils.sendBulkMailOnServer instead.
+ * 
+ * @param {String} pBulkMailId id of the bulk mail 
+ * 
+ * @return {Object} count of sucessful and failed mails 
+ */
+BulkMailUtils.sendBulkMail = function (pBulkMailId)
+{
+    //TODO: Mailbridge, Werbesperre beachten
+    
+    var [templateId, subject, emailSender] = db.array(db.ROW, SqlCondition.begin()
+        .andPrepare("BULKMAIL.BULKMAILID", pBulkMailId)
+        .buildSql("select DOCUMENTTEMPLATE_ID, SUBJECT, SENDER from BULKMAIL", "1=2")
+    );
+    var template;
+    if (templateId)
+        template = DocumentTemplate.loadTemplate(templateId);
+    else
+        template = DocumentTemplate.loadTemplate(pBulkMailId, "BULKMAIL");
+    
+    var recipientData = db.table(SqlCondition.begin()
+        .andPrepare("BULKMAILRECIPIENT.BULKMAIL_ID", pBulkMailId)
+        .andPrepare("BULKMAILRECIPIENT.STATUS", $KeywordRegistry.bulkMailRecipientStatus$sent(), "# != ?")
+        .andSqlCondition(ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail(), true))
+        .buildSql("select BULKMAILRECIPIENTID, BULKMAILRECIPIENT.CONTACT_ID, '' from BULKMAILRECIPIENT \n\
+            join CONTACT on BULKMAILRECIPIENT.CONTACT_ID = CONTACT.CONTACTID", "1=2")
+    );
+    var contactIds = recipientData.map(function (e) {return e[1];});
+    var successIds = [];
+    var failedIds = [];
+    var sentDate = vars.get("$sys.date");
+    var mails = template.getReplacedEmailsByContactIds(contactIds);
+    
+    var subjectTemplate = new DocumentTemplate(subject, DocumentTemplate.types.PLAIN);
+    var subjects = subjectTemplate.getReplacedContentByContactIds(contactIds);
+    
+    for (let i = 0, l = recipientData.length; i < l; i++)
+    {
+        let isSuccess = false;
+        let contactId = recipientData[i][1];
+        let email = mails[contactId];
+        if (email !== undefined && recipientData[i][2])
+        {
+            email.toRecipients = [recipientData[i][2]]; //TODO: email address missing
+            email.sender = emailSender;
+            email.subject = subjects[contactId];
+
+            isSuccess = email.send();
+        }
+        if (isSuccess)
+            successIds.push(recipientData[i][0]); //set the recipient status to 'sent'
+        else
+            failedIds.push(recipientData[i][0]); //set the recipient status to 'failed'
+    }
+    db.updateData("BULKMAILRECIPIENT", ["STATUS", "SENTDATE"], null, [$KeywordRegistry.bulkMailRecipientStatus$sent(), sentDate], 
+        SqlCondition.begin()
+            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", successIds)
+            .build("1=2")
+    );
+    db.updateData("BULKMAILRECIPIENT", ["STATUS", "SENTDATE"], null, [$KeywordRegistry.bulkMailRecipientStatus$failed(), sentDate], 
+        SqlCondition.begin()
+            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", failedIds)
+            .build("1=2")
+    );
+        
+    db.updateData("BULKMAIL", ["STATUS"], null, [$KeywordRegistry.bulkMailStatus$sent()], 
+        SqlCondition.equals("BULKMAIL.BULKMAILID", pBulkMailId, "1=2"));
+        
+    return {
+        sucessful : successIds.length,
+        failed : failedIds.length
+    };
+}
+
+BulkMailUtils.openAddRecipientView = function (pContactIds)
+{
+    var params = {
+        "ContactIds_param" : pContactIds
+    };
+    neon.openContext("BulkMailAddRecipients", "BulkMailAddRecipientsEdit_view", null, neon.OPERATINGSTATE_NEW, params);
+}
+
+BulkMailUtils.removeCommRestrictionRecipients = function (pBulkMailId)
+{
+    var recipientIds = db.array(db.COLUMN, SqlBuilder.begin()
+        .select("BULKMAILRECIPIENTID")
+        .from("BULKMAILRECIPIENT")
+        .join("CONTACT", SqlCondition.begin()
+            .and("BULKMAILRECIPIENT.CONTACT_ID = CONTACT.CONTACTID")
+            .andSqlCondition(ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$mail())))
+        .where(SqlCondition.begin()
+            .andPrepare("BULKMAILRECIPIENT.BULKMAIL_ID", pBulkMailId))
+        .build());
+
+    if (recipientIds.length)
+    {
+        db.deleteData("BULKMAILRECIPIENT", SqlCondition.begin()
+            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", recipientIds)
+            .build("1=2"));
+    }
+}
+
+BulkMailUtils.addRecipients = function (pBulkMailId, pContactIds)
+{
+    var columns = [
+        "BULKMAILRECIPIENTID",
+        "BULKMAIL_ID",
+        "CONTACT_ID",
+        "STATUS"
+    ];
+    var inserts = [];
+    for (let i = 0, l = pContactIds.length; i < l; i++)
+    {
+        inserts.push(["BULKMAILRECIPIENT", columns, null, [util.getNewUUID(), pBulkMailId, pContactIds[i], $KeywordRegistry.bulkMailRecipientStatus$pending()]]);
+    }
+    db.inserts(inserts);
+}
+
+function SerialLetterUtils () {}
+
+SerialLetterUtils.addRecipients = function (pBulkMailId, pContactIds)
+{
+    var columns = [
+        "LETTERRECIPIENTID",
+        "SERIALLETTER_ID",
+        "CONTACT_ID"
+    ];
+    var inserts = [];
+    for (let i = 0, l = pContactIds.length; i < l; i++)
+    {
+        inserts.push(["LETTERRECIPIENT", columns, null, [util.getNewUUID(), pBulkMailId, pContactIds[i]]]);
+    }
+    db.inserts(inserts);
+}
+
+
+SerialLetterUtils.openAddRecipientView = function (pContactIds)
+{
+    var params = {
+        "ContactIds_param" : pContactIds
+    };
+    neon.openContext("SerialLetterAddRecipients", "SerialLetterAddRecipientsEdit_view", null, neon.OPERATINGSTATE_NEW, params);
+}
+
+SerialLetterUtils.buildSerialLetter = function (pSerialLetterId)
+{
+    process.executeAsync("buildSerialLetter_serverProcess", {
+        "serialLetterId" : pSerialLetterId
+    }, false, "_____USER_bcdfb521-c7d0-4ef1-8916-78e7d3232046", process.THREADPRIORITY_VERYHIGH);
 }
\ No newline at end of file
diff --git a/process/DocumentTemplate_lib/process.js b/process/DocumentTemplate_lib/process.js
index 6b72a2999d6adb049e89764d555451f40afab0db..4198d8c2bdb353edb4c11d907fc6ea5f28dafa50 100644
--- a/process/DocumentTemplate_lib/process.js
+++ b/process/DocumentTemplate_lib/process.js
@@ -1,435 +1,436 @@
-import("Communication_lib");
-import("system.neon");
-import("Employee_lib");
-import("KeywordRegistry_basic");
-import("Document_lib");
-import("KeywordData_lib");
-import("Sql_lib");
-import("Address_lib");
-import("system.process");
-import("system.vars");
-import("system.db");
-import("system.util");
-import("system.pack");
-import("system.fileIO");
-import("system.translate");
-import("system.datetime");
-import("system.text");
-import("system.mail");
-import("Keyword_lib");
-import("Placeholder_lib");
-import("Email_lib");
-
-/**
- * Object for working with document templates, holds the content and type of the template.
- * Provides functions to replace placeholders in the content.
- * 
- * @class
- */
-function DocumentTemplate (pTemplateContent, pType, pFilename)
-{
-    this.content = pTemplateContent;
-    this.type = pType;
-    this.filename = pFilename;
-}
-
-DocumentTemplate.prototype.toString = function ()
-{
-    if (this.type == DocumentTemplate.types.PLAIN)
-        return this.content;
-    return text.parseDocument(this.content);
-}
-
-/**
- * The types a DocumentTemplate can have. Depending on the type,
- * the correct method for replacing the placeholders can be chosen
- * 
- * @enum {String}
- */
-DocumentTemplate.types = {
-    TXT : "txt",
-    HTML : "html",
-    EML : "eml",
-    ODT : "odt",
-    DOCX : "docx",
-    PLAIN : "plain" //for simple strings
-};
-
-/**
- * Loads the content of a document template and creates a new DocumentTemplate object with that.
- * 
- * @param {String} pAssignmentRowId id of the assignment (in most cases the document template id)
- * @param {String} [pAssignmentTable="DOCUMENTTEMPLATE"] the LOB assignment table
- * 
- * @return {DocumentTemplate} template object
- * 
- * @throws {Error} if the type can't be used
- */
-DocumentTemplate.loadTemplate = function (pAssignmentRowId, pAssignmentTable)
-{
-    var alias = "_____SYSTEMALIAS";
-    if (!pAssignmentTable)
-        pAssignmentTable = "DOCUMENTTEMPLATE";
-    var templateDocument = db.getBinaryMetadata(pAssignmentTable, "DOCUMENT", pAssignmentRowId, false, alias, null);
-    if (!templateDocument[0])
-        return new DocumentTemplate();
-    var binaryId = templateDocument[0][db.BINARY_ID];
-    var filename = templateDocument[0][db.BINARY_FILENAME];
-    var mimetype = templateDocument[0][db.BINARY_MIMETYPE];
-    var typeMap = {
-        "text/plain" : DocumentTemplate.types.TXT,
-        "text/html" : DocumentTemplate.types.HTML,
-        "message/rfc822" : DocumentTemplate.types.EML,
-        "application/vnd.oasis.opendocument.text" : DocumentTemplate.types.ODT,
-        "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : DocumentTemplate.types.DOCX
-    };
-    var type = typeMap[mimetype];
-    if (type === undefined)
-        throw new Error("Invalid mime type for document template");
-    
-    return new DocumentTemplate(db.getBinaryContent(binaryId, alias), type, filename);
-}
-
-/**
- * Replace function that works with strings instead of regular expressions
- * so that control characters (for example '{', '}') don't have to be escaped.
- * 
- * @private
- */
-DocumentTemplate._replaceText = function (pText, pReplacements)
-{
-    for (let placeholder in pReplacements)
-        pText = pText.replace(placeholder, pReplacements[placeholder], "ig");
-    return pText;
-}
-
-/**
- * returns the 'simpleName' of all placeholders that are used in the template
- * 
- * @private
- */
-DocumentTemplate.prototype._getRequiredPlaceholders = function ()
-{
-    var allPlaceholders = PlaceholderUtils.getPlaceholders();
-    var plainText = this.toString();
-    var usedPlaceholders = [];
-    for (let i = 0, l = allPlaceholders.length; i < l; i++)
-    {
-        if (plainText.indexOf(allPlaceholders[i]) !== -1)
-            usedPlaceholders.push(allPlaceholders[i]);
-    }
-    return usedPlaceholders; 
-}
-
-/**
- * Returns the template content with replaced placeholders by choosing the right
- * replace function for the type.
- * 
- * @param {Object} pReplacements map, the structure is {placeholder : value}
- * 
- * @return {String} the replaced content
- */
-DocumentTemplate.prototype.getReplacedContent = function (pReplacements)
-{
-    switch (this.type)
-    {
-        case DocumentTemplate.types.HTML:
-            for (let i in pReplacements)
-                pReplacements[i] = text.replaceAll(pReplacements[i], {"\n" : "<br>"});
-        case DocumentTemplate.types.TXT:
-            let decodedContent = util.decodeBase64String(this.content)
-            return DocumentTemplate._replaceText(decodedContent, pReplacements);
-        case DocumentTemplate.types.EML:
-            return this._getReplacedEML(pReplacements);
-        case DocumentTemplate.types.ODT:
-            return this._getReplacedODT(pReplacements);
-        case DocumentTemplate.types.DOCX:
-            return this._getReplacedDOCX(pReplacements);
-        case DocumentTemplate.types.PLAIN:
-            return DocumentTemplate._replaceText(this.content, pReplacements);
-        default:
-            return null;
-    }
-}
-
-/**
- * replaces the placeholders with data from one contact and returns the result
- */
-DocumentTemplate.prototype.getReplacedContentByContactId = function (pContactId) 
-{
-    var replacements = this._getReplacementsByContactIds([pContactId]); 
-    return this.getReplacedContent(replacements[pContactId]);
-}
-
-/**
- * replaces the placeholders with data from the contacts and returns the result
- * 
- * @param {Array} pContactIds contact ids
- * 
- * @return {Object} replaced content for every contactId
- */
-DocumentTemplate.prototype.getReplacedContentByContactIds = function (pContactIds) 
-{
-    var replacements = this._getReplacementsByContactIds(pContactIds);
-    var contents = {};
-    for (let contactId in replacements)
-    {
-        contents[contactId] = this.getReplacedContent(replacements[contactId]);
-    }
-    return contents;
-}
-
-/**
- * Replaces the placeholders with data from the contacts and returns a serial letter, works
- * only for ODT
- * 
- * @param {Array} pContactIds contact ids
- * 
- * @return {Object} the content of the replaced ODT
- */
-DocumentTemplate.prototype.getSerialLetterByContactIds = function (pContactIds)
-{
-    var replacements = this._getReplacementsByContactIds(pContactIds);
-    if (this.type == DocumentTemplate.types.ODT)
-    {
-        let replaceArray = [];
-        for (let i = 0, l = pContactIds.length; i < l; i++)
-            replaceArray.push(replacements[pContactIds[i]]);
-        return this._getReplacedODT(replaceArray);
-    }
-    return null;
-}
-
-/**
- * Replaces the placeholders with data from the contacts and returns the resulting Emails.
- * 
- * @param {Array} pContactIds contact ids
- * 
- * @return {Object} Object containing the contact ids as keys and the corresponding Email
- *                   objects as values
- */
-DocumentTemplate.prototype.getReplacedEmailsByContactIds = function (pContactIds) 
-{
-    var replacements = this._getReplacementsByContactIds(pContactIds);
-    var emailObj = {};
-    for (let contactId in replacements)
-    {
-        if (this.type == DocumentTemplate.types.EML)
-        {
-            //use the special function for EML to also fill subject and sender
-            emailObj[contactId] = this._getReplacedEML(replacements[contactId], true);
-        }
-        else
-        {
-            let body = this.getReplacedContent(replacements[contactId]);
-            if (this.type == DocumentTemplate.types.TXT)
-                body = text.replaceAll(body, {"\n" : "<br>"});
-            emailObj[contactId] = new Email(null, null, null, body);
-        }
-    }
-    return emailObj;
-}
-
-/**
- * Builds an object with the placeholder data for multiple contacts
- * 
- * @param {Array} pContactIds contact ids
- * 
- * @return {Object} Object containing the data. The structure is like {contactId : {placeholderName : replacementValue, ...}, ...}
- * 
- * @private
- */
-DocumentTemplate.prototype._getReplacementsByContactIds = function (pContactIds)
-{
-    var config = this._getRequiredPlaceholders();
-    var contactIdPlaceholder = new Placeholder("contactId", Placeholder.types.SQLPART, "CONTACT.CONTACTID");
-    config = [contactIdPlaceholder].concat(config);
-    var addressData = getAddressesData(pContactIds, config, EmployeeUtils.getCurrentContactId()); //TODO: add sender selection
-    var replacements = {};
-    var placeholderNames = addressData[0];
-    var contactIdIndex = placeholderNames.indexOf(contactIdPlaceholder.toString());
-    for (let i = 1; i < addressData.length; i++)
-    {
-        let contactId = addressData[i][contactIdIndex];
-        for (let ii = 0, ll = placeholderNames.length; ii < ll; ii++)
-        {
-            if (!(contactId in replacements))
-                replacements[contactId] = {};
-            replacements[contactId][placeholderNames[ii]] = addressData[i][ii];
-        }
-    }
-    return replacements;
-}
-
-/**
- * Replaces placeholders for EML
- * 
- * @param {Object} pReplacements mapping with replacements for every placeholder
- * @param {boolean} [pGetEmail] if true, return an Email object (use this if the sender and subject are required)
- * 
- * @return {String|Email} the replaced content
- * 
- * @private
- */
-DocumentTemplate.prototype._getReplacedEML = function (pReplacements, pGetEmail)
-{
-    var mailData = mail.parseRFC(util.decodeBase64String(this.content));
-    var email;
-    var body = DocumentTemplate._replaceText(mailData[mail.MAIL_HTMLTEXT], pReplacements);
-    if (pGetEmail)
-    {
-        var sender = DocumentTemplate._replaceText(mailData[mail.MAIL_SENDER], pReplacements);
-        var subject = DocumentTemplate._replaceText(mailData[mail.MAIL_SUBJECT], pReplacements);
-        email = new Email(null, sender, subject, body);
-    }
-    else
-        email = body;
-    return email;
-}
-
-/*
- * replaces a given Odt-File on the server and returns the replaced base64-file
- *
- * @param {Object} pReplacements map of placeholders and replacements
- *
- * @return {String} base64-encoded replaced file
- * 
- * @private
- */
-DocumentTemplate.prototype._getReplacedODT = function (pReplacements)
-{
-    //save the file on the server so it can be unzipped via pack.getFromZip
-    var serverFilePath = vars.get("$sys.servertemp") + "/clientid_" + vars.get("$sys.clientid")
-        + "/" + util.getNewUUID() + "/" + this.filename.replace(/\\/g, "/");
-    
-    fileIO.storeData(serverFilePath, this.content, util.DATA_BINARY, false);
-    if (!_replaceODTFile(pReplacements, serverFilePath))
-        return null;
-
-    var replacedFileData = fileIO.getData(serverFilePath, util.DATA_BINARY);
-    fileIO.remove(serverFilePath);
-
-    return replacedFileData;
-    
-    /**
-    * replaces placeholders in a odt file
-    *
-    * @param {Object} pReplacements replacement object
-    * @param {String} pODTFileName filename of the odt
-    * 
-    * @return {Boolean}
-    */
-    function _replaceODTFile (pReplacements, pODTFileName)
-    {
-        var senderRelId = EmployeeUtils.getCurrentContactId();
-        if (senderRelId == null)
-            return false;
-        if (pReplacements.length === undefined)
-            pReplacements = [pReplacements];
-        if (pReplacements.length !== 0)
-        {
-            //replace placeholders in content.xml
-            var contentXml = util.decodeBase64String(pack.getFromZip(pODTFileName, "content.xml"));
-            var bodybegin = contentXml.indexOf("<office:body>");
-            var bodyend =  contentXml.indexOf("</office:body>") + 14;
-            
-            var bodyTemplate = contentXml.substring(bodybegin, bodyend);
-            var fullBody = "";  //body that contains all pages (required when the replacing is done for several contacts)
-            var beforeBody = contentXml.substring(0, bodybegin);
-            var afterBody = contentXml.substr(bodyend);
-            
-            for (let i = 0, l = pReplacements.length; i < l; i++)
-            {
-                let replacements = pReplacements[i];
-                let currentBody = bodyTemplate;
-            
-                /* This only works if the text of the placeholders in the odt were not edited since they were written.
-                 * If you edit the odt and change a placeholder (for example: you change '{@addres@}' to '{@address@}'),
-                 * the text is saved in different XML tags and won't be replaced correctly.
-                 */
-
-                for (let placeholder in replacements)
-                {
-                    currentBody = currentBody.replace(placeholder,
-                        replacements[placeholder].replace(/\n/ig, "<text:line-break/>").replace(/&/ig, "&amp;"), "ig");
-                }
-                
-                fullBody += currentBody;
-            }
-            //TODO: TableData
-//            for (ti = 0; ti < pTableData.length; ti++)
-//                {
-//                    var tablepos = bulkbody.indexOf( getDefaultODTplaceholer(pTableData[ti].Table, true));
-//                    if ( tablepos != -1 )
-//                    {
-//                        var tablebegin = bulkbody.lastIndexOf("<table:table-row", tablepos);
-//                        var tableend =  bulkbody.indexOf("</table:table-row>", tablepos ) + 18;
-//                        var lasttable =  bulkbody.substr( tableend );
-//                        var tablerow = bulkbody.substring( tablebegin, tableend );
-//                        bulkbody = bulkbody.substring( 0, tablebegin );
-//                        var tabledata = pTableData[ti].TableData[addrdata[i][0]];
-//                        if ( tabledata != undefined )
-//                        {
-//                            for (var tz = 0; tz < tabledata.length; tz++)
-//                            {
-//                                var table = tablerow;
-//                                bulkbody += relaceAdditionValues( table, pTableData[ti].Fields, tabledata[tz], pTableData[ti].Table + "." );
-//                            }
-//                        }
-//                        bulkbody += lasttable;
-//                }
-                
-                
-            contentXml = beforeBody + fullBody + afterBody;
-            pack.addToZip(pODTFileName, "content.xml", util.encodeBase64String(contentXml));
-            
-            //replace placeholders in styles.xml
-            var styles = util.decodeBase64String(pack.getFromZip(pODTFileName, "styles.xml"));
-            for (let placeholder in pReplacements[0])
-            {
-                styles = styles.replace(placeholder,
-                    pReplacements[0][placeholder].replace(/\n/ig, "<text:line-break/>").replace(/&/ig, "&amp;"), "ig");
-            }
-            pack.addToZip(pODTFileName, "styles.xml", util.encodeBase64String(styles));
-            return true;
-        }
-        return false;
-    }
-}
-
-/*
- * This function is used to replace placeholders via DocXTemplater
- * 
- * @param {Object} pReplacements - Must contain an object, which holds the placeholders
- * 
- * @return {String} returns the modified document in a BASE64 coded string
- * 
- * @private
- */
-DocumentTemplate.prototype._getReplacedDOCX = function (pReplacements)
-{
-    var replacements = {};
-    for (let placeholder in pReplacements)  //removes the prefix and postfix, the process needs it like this
-        replacements[placeholder.slice(3, -3)] = pReplacements[placeholder];
-    
-    //this is executed as a process because of better performance
-    var documentData = process.execute("getDocxDocument_serverProcess", {
-        templateb64: this.content,
-        placeholderConfig: JSON.stringify(replacements) //process.execute is only able to handle strings
-    });
-
-    return documentData;
-}
-
-/**
- * functions for working with letters (mails)
- */
-function LetterUtils () {}
-
-LetterUtils.openNewLetter = function (pContactId)
-{
-    var params = {
-        "ContactId_param" : pContactId
-    };
-    neon.openContext("Letter", "LetterEdit_view", null, neon.OPERATINGSTATE_NEW, params);
+import("Communication_lib");
+import("system.neon");
+import("Employee_lib");
+import("KeywordRegistry_basic");
+import("Document_lib");
+import("KeywordData_lib");
+import("Sql_lib");
+import("Address_lib");
+import("system.process");
+import("system.vars");
+import("system.db");
+import("system.util");
+import("system.pack");
+import("system.fileIO");
+import("system.translate");
+import("system.datetime");
+import("system.text");
+import("system.mail");
+import("Keyword_lib");
+import("Placeholder_lib");
+import("Email_lib");
+
+/**
+ * Object for working with document templates, holds the content and type of the template.
+ * Provides functions to replace placeholders in the content.
+ * 
+ * @class
+ */
+function DocumentTemplate (pTemplateContent, pType, pFilename)
+{
+    this.content = pTemplateContent;
+    this.type = pType;
+    this.filename = pFilename;
+}
+
+DocumentTemplate.prototype.toString = function ()
+{
+    if (this.type == DocumentTemplate.types.PLAIN)
+        return this.content;
+    return text.parseDocument(this.content);
+}
+
+/**
+ * The types a DocumentTemplate can have. Depending on the type,
+ * the correct method for replacing the placeholders can be chosen
+ * 
+ * @enum {String}
+ */
+DocumentTemplate.types = {
+    TXT : "txt",
+    HTML : "html",
+    EML : "eml",
+    ODT : "odt",
+    DOCX : "docx",
+    PLAIN : "plain" //for simple strings
+};
+
+/**
+ * Loads the content of a document template and creates a new DocumentTemplate object with that.
+ * 
+ * @param {String} pAssignmentRowId id of the assignment (in most cases the document template id)
+ * @param {String} [pAssignmentTable="DOCUMENTTEMPLATE"] the LOB assignment table
+ * 
+ * @return {DocumentTemplate} template object
+ * 
+ * @throws {Error} if the type can't be used
+ */
+DocumentTemplate.loadTemplate = function (pAssignmentRowId, pAssignmentTable)
+{
+    var alias = "_____SYSTEMALIAS";
+    if (!pAssignmentTable)
+        pAssignmentTable = "DOCUMENTTEMPLATE";
+    var templateDocument = db.getBinaryMetadata(pAssignmentTable, "DOCUMENT", pAssignmentRowId, false, alias, null);
+    if (!templateDocument[0])
+        return new DocumentTemplate();
+    var binaryId = templateDocument[0][db.BINARY_ID];
+    var filename = templateDocument[0][db.BINARY_FILENAME];
+    var mimetype = templateDocument[0][db.BINARY_MIMETYPE];
+    var typeMap = {
+        "text/plain" : DocumentTemplate.types.TXT,
+        "text/html" : DocumentTemplate.types.HTML,
+        "message/rfc822" : DocumentTemplate.types.EML,
+        "application/vnd.oasis.opendocument.text" : DocumentTemplate.types.ODT,
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : DocumentTemplate.types.DOCX
+    };
+    var type = typeMap[mimetype];
+    if (type === undefined)
+        throw new Error("Invalid mime type for document template");
+    
+    return new DocumentTemplate(db.getBinaryContent(binaryId, alias), type, filename);
+}
+
+/**
+ * Replace function that works with strings instead of regular expressions
+ * so that control characters (for example '{', '}') don't have to be escaped.
+ * 
+ * @private
+ */
+DocumentTemplate._replaceText = function (pText, pReplacements)
+{
+    for (let placeholder in pReplacements)
+        pText = pText.replace(placeholder, pReplacements[placeholder], "ig");
+    return pText;
+}
+
+/**
+ * returns the 'simpleName' of all placeholders that are used in the template
+ * 
+ * @private
+ */
+DocumentTemplate.prototype._getRequiredPlaceholders = function ()
+{
+    var allPlaceholders = PlaceholderUtils.getPlaceholders();
+    var plainText = this.toString();
+    var usedPlaceholders = [];
+    for (let i = 0, l = allPlaceholders.length; i < l; i++)
+    {
+        if (plainText.indexOf(allPlaceholders[i]) !== -1)
+            usedPlaceholders.push(allPlaceholders[i]);
+    }
+    return usedPlaceholders; 
+}
+
+/**
+ * Returns the template content with replaced placeholders by choosing the right
+ * replace function for the type.
+ * 
+ * @param {Object} pReplacements map, the structure is {placeholder : value}
+ * 
+ * @return {String} the replaced content
+ */
+DocumentTemplate.prototype.getReplacedContent = function (pReplacements)
+{
+    switch (this.type)
+    {
+        case DocumentTemplate.types.HTML:
+            for (let i in pReplacements)
+                pReplacements[i] = text.replaceAll(pReplacements[i], {"\n" : "<br>"});
+        case DocumentTemplate.types.TXT:
+            let decodedContent = util.decodeBase64String(this.content)
+            return DocumentTemplate._replaceText(decodedContent, pReplacements);
+        case DocumentTemplate.types.EML:
+            return this._getReplacedEML(pReplacements);
+        case DocumentTemplate.types.ODT:
+            return this._getReplacedODT(pReplacements);
+        case DocumentTemplate.types.DOCX:
+            return this._getReplacedDOCX(pReplacements);
+        case DocumentTemplate.types.PLAIN:
+            return DocumentTemplate._replaceText(this.content, pReplacements);
+        default:
+            return null;
+    }
+}
+
+/**
+ * replaces the placeholders with data from one contact and returns the result
+ */
+DocumentTemplate.prototype.getReplacedContentByContactId = function (pContactId) 
+{
+    var replacements = this._getReplacementsByContactIds([pContactId]); 
+    return this.getReplacedContent(replacements[pContactId]);
+}
+
+/**
+ * replaces the placeholders with data from the contacts and returns the result
+ * 
+ * @param {Array} pContactIds contact ids
+ * 
+ * @return {Object} replaced content for every contactId
+ */
+DocumentTemplate.prototype.getReplacedContentByContactIds = function (pContactIds) 
+{
+    var replacements = this._getReplacementsByContactIds(pContactIds);
+    var contents = {};
+    for (let contactId in replacements)
+    {
+        contents[contactId] = this.getReplacedContent(replacements[contactId]);
+    }
+    return contents;
+}
+
+/**
+ * Replaces the placeholders with data from the contacts and returns a serial letter, works
+ * only for ODT
+ * 
+ * @param {Array} pContactIds contact ids
+ * 
+ * @return {Object} the content of the replaced ODT
+ */
+DocumentTemplate.prototype.getSerialLetterByContactIds = function (pContactIds)
+{
+    var replacements = this._getReplacementsByContactIds(pContactIds);
+    if (this.type == DocumentTemplate.types.ODT)
+    {
+        let replaceArray = [];
+        for (let i = 0, l = pContactIds.length; i < l; i++)
+            for (let ii = 0; ii < 1000; ii++)
+            replaceArray.push(replacements[pContactIds[i]]);
+        return this._getReplacedODT(replaceArray);
+    }
+    return null;
+}
+
+/**
+ * Replaces the placeholders with data from the contacts and returns the resulting Emails.
+ * 
+ * @param {Array} pContactIds contact ids
+ * 
+ * @return {Object} Object containing the contact ids as keys and the corresponding Email
+ *                   objects as values
+ */
+DocumentTemplate.prototype.getReplacedEmailsByContactIds = function (pContactIds) 
+{
+    var replacements = this._getReplacementsByContactIds(pContactIds);
+    var emailObj = {};
+    for (let contactId in replacements)
+    {
+        if (this.type == DocumentTemplate.types.EML)
+        {
+            //use the special function for EML to also fill subject and sender
+            emailObj[contactId] = this._getReplacedEML(replacements[contactId], true);
+        }
+        else
+        {
+            let body = this.getReplacedContent(replacements[contactId]);
+            if (this.type == DocumentTemplate.types.TXT)
+                body = text.replaceAll(body, {"\n" : "<br>"});
+            emailObj[contactId] = new Email(null, null, null, body);
+        }
+    }
+    return emailObj;
+}
+
+/**
+ * Builds an object with the placeholder data for multiple contacts
+ * 
+ * @param {Array} pContactIds contact ids
+ * 
+ * @return {Object} Object containing the data. The structure is like {contactId : {placeholderName : replacementValue, ...}, ...}
+ * 
+ * @private
+ */
+DocumentTemplate.prototype._getReplacementsByContactIds = function (pContactIds)
+{
+    var config = this._getRequiredPlaceholders();
+    var contactIdPlaceholder = new Placeholder("contactId", Placeholder.types.SQLPART, "CONTACT.CONTACTID");
+    config = [contactIdPlaceholder].concat(config);
+    var addressData = getAddressesData(pContactIds, config, EmployeeUtils.getCurrentContactId()); //TODO: add sender selection
+    var replacements = {};
+    var placeholderNames = addressData[0];
+    var contactIdIndex = placeholderNames.indexOf(contactIdPlaceholder.toString());
+    for (let i = 1; i < addressData.length; i++)
+    {
+        let contactId = addressData[i][contactIdIndex];
+        for (let ii = 0, ll = placeholderNames.length; ii < ll; ii++)
+        {
+            if (!(contactId in replacements))
+                replacements[contactId] = {};
+            replacements[contactId][placeholderNames[ii]] = addressData[i][ii];
+        }
+    }
+    return replacements;
+}
+
+/**
+ * Replaces placeholders for EML
+ * 
+ * @param {Object} pReplacements mapping with replacements for every placeholder
+ * @param {boolean} [pGetEmail] if true, return an Email object (use this if the sender and subject are required)
+ * 
+ * @return {String|Email} the replaced content
+ * 
+ * @private
+ */
+DocumentTemplate.prototype._getReplacedEML = function (pReplacements, pGetEmail)
+{
+    var mailData = mail.parseRFC(util.decodeBase64String(this.content));
+    var email;
+    var body = DocumentTemplate._replaceText(mailData[mail.MAIL_HTMLTEXT], pReplacements);
+    if (pGetEmail)
+    {
+        var sender = DocumentTemplate._replaceText(mailData[mail.MAIL_SENDER], pReplacements);
+        var subject = DocumentTemplate._replaceText(mailData[mail.MAIL_SUBJECT], pReplacements);
+        email = new Email(null, sender, subject, body);
+    }
+    else
+        email = body;
+    return email;
+}
+
+/*
+ * replaces a given Odt-File on the server and returns the replaced base64-file
+ *
+ * @param {Object} pReplacements map of placeholders and replacements
+ *
+ * @return {String} base64-encoded replaced file
+ * 
+ * @private
+ */
+DocumentTemplate.prototype._getReplacedODT = function (pReplacements)
+{
+    //save the file on the server so it can be unzipped via pack.getFromZip
+    var serverFilePath = vars.get("$sys.servertemp") + "/clientid_" + vars.get("$sys.clientid")
+        + "/" + util.getNewUUID() + "/" + this.filename.replace(/\\/g, "/");
+    
+    fileIO.storeData(serverFilePath, this.content, util.DATA_BINARY, false);
+    if (!_replaceODTFile(pReplacements, serverFilePath))
+        return null;
+
+    var replacedFileData = fileIO.getData(serverFilePath, util.DATA_BINARY);
+    fileIO.remove(serverFilePath);
+
+    return replacedFileData;
+    
+    /**
+    * replaces placeholders in a odt file
+    *
+    * @param {Object} pReplacements replacement object
+    * @param {String} pODTFileName filename of the odt
+    * 
+    * @return {Boolean}
+    */
+    function _replaceODTFile (pReplacements, pODTFileName)
+    {
+        var senderRelId = EmployeeUtils.getCurrentContactId();
+        if (senderRelId == null)
+            return false;
+        if (pReplacements.length === undefined)
+            pReplacements = [pReplacements];
+        if (pReplacements.length !== 0)
+        {
+            //replace placeholders in content.xml
+            var contentXml = util.decodeBase64String(pack.getFromZip(pODTFileName, "content.xml"));
+            var bodybegin = contentXml.indexOf("<office:body>");
+            var bodyend =  contentXml.indexOf("</office:body>") + 14;
+            
+            var bodyTemplate = contentXml.substring(bodybegin, bodyend);
+            var fullBody = "";  //body that contains all pages (required when the replacing is done for several contacts)
+            var beforeBody = contentXml.substring(0, bodybegin);
+            var afterBody = contentXml.substr(bodyend);
+            
+            for (let i = 0, l = pReplacements.length; i < l; i++)
+            {
+                let replacements = pReplacements[i];
+                let currentBody = bodyTemplate;
+            
+                /* This only works if the text of the placeholders in the odt were not edited since they were written.
+                 * If you edit the odt and change a placeholder (for example: you change '{@addres@}' to '{@address@}'),
+                 * the text is saved in different XML tags and won't be replaced correctly.
+                 */
+
+                for (let placeholder in replacements)
+                {
+                    currentBody = currentBody.replace(placeholder,
+                        replacements[placeholder].replace(/\n/ig, "<text:line-break/>").replace(/&/ig, "&amp;"), "ig");
+                }
+                
+                fullBody += currentBody;
+            }
+            //TODO: TableData
+//            for (ti = 0; ti < pTableData.length; ti++)
+//                {
+//                    var tablepos = bulkbody.indexOf( getDefaultODTplaceholer(pTableData[ti].Table, true));
+//                    if ( tablepos != -1 )
+//                    {
+//                        var tablebegin = bulkbody.lastIndexOf("<table:table-row", tablepos);
+//                        var tableend =  bulkbody.indexOf("</table:table-row>", tablepos ) + 18;
+//                        var lasttable =  bulkbody.substr( tableend );
+//                        var tablerow = bulkbody.substring( tablebegin, tableend );
+//                        bulkbody = bulkbody.substring( 0, tablebegin );
+//                        var tabledata = pTableData[ti].TableData[addrdata[i][0]];
+//                        if ( tabledata != undefined )
+//                        {
+//                            for (var tz = 0; tz < tabledata.length; tz++)
+//                            {
+//                                var table = tablerow;
+//                                bulkbody += relaceAdditionValues( table, pTableData[ti].Fields, tabledata[tz], pTableData[ti].Table + "." );
+//                            }
+//                        }
+//                        bulkbody += lasttable;
+//                }
+                
+                
+            contentXml = beforeBody + fullBody + afterBody;
+            pack.addToZip(pODTFileName, "content.xml", util.encodeBase64String(contentXml));
+            
+            //replace placeholders in styles.xml
+            var styles = util.decodeBase64String(pack.getFromZip(pODTFileName, "styles.xml"));
+            for (let placeholder in pReplacements[0])
+            {
+                styles = styles.replace(placeholder,
+                    pReplacements[0][placeholder].replace(/\n/ig, "<text:line-break/>").replace(/&/ig, "&amp;"), "ig");
+            }
+            pack.addToZip(pODTFileName, "styles.xml", util.encodeBase64String(styles));
+            return true;
+        }
+        return false;
+    }
+}
+
+/*
+ * This function is used to replace placeholders via DocXTemplater
+ * 
+ * @param {Object} pReplacements - Must contain an object, which holds the placeholders
+ * 
+ * @return {String} returns the modified document in a BASE64 coded string
+ * 
+ * @private
+ */
+DocumentTemplate.prototype._getReplacedDOCX = function (pReplacements)
+{
+    var replacements = {};
+    for (let placeholder in pReplacements)  //removes the prefix and postfix, the process needs it like this
+        replacements[placeholder.slice(3, -3)] = pReplacements[placeholder];
+    
+    //this is executed as a process because of better performance
+    var documentData = process.execute("getDocxDocument_serverProcess", {
+        templateb64: this.content,
+        placeholderConfig: JSON.stringify(replacements) //process.execute is only able to handle strings
+    });
+
+    return documentData;
+}
+
+/**
+ * functions for working with letters (mails)
+ */
+function LetterUtils () {}
+
+LetterUtils.openNewLetter = function (pContactId)
+{
+    var params = {
+        "ContactId_param" : pContactId
+    };
+    neon.openContext("Letter", "LetterEdit_view", null, neon.OPERATINGSTATE_NEW, params);
 }
\ No newline at end of file
diff --git a/process/buildSerialLetter_serverProcess/buildSerialLetter_serverProcess.aod b/process/buildSerialLetter_serverProcess/buildSerialLetter_serverProcess.aod
new file mode 100644
index 0000000000000000000000000000000000000000..0cfcaabc36e1978d969d6e4543408803fb9d1004
--- /dev/null
+++ b/process/buildSerialLetter_serverProcess/buildSerialLetter_serverProcess.aod
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>buildSerialLetter_serverProcess</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/buildSerialLetter_serverProcess/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/buildSerialLetter_serverProcess/process.js b/process/buildSerialLetter_serverProcess/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ad448bad7ed33c636a69df3b55c9798fc76dcad
--- /dev/null
+++ b/process/buildSerialLetter_serverProcess/process.js
@@ -0,0 +1,25 @@
+import("system.logging");
+import("system.result");
+import("system.vars");
+import("KeywordRegistry_basic");
+import("Contact_lib");
+import("Sql_lib");
+import("system.db");
+import("system.neon");
+import("DocumentTemplate_lib");
+
+var letterId = vars.get("$local.serialLetterId");
+var templateId = db.cell(SqlCondition.begin()
+    .andPrepare("SERIALLETTER.SERIALLETTERID", letterId)
+    .buildSql("select DOCUMENTTEMPLATE_ID from SERIALLETTER", "1=2"));
+
+var template = DocumentTemplate.loadTemplate(templateId);
+var contactIds = db.table(SqlCondition.begin()
+    .andPrepare("LETTERRECIPIENT.SERIALLETTER_ID", letterId)
+    .andSqlCondition(ContactUtils.getCommRestrictionCondition($KeywordRegistry.communicationMediumCampaign$letter(), true))
+    .buildSql("select CONTACT_ID from LETTERRECIPIENT join CONTACT on LETTERRECIPIENT.CONTACT_ID = CONTACT.CONTACTID", "1=2")
+);
+var document = template.getSerialLetterByContactIds(contactIds);
+logging.log("ja")
+if (document)
+    neon.download(document, template.filename);
\ No newline at end of file
diff --git a/process/sendBulkMail_serverProcess/process.js b/process/sendBulkMail_serverProcess/process.js
index 0403cb3f69f8223ece7216225bf9f0518720fa78..68c0371b37a3721cd13e0aef3113a8e170034ecd 100644
--- a/process/sendBulkMail_serverProcess/process.js
+++ b/process/sendBulkMail_serverProcess/process.js
@@ -1,25 +1,25 @@
-import("system.datetime");
-import("Sql_lib");
-import("system.db");
-import("system.util");
-import("system.translate");
-import("Bulkmail_lib");
-import("system.vars");
-import("system.notification");
-
-var startTime = datetime.date();
-var bulkMailId = vars.get("$local.bulkMailId");
-var user = vars.get("$local.user");
-var res = BulkMailUtils.sendBulkMail(bulkMailId);
-
-if (user)
-{
-    var mailName = db.cell(SqlCondition.begin()
-        .andPrepare("BULKMAIL.BULKMAILID", bulkMailId)
-        .buildSql("select NAME from BULKMAIL")
-    );
-    var message = translate.withArguments("Bulk mail \"%0\" was sent!", [mailName]);
-    var description = translate.withArguments("%0 mails sent sucessfully, %1 mails failed. Process took %2 s.", 
-        [res.sucessful, res.failed, Math.round((datetime.date() - startTime) / datetime.ONE_SECOND)]);
-    notification.addNotification(util.getNewUUID(), null, null, null, "BulkMailSent", notification.PRIO_NORMAL, 2, notification.STATE_UNSEEN, [user], message, description);
+import("system.datetime");
+import("Sql_lib");
+import("system.db");
+import("system.util");
+import("system.translate");
+import("Bulkmail_lib");
+import("system.vars");
+import("system.notification");
+
+var startTime = datetime.date();
+var bulkMailId = vars.get("$local.bulkMailId");
+var user = vars.get("$local.user");
+var res = BulkMailUtils.sendBulkMail(bulkMailId);
+
+if (user)
+{
+    var mailName = db.cell(SqlCondition.begin()
+        .andPrepare("BULKMAIL.BULKMAILID", bulkMailId)
+        .buildSql("select NAME from BULKMAIL")
+    );
+    var message = translate.withArguments("Bulk mail \"%0\" was sent!", [mailName]);
+    var description = translate.withArguments("%0 mails sent sucessfully, %1 mails failed. Process took %2 s.", 
+        [res.sucessful, res.failed, Math.round((datetime.date() - startTime) / datetime.ONE_SECOND)]);
+    notification.addNotification(util.getNewUUID(), null, null, null, "BulkMailSent", notification.PRIO_NORMAL, 2, notification.STATE_UNSEEN, [user], message, description);
 }
\ No newline at end of file