From fa19e5ebcea9164ffeb9a6a05e55a7a4dc0f51be Mon Sep 17 00:00:00 2001 From: yuri Date: Thu, 10 Sep 2015 17:27:27 +0300 Subject: [PATCH] dev --- .../Core/Mail/Mail/Header/XQueueItemId.php | 53 ++++++++++++++++ application/Espo/Core/Mail/Sender.php | 4 +- application/Espo/Core/defaults/config.php | 1 + .../Espo/Core/defaults/systemConfig.php | 3 +- .../i18n/en_US/CampaignLogRecord.json | 3 +- .../Resources/i18n/en_US/EmailQueueItem.json | 3 +- .../entityDefs/CampaignLogRecord.json | 8 +++ .../metadata/entityDefs/EmailQueueItem.json | 4 ++ .../Espo/Modules/Crm/Services/Campaign.php | 48 +++++++++++++++ .../Espo/Modules/Crm/Services/MassEmail.php | 60 +++++++++++++++++-- application/Espo/ORM/Repositories/RDB.php | 4 +- .../Espo/Resources/i18n/en_US/Settings.json | 6 +- .../layouts/Settings/outboundEmails.json | 6 ++ .../metadata/entityDefs/Settings.json | 4 ++ application/Espo/Services/InboundEmail.php | 54 +++++++++++++++++ .../views/campaign-log-record/fields/data.js | 12 +++- 16 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 application/Espo/Core/Mail/Mail/Header/XQueueItemId.php diff --git a/application/Espo/Core/Mail/Mail/Header/XQueueItemId.php b/application/Espo/Core/Mail/Mail/Header/XQueueItemId.php new file mode 100644 index 0000000000..e4378ef1b1 --- /dev/null +++ b/application/Espo/Core/Mail/Mail/Header/XQueueItemId.php @@ -0,0 +1,53 @@ +fieldName; + } + + public function setFieldName($value) + { + } + + public function setEncoding($encoding) + { + return $this; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getEncoding() + { + return 'ASCII'; + } + + public function toString() + { + return $this->fieldName . ': ' . $this->getFieldValue(); + } + + public function getFieldValue($format = \Zend\Mail\Header\HeaderInterface::FORMAT_RAW) + { + return $this->id; + } +} diff --git a/application/Espo/Core/Mail/Sender.php b/application/Espo/Core/Mail/Sender.php index 45a56d6f45..76591e059e 100644 --- a/application/Espo/Core/Mail/Sender.php +++ b/application/Espo/Core/Mail/Sender.php @@ -133,7 +133,9 @@ class Sender public function send(Email $email, $params = array(), &$message = null, $attachmetList = []) { - $message = new Message(); + if (!$message) { + $message = new Message(); + } $config = $this->config; $params = $this->params + $params; diff --git a/application/Espo/Core/defaults/config.php b/application/Espo/Core/defaults/config.php index 8ba084813e..f1b5a0ad88 100644 --- a/application/Espo/Core/defaults/config.php +++ b/application/Espo/Core/defaults/config.php @@ -103,6 +103,7 @@ return array ( 'b2cMode' => false, 'restrictedMode' => false, 'theme' => 'Espo', + 'massEmailMaxPerHourCount' => 100, 'isInstalled' => false, ); diff --git a/application/Espo/Core/defaults/systemConfig.php b/application/Espo/Core/defaults/systemConfig.php index d31ac23cdb..4bb9545da1 100644 --- a/application/Espo/Core/defaults/systemConfig.php +++ b/application/Espo/Core/defaults/systemConfig.php @@ -116,7 +116,8 @@ return array ( 'ldapTryUsernameSplit', 'ldapOptReferrals', 'ldapCreateEspoUser', - 'maxEmailAccountCount' + 'maxEmailAccountCount', + 'massEmailMaxPerHourCount' ), 'isInstalled' => false, ); diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/CampaignLogRecord.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/CampaignLogRecord.json index 67f75dbc6a..ea359bc648 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/CampaignLogRecord.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/CampaignLogRecord.json @@ -6,7 +6,8 @@ "campaign": "Campaign", "parent": "Target", "object": "Object", - "application": "Application" + "application": "Application", + "queueItem": "Queue Item" }, "options": { "action": { diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/EmailQueueItem.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/EmailQueueItem.json index cdcee57526..b51847c004 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/EmailQueueItem.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/EmailQueueItem.json @@ -4,7 +4,8 @@ "status": "Status", "target": "Target", "sentAt": "Date Sent", - "attemptCount": "Attempts" + "attemptCount": "Attempts", + "emailAddress": "Email Address" }, "links": { "target": "Target", diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/CampaignLogRecord.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/CampaignLogRecord.json index fa8f1536e7..e8620782e7 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/CampaignLogRecord.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/CampaignLogRecord.json @@ -44,6 +44,9 @@ }, "object": { "type": "linkParent" + }, + "queueItem": { + "type": "link" } }, "links": { @@ -56,6 +59,11 @@ "entity": "Campaign", "foreign": "campaignLogRecords" }, + "queueItem": { + "type": "belongsTo", + "entity": "EmailQueueItem", + "noJoin": true + }, "parent": { "type": "belongsToParent", "entityList": ["Account", "Contact", "Lead", "Opportunity", "User"] diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/EmailQueueItem.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/EmailQueueItem.json index 9fe9d78dc6..6c5ba2c581 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/EmailQueueItem.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/EmailQueueItem.json @@ -21,6 +21,10 @@ "sentAt": { "type": "datetime", "readOnly": true + }, + "emailAddress": { + "type": "varchar", + "readOnly": true } }, "links": { diff --git a/application/Espo/Modules/Crm/Services/Campaign.php b/application/Espo/Modules/Crm/Services/Campaign.php index f23eed239b..871cc6dc5e 100644 --- a/application/Espo/Modules/Crm/Services/Campaign.php +++ b/application/Espo/Modules/Crm/Services/Campaign.php @@ -95,5 +95,53 @@ class Campaign extends \Espo\Services\Record } } + public function logSent($campaignId, $queueItemId = null, Entity $target, Entity $emailOrEmailTemplate = null, $emailAddress, $actionDate = null) + { + if (empty($actionDate)) { + $actionDate = date('Y-m-d H:i:s'); + } + $logRecord = $this->getEntityManager()->getEntity('CampaignLogRecord'); + $logRecord->set(array( + 'campaignId' => $campaignId, + 'actionDate' => $actionDate, + 'parentId' => $target->id, + 'parentType' => $target->getEntityType(), + 'action' => 'Sent', + 'stringData' => $emailAddress, + 'queueItemId' => $queueItemId + )); + + if ($emailOrEmailTemplate) { + $logRecord->set(array( + 'objectId' => $emailOrEmailTemplate->id, + 'objectType' => $emailOrEmailTemplate->getEntityType() + )); + } + $this->getEntityManager()->saveEntity($logRecord); + } + + public function logBounced($campaignId, $queueItemId = null, Entity $target, $emailAddress, $isHard = false, $actionDate = null) + { + if (empty($actionDate)) { + $actionDate = date('Y-m-d H:i:s'); + } + $logRecord = $this->getEntityManager()->getEntity('CampaignLogRecord'); + $logRecord->set(array( + 'campaignId' => $campaignId, + 'actionDate' => $actionDate, + 'parentId' => $target->id, + 'parentType' => $target->getEntityType(), + 'action' => 'Bounced', + 'stringData' => $emailAddress, + 'queueItemId' => $queueItemId + )); + if ($isHard) { + $logRecord->set('stringAdditionalData', 'Hard'); + } else { + $logRecord->set('stringAdditionalData', 'Soft'); + } + $this->getEntityManager()->saveEntity($logRecord); + } + } diff --git a/application/Espo/Modules/Crm/Services/MassEmail.php b/application/Espo/Modules/Crm/Services/MassEmail.php index ac2cfcee9f..49c34d8e2b 100644 --- a/application/Espo/Modules/Crm/Services/MassEmail.php +++ b/application/Espo/Modules/Crm/Services/MassEmail.php @@ -30,6 +30,12 @@ class MassEmail extends \Espo\Services\Record { const MAX_ATTEMPT_COUNT = 3; + const MAX_PER_HOUR_COUNT = 100; + + private $campaignService = null; + + private $emailTemplateService = null; + protected function init() { $this->dependencies[] = 'container'; @@ -138,10 +144,25 @@ class MassEmail extends \Espo\Services\Record public function processSending(Entity $massEmail) { + $threshold = new \DateTime(); + $threshold->modify('-1 hour'); + $sentLastHourCount = $this->getEntityManager()->getRepository('EmailQueueItem')->where(array( + 'status' => 'Sent', + 'sentAt>' => $threshold->format('Y-m-d H:i:s') + ))->count(); + + $maxBatchSize = $this->getConfig()->get('massEmailMaxPerHourCount', self::MAX_PER_HOUR_COUNT); + + if ($sentLastHourCount >= $maxBatchSize) { + return; + } + + $maxBatchSize = $maxBatchSize - $sentLastHourCount; + $queueItemList = $this->getEntityManager()->getRepository('EmailQueueItem')->where(array( 'status' => 'Pending', 'massEmailId' => $massEmail->id - ))->find(); + ))->limit(0, $maxBatchSize)->find(); $templateId = $massEmail->get('emailTemplateId'); if (!$templateId) { @@ -149,6 +170,11 @@ class MassEmail extends \Espo\Services\Record return; } + $campaign = null; + $campaignId = $massEmail->get('campaignId'); + if ($campaignId) { + $campaign = $this->getEntityManager()->getEntity('Campaign', $campaignId); + } $emailTemplate = $this->getEntityManager()->getEntity('EmailTemplate', $templateId); if (!$emailTemplate) { $this->setFailed($massEmail); @@ -157,7 +183,7 @@ class MassEmail extends \Espo\Services\Record $attachmetList = $emailTemplate->get('attachmets'); foreach ($queueItemList as $queueItem) { - $this->sendQueueItem($queueItem, $massEmail, $emailTemplate, $attachmetList); + $this->sendQueueItem($queueItem, $massEmail, $emailTemplate, $attachmetList, $campaign); } $countLeft = $this->getEntityManager()->getRepository('EmailQueueItem')->where(array( @@ -170,7 +196,7 @@ class MassEmail extends \Espo\Services\Record } } - protected function sendQueueItem(Entity $queueItem, Entity $massEmail, Entity $emailTemplate, $attachmetList = []) + protected function sendQueueItem(Entity $queueItem, Entity $massEmail, Entity $emailTemplate, $attachmetList = [], $campaign = null) { $target = $this->getEntityManager()->getEntity($queueItem->get('targetType'), $queueItem->get('targetId')); if (!$target || !$target->id || !$target->get('emailAddress')) { @@ -214,12 +240,30 @@ class MassEmail extends \Espo\Services\Record $attemptCount++; $queueItem->set('attemptCount', $attemptCount); - $message = false; + $message = new \Zend\Mail\Message(); + + $header = new \Espo\Core\Mail\Mail\Header\XQueueItemId(); + $header->setId($queueItem->id); + $message->getHeaders()->addHeader($header); + $this->getMailSender()->useGlobal()->send($email, $params, $message, $attachmetList); + $emailObject = $emailTemplate; + if ($massEmail->get('storeSentEmails')) { + $this->getEntityManager()->saveEntity($email); + $emailObject = $email; + } + + $queueItem->set('emailAddress', $target->get('emailAddress')); + $queueItem->set('status', 'Sent'); $queueItem->set('sentAt', date('Y-m-d H:i:s')); $this->getEntityManager()->saveEntity($queueItem); + + if ($campaign) { + $this->getCampaignService()->logSent($campaign->id, $queueItem->id, $target, $emailObject, $target->get('emailAddress')); + } + } catch (\Exception $e) { if ($queueItem->get('attemptCount') >= self::MAX_ATTEMPT_COUNT) { $queueItem->set('status', 'Failed'); @@ -239,5 +283,13 @@ class MassEmail extends \Espo\Services\Record } return $this->emailTemplateService; } + + protected function getCampaignService() + { + if (!$this->campaignService) { + $this->campaignService = $this->getServiceFactory()->create('Campaign'); + } + return $this->campaignService; + } } diff --git a/application/Espo/ORM/Repositories/RDB.php b/application/Espo/ORM/Repositories/RDB.php index 23316d4f12..6a7bb22081 100644 --- a/application/Espo/ORM/Repositories/RDB.php +++ b/application/Espo/ORM/Repositories/RDB.php @@ -308,7 +308,9 @@ class RDB extends \Espo\ORM\Repository $this->handleSelectParams($params); $params = $this->getSelectParams($params); - return $this->getMapper()->count($this->seed, $params); + $count = $this->getMapper()->count($this->seed, $params); + $this->reset(); + return $count; } public function max($field) diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index da5b9dca18..80027b36d0 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -55,7 +55,8 @@ "displayListViewRecordCount": "Display Total Count (on List View)", "theme": "Theme", "userThemesDisabled": "Disable User Themes", - "emailMessageMaxSize": "Email Max Size (Mb)" + "emailMessageMaxSize": "Email Max Size (Mb)", + "massEmailMaxPerHourCount": "Max count of emails sent per hour" }, "options": { "weekStart": { @@ -77,6 +78,7 @@ "In-app Notifications": "In-app Notifications", "Email Notifications": "Email Notifications", "Currency Settings": "Currency Settings", - "Currency Rates": "Currency Rates" + "Currency Rates": "Currency Rates", + "Mass Email" } } diff --git a/application/Espo/Resources/layouts/Settings/outboundEmails.json b/application/Espo/Resources/layouts/Settings/outboundEmails.json index 4e9e2c6c91..74cc93ac79 100644 --- a/application/Espo/Resources/layouts/Settings/outboundEmails.json +++ b/application/Espo/Resources/layouts/Settings/outboundEmails.json @@ -14,5 +14,11 @@ [{"name": "outboundEmailFromAddress"}, {"name": "outboundEmailIsShared"}], [{"name": "outboundEmailFromName"}, false] ] + }, + { + "label": "Mass Email", + "rows": [ + [{"name": "massEmailMaxPerHourCount"}, false] + ] } ] diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index f9fa0d3ebb..a44b64fa24 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -252,6 +252,10 @@ "type": "float", "min": 0, "tooltip": true + }, + "massEmailMaxPerHourCount": { + "type": "int", + "min": 0 } } } diff --git a/application/Espo/Services/InboundEmail.php b/application/Espo/Services/InboundEmail.php index a26b3018e1..07920ec97e 100644 --- a/application/Espo/Services/InboundEmail.php +++ b/application/Espo/Services/InboundEmail.php @@ -31,6 +31,8 @@ class InboundEmail extends \Espo\Services\Record { protected $internalFields = array('password'); + private $campaignService = null; + const PORTION_LIMIT = 20; public function createEntity($data) @@ -255,6 +257,11 @@ class InboundEmail extends \Espo\Services\Record $fromString = $message->from; if (preg_match('/MAILER-DAEMON|POSTMASTER/i', $fromString)) { $toSkip = true; + try { + $this->processBouncedMessage($message); + } catch (\Exception $e) { + $GLOBALS['log']->error('InboundEmail '.$emailAccount->id.' (Process Bounced Message: [' . $e->getCode() . '] ' .$e->getMessage()); + } } } if (!$toSkip) { @@ -551,5 +558,52 @@ class InboundEmail extends \Espo\Services\Record } catch (\Exception $e) {} } + + protected function processBouncedMessage(\Zend\Mail\Message $message) + { + $content = $message->getContent(); + echo "bounced\n"; + + $isHard = false; + if (preg_match('/permanent[ ]*error/', $content)) { + $isHard = true; + } + if (preg_match('/X-QueueItemId: [a-z0-9\-]*/', $content, $m)) { + $queueItemId = preg_split('/X-QueueItemId: /', $m[0], -1, \PREG_SPLIT_NO_EMPTY); + if (!$queueItemId) return; + echo $queueItemId . "\n"; + + $queueItem = $this->getEntityManager()->getEntity('EmailQueueItem', $queueItemId); + + if (!$queueItem) return; + + $campaignId = $queueItem->get('campaignId'); + + $targetType = $queueItem->get('targetType'); + $targetId = $queueItem->get('targetId'); + $target = $this->getEntityManager()->getEntity($targetType, $targetId); + + $emailAddress = $queueItem->get('emailAddress'); + + if ($isHard && $emailAddress) { + $emailAddressEntity = $this->getEntityManager()->getRepository('EmailAddress')->getByAddress($emailAddress); + $emailAddressEntity->set('invalid', true); + $this->getEntityManager()->saveEntity($emailAddressEntity); + } + + if ($campaignId && $target) { + $this->getCampaignService()->logBounced($campaignId, $queueItemId, $target, $emailAddress, $isHard); + } + } + } + + protected function getCampaignService() + { + if (!$this->campaignService) { + $this->campaignService = $this->getServiceFactory()->create('Campaign'); + } + return $this->campaignService; + } + } diff --git a/frontend/client/modules/crm/src/views/campaign-log-record/fields/data.js b/frontend/client/modules/crm/src/views/campaign-log-record/fields/data.js index 84dc82b665..364f10def0 100644 --- a/frontend/client/modules/crm/src/views/campaign-log-record/fields/data.js +++ b/frontend/client/modules/crm/src/views/campaign-log-record/fields/data.js @@ -31,11 +31,17 @@ Espo.define('Crm:Views.CampaignLogRecord.Fields.Data', 'Views.Fields.Base', func switch (action) { case 'Sent': - return this.model.get('stringData'); + if (this.model.get('objectId') && this.model.get('objectType') && this.model.get('objectName')) { + return ''+this.model.get('objectName')+''; + } + return this.model.get('stringData') || ''; case 'Clicked': - return '' + this.model.get('stringData') + ''; + if (this.model.get('objectId') && this.model.get('objectType') && this.model.get('objectName')) { + return ''+this.model.get('objectName')+''; + } + return '' + (this.model.get('stringData') || '') + ''; case 'Opted Out': - return '' + this.model.get('stringData') + ''; + return '' + this.model.get('stringData') + ''; case 'Bounced': var emailAddress = this.model.get('stringData'); var type = this.model.get('stringAdditionalData');