diff --git a/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js b/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js
index 4aa2fc2aac11e228e042b757019152b6ee802d5f..b71a637ad5f3e5f63751e823d8acaa560e4a7a87 100644
--- a/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js
+++ b/entity/Attribute_entity/recordcontainers/jdito/contentProcess.js
@@ -1,4 +1,5 @@
 import("system.logging");
+import("system.datetime");
 import("system.translate");
 import("Util_lib");
 import("JditoFilter_lib");
@@ -10,20 +11,24 @@ import("system.result");
 import("Sql_lib");
 import("Attribute_lib");
 
+var t = datetime.date()
+
 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");
-
-logging.log([getGroups, objectType, parentType])
+var fetchUsages = true;
 
 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 " 
-    + "from AB_ATTRIBUTE UIDROW "
-    + "left join AB_ATTRIBUTE PARENT1 on UIDROW.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID "    //always select the names of the next 3 parents so that less queries
-    + "left join AB_ATTRIBUTE PARENT2 ON PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID " //are required later when buildung the full name
-    + "left join AB_ATTRIBUTE PARENT3 ON PARENT2.ATTRIBUTE_PARENT_ID = PARENT3.AB_ATTRIBUTEID"; 
+    + ", '', 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";
 
@@ -69,18 +74,7 @@ else if (objectType)  //if there's an objectType, it comes from the AttributeRel
     else  // do not return anything, if parameter is there but an empty array
         condition.and("1=2");
 
-        
-} 
-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");
+     fetchUsages = false;   
 }
 
 //when there are filters selected, add them to the conditon
@@ -91,21 +85,26 @@ if (vars.exists("$local.filter") && vars.get("$local.filter"))
         condition.andSqlCondition(JditoFilterUtils.getSqlCondition(filter.filter, "AB_ATTRIBUTE", uidTableAlias));
 }
 
-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"));
-var usages = {};
-for (let i = 0, l = usageTbl.length; i < l; i++)
+var usages;
+if (fetchUsages) //this query is only necessary in Attribute, not in AttributeRelation
 {
-    let attrId = usageTbl[i][0];
-    if (attrId in usages)
-        usages[attrId].push(usageTbl[i][1]);
-    else
-        usages[attrId] = [usageTbl[i][1]];
+    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));
 
 
@@ -140,7 +139,7 @@ function _buildAttributeTable (pAttributes, pUsages)
     for (let i in rows)
     {
         let rowData = rows[i].data;
-        if (rowData[5].trim() != $AttributeTypes.COMBOVALUE && i in pUsages)
+        if (pUsages && rowData[5].trim() != $AttributeTypes.COMBOVALUE && i in pUsages)
         {
             rowData[7] = pUsages[i].map(function (usage)
             {
@@ -163,8 +162,17 @@ function _buildAttributeTable (pAttributes, pUsages)
  */
 function _getFullName (pAttributeName, pParent1Name, pParent2Name, pParent3Name, pParent4Id)
 {
-    var parent4FullName = pParent4Id ? AttributeUtil.getFullAttributeName(pParent4Id) : null;
+    var parent4FullName;
+    if (pParent4Id && pParent4Id in nameCache)
+        parent4FullName = nameCache[pParent4Id];
+    else
+    {
+        parent4FullName = pParent4Id ? AttributeUtil.getFullAttributeName(pParent4Id) : null;
+        nameCache[pParent4Id] = parent4FullName;
+    }
     pAttributeName = ArrayUtils.joinNonEmptyFields([parent4FullName, pParent3Name, pParent2Name, pParent1Name, pAttributeName], " / ");
     
     return pAttributeName;
-}
\ No newline at end of file
+}
+
+logging.log(datetime.date() - t)
\ No newline at end of file
diff --git a/entity/Offer_entity/entityfields/lettersalutation/valueProcess.js b/entity/Offer_entity/entityfields/lettersalutation/valueProcess.js
index 4fcb6ca5a0ce8770d33d6201b8822ebca6553037..0dd257f7f36ea76b1dea44e0405d5105a408a75d 100644
--- a/entity/Offer_entity/entityfields/lettersalutation/valueProcess.js
+++ b/entity/Offer_entity/entityfields/lettersalutation/valueProcess.js
@@ -2,7 +2,7 @@ import("system.logging");
 import("system.result");
 import("system.neon");
 import("system.vars");
-import("PostalAddress_lib");
+import("Address_lib");
 
 if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW && !vars.get("$this.value")) 
 {
diff --git a/process/Bulkmail_lib/Bulkmail_lib.aod b/process/Bulkmail_lib/Bulkmail_lib.aod
index e02358029db1060e0396ba5d77f7b402cf4d7d27..3181ac4567c83ec13a2f9438ce34c5c9957bc113 100644
--- a/process/Bulkmail_lib/Bulkmail_lib.aod
+++ b/process/Bulkmail_lib/Bulkmail_lib.aod
@@ -1,6 +1,6 @@
 <?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>Bulkmail_lib</name>
+  <name>BulkMail_lib</name>
   <majorModelMode>DISTRIBUTED</majorModelMode>
   <process>%aditoprj%/process/Bulkmail_lib/process.js</process>
   <variants>
diff --git a/process/Bulkmail_lib/process.js b/process/Bulkmail_lib/process.js
index 27a4b8362f5d4c49ca9a2094eb1fab7919eddaa8..49290ffaef9ab4092b94476882052e1aaf7b0865 100644
--- a/process/Bulkmail_lib/process.js
+++ b/process/Bulkmail_lib/process.js
@@ -1,6 +1,52 @@
+import("KeywordRegistry_basic");
+import("Sql_lib");
+import("system.db");
 import("DocumentTemplate_lib");
+import("Email_lib");
 
-function Bulkmail ()
+
+function BulkMailUtils () {}
+
+BulkMailUtils.sendBulkMail = function (pBulkMailId)
 {
+    var templateId = db.cell(SqlCondition.begin()
+        .andPrepare("BULKMAIL.BULKMAILID", pBulkMailId)
+        .buildSql("select DOCUMENTTEMPLATE_ID from BULKMAIL", "1=2")
+    );
+    var template = DocumentTemplate.loadTemplate(templateId);
+    var emailSender;
+    
+    var recipientData = db.table(SqlCondition.begin()
+        .andPrepare("BULKMAILRECIPIENT.BULKMAIL_ID", pBulkMailId)
+        //TODO: more condition
+        .buildSql("select BULKMAILRECIPIENTID, CONTACT_ID, '' from BULKMAILRECIPIENT", "1=2")
+    );
+    
+    var successIds = [];
+    var failedIds = [];
+    var mails = template.getReplacedEmailsByContactIds(recipientData.map(function (e) {return e[0];}));
+    for (let i = 0, l = recipientData.length; i < l; i++)
+    {
+        let contactId = recipientData[i][1];
+        let email = mails[contactId];
+        email.toRecipients = [recipientData[i][1]];
+        email.sender = emailSender;
+        
+        let 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"], null, [$KeywordRegistry.bulkMailSentStatus$sent()], 
+        SqlCondition.begin()
+            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", successIds)
+            .build("1=2")
+    );
+    db.updateData("BULKMAILRECIPIENT", ["STATUS"], null, [$KeywordRegistry.bulkMailSentStatus$failed()], 
+        SqlCondition.begin()
+            .andIn("BULKMAILRECIPIENT.BULKMAILRECIPIENTID", failedIds)
+            .build("1=2")
+    );
     
 }
\ No newline at end of file
diff --git a/process/DocumentTemplate_lib/process.js b/process/DocumentTemplate_lib/process.js
index 2cbeffcce9962843b8873fbbab9a2db3a4efd75d..d4c01e318b614e7fedbb43822aac0bdf8c1db096 100644
--- a/process/DocumentTemplate_lib/process.js
+++ b/process/DocumentTemplate_lib/process.js
@@ -1,4 +1,3 @@
-import("system.logging");
 import("Communication_lib");
 import("system.neon");
 import("Employee_lib");
@@ -147,67 +146,103 @@ DocumentTemplate.prototype.getReplacedContent = function (pReplacements)
 /**
  * replaces the placeholders with data from the contact and returns the result
  */
-DocumentTemplate.prototype.getReplacedContentByContactId = function (pContactId)  //TODO: function required for mass replacing (for bulkmails)
+DocumentTemplate.prototype.getReplacedContentByContactId = function (pContactId) 
 {
-    var replacements = this._getReplacementsByContactId(pContactId); 
-    return this.getReplacedContent(replacements);
+    var replacements = this._getReplacementsByContactIds([pContactId]); 
+    return this.getReplacedContent(replacements[pContactId]);
 }
 
 /**
- * @private
+ * 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._getReplacementsByContactId = function (pContactId)
+DocumentTemplate.prototype.getReplacedContentByContactIds = function (pContactIds) 
 {
-    var config = this._getRequiredPlaceholders();
-    var addressData = getAddressesData([pContactId], config, EmployeeUtils.getCurrentContactId()); //TODO: add sender selection
-    var replacements = {};
-    for (let i = 0, l = addressData[0].length; i < l; i++)
+    var replacements = this._getReplacementsByContactIds(pContactIds);
+    var contents = {};
+    for (let contactId in replacements)
     {
-        replacements[addressData[0][i]] = addressData[1][i];
+        contents[contactId] = this.getReplacedContent(replacements[contactId]);
     }
-    return replacements;
+    return contents;
 }
 
 /**
- * loads data into an Email object from a template
+ * replaces the placeholders with data from the contacts and returns the result
  * 
- * @param {Email} pEmail email object to change
- * @param {String} pContactId contactId of the recipient
+ * @param {Array} pContactIds contact ids
+ * 
+ * @return {Object} emails for every contactId
  */
-DocumentTemplate.prototype.setEmailTemplateByContactId = function (pEmail, pContactId)
+DocumentTemplate.prototype.getReplacedEmailsByContactIds = function (pContactIds) 
 {
-    if (this.type != DocumentTemplate.types.EML && this.type != DocumentTemplate.types.HTML && this.type != DocumentTemplate.types.TXT)
-        throw new Error("Invalid document type for an email template");
-    
-    var replacements = DocumentTemplate._getReplacementsByContactId(pContactId);
-    
-    if (this.type == DocumentTemplate.types.EML) //special treatment because eml contains more information than just the body
+    var replacements = this._getReplacementsByContactIds(pContactIds);
+    var emailObj = {};
+    for (let contactId in replacements)
     {
-        let email = mail.parseRFC(util.decodeBase64String(this.content));
-        pEmail.sender = DocumentTemplate._replaceText(email[mail.MAIL_SENDER], replacements);
-        pEmail.subject = DocumentTemplate._replaceText(email[mail.MAIL_SUBJECT], replacements);
-        pEmail.body = DocumentTemplate._replaceText(email[mail.MAIL_HTMLTEXT], replacements);
+        if (this.type == DocumentTemplate.types.EML)
+            emailObj[contactId] = this._getReplacedEML(replacements[contactId], true);
+        else
+        {
+            let body = this.getReplacedContent(replacements[contactId]);
+            emailObj[contactId] = new Email(null, null, null, body);
+        }
     }
-    else
-        pEmail.body = this.getReplacedContent(replacements);
-    
+    return emailObj;
 }
 
 /**
- * Replaces placeholders for EML. This function only works for the email body,
- * so if you also need the subject, sender, etc use DocumentTemplate.prototype.setEmailTemplateByContactId.
+ * @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 Email object
  * 
- * @return {String} the replaced content
+ * @return {String|Email} the replaced content
  * 
  * @private
  */
-DocumentTemplate.prototype._getReplacedEML = function (pReplacements)
+DocumentTemplate.prototype._getReplacedEML = function (pReplacements, pGetEmail)
 {
-    var email = mail.parseRFC(util.decodeBase64String(this.content));
-    var htmlText = email[mail.MAIL_HTMLTEXT];
-    return DocumentTemplate._replaceText(htmlText, pReplacements);
+    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;
 }
 
 /*
@@ -297,7 +332,7 @@ DocumentTemplate.prototype._getReplacedODT = function (pReplacements)
 DocumentTemplate.prototype._getReplacedDOCX = function (pReplacements)
 {
     var replacements = {};
-    for (let placeholder in pReplacements)
+    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
diff --git a/process/Email_lib/process.js b/process/Email_lib/process.js
index 5047809e526692897da79d968059aad4a0325d89..bf786a48d2025baa41d5c9ba7045d26a5cc27c6e 100644
--- a/process/Email_lib/process.js
+++ b/process/Email_lib/process.js
@@ -24,9 +24,9 @@ function EmailUtils () {}
 EmailUtils.openMailTemplate = function (pToRecipients, pSenderContactId, pTemplateId, pRecipientContactId)
 {
     var email = new Email(pToRecipients);
-    email.setSender(pSenderContactId);
     if (pTemplateId)
         email.setTemplate(pTemplateId, pRecipientContactId);
+    email.setSender(pSenderContactId);
 
     email.downloadEML();
 }
@@ -74,13 +74,14 @@ function Email (pToRecipients, pSender, pSubject, pBody, pCcRecipients, pBccReci
  * 
  * @param {String} pTemplateId the id of the template
  * @param {String} pContactId the id of the template
- * 
- * @throws {Error} if the type of the template is invalid
  */
 Email.prototype.setTemplate = function (pTemplateId, pContactId)
 {
     var template = DocumentTemplate.loadTemplate(pTemplateId);
-    template.setEmailTemplateByContactId(this, pContactId);
+    var email = template.getReplacedEmailsByContactIds([pContactId])[pContactId];
+    this.sender = email.sender;
+    this.body = email.body;
+    this.subject = email.subject;
 }
 
 /**
@@ -186,4 +187,52 @@ Email.prototype.downloadEML = function()
 {
     neon.download(util.encodeBase64String(this.getRFCmail(), null), (this.subject || translate.text("Email Template")) + ".eml");
 }
-    
\ No newline at end of file
+
+/**
+ * sends the email object
+ * 
+ * @return {boolean} true, if the mail was sent sucessfully
+ */
+Email.prototype.send = function ()
+{
+    var ENCODING = "UTF-8";
+    var mailId;
+    try 
+    {
+        mailId = mail.newMail();
+    }
+    catch(ex)
+    {
+        //TODO: fix this dirty workaround [waiting for #1038963], since newMail causes an error on the first call after a user logged in
+        logging.log(ex);
+        util.sleep(1500);
+        mailId = mail.newMail();
+    }
+
+    if (this.toRecipients.length)
+        mail.addRecipients(mailId, mail.RECIPIENT_TO, this.toRecipients);
+
+    if (this.ccRecipients.length)
+        mail.addRecipients(mailId, mail.RECIPIENT_CC, this.ccRecipients);
+
+    if (this.bccRecipients.length)
+        mail.addRecipients(mailId, mail.RECIPIENT_BCC, this.bccRecipients);
+
+    if (this.subject)
+        mail.setSubject(mailId, this.subject, ENCODING);
+
+    if (this.body)
+        mail.addText(mailId, this.body, "text/html", ENCODING, null);
+    else
+        mail.addText(mailId, "", "text/html", ENCODING, null);
+    
+    try
+    {
+        mail.sendMail(mailId);
+        return true;
+    }
+    catch (ex)
+    {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/process/KeywordRegistry_basic/process.js b/process/KeywordRegistry_basic/process.js
index 3c087550b0af4dbc39812ac5b9f608ccf78bfd2c..276c521b0f52d3de505491a4868c559016d27c28 100644
--- a/process/KeywordRegistry_basic/process.js
+++ b/process/KeywordRegistry_basic/process.js
@@ -107,3 +107,8 @@ $KeywordRegistry.permissionCondType = function(){return "PermissionCondType";};
 $KeywordRegistry.permissionAccessType = function(){return "PermissionAccessType";};
 
 $KeywordRegistry.communicationMediumCampaign = function(){return "CommunicationMediumCampaign";};
+
+$KeywordRegistry.bulkMailSentStatus = function(){return "BulkMailSentStatus";};
+$KeywordRegistry.bulkMailSentStatus$pending = function(){return "9a0c5608-070e-49fb-92cd-f6abece9242d";};
+$KeywordRegistry.bulkMailSentStatus$sent = function(){return "147211fb-a1cf-49c8-8e08-c3cfe0404f9b";};
+$KeywordRegistry.bulkMailSentStatus$failed = function(){return "353e27e9-7491-4bfd-b9f9-f18f2cb2a36c";};
\ No newline at end of file