This commit is contained in:
yuri
2015-09-10 17:27:27 +03:00
parent b67e8960fa
commit fa19e5ebce
16 changed files with 259 additions and 14 deletions
@@ -0,0 +1,53 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Espo\Core\Mail\Mail\Header;
use Zend\Mime\Mime;
class XQueueItemId extends \Zend\Mail\Header\GenericHeader
{
protected $fieldName = 'X-QueueItemId';
protected $id = null;
public function getFieldName()
{
return $this->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;
}
}
+3 -1
View File
@@ -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;
@@ -103,6 +103,7 @@ return array (
'b2cMode' => false,
'restrictedMode' => false,
'theme' => 'Espo',
'massEmailMaxPerHourCount' => 100,
'isInstalled' => false,
);
@@ -116,7 +116,8 @@ return array (
'ldapTryUsernameSplit',
'ldapOptReferrals',
'ldapCreateEspoUser',
'maxEmailAccountCount'
'maxEmailAccountCount',
'massEmailMaxPerHourCount'
),
'isInstalled' => false,
);
@@ -6,7 +6,8 @@
"campaign": "Campaign",
"parent": "Target",
"object": "Object",
"application": "Application"
"application": "Application",
"queueItem": "Queue Item"
},
"options": {
"action": {
@@ -4,7 +4,8 @@
"status": "Status",
"target": "Target",
"sentAt": "Date Sent",
"attemptCount": "Attempts"
"attemptCount": "Attempts",
"emailAddress": "Email Address"
},
"links": {
"target": "Target",
@@ -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"]
@@ -21,6 +21,10 @@
"sentAt": {
"type": "datetime",
"readOnly": true
},
"emailAddress": {
"type": "varchar",
"readOnly": true
}
},
"links": {
@@ -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);
}
}
@@ -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;
}
}
+3 -1
View File
@@ -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)
@@ -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"
}
}
@@ -14,5 +14,11 @@
[{"name": "outboundEmailFromAddress"}, {"name": "outboundEmailIsShared"}],
[{"name": "outboundEmailFromName"}, false]
]
},
{
"label": "Mass Email",
"rows": [
[{"name": "massEmailMaxPerHourCount"}, false]
]
}
]
@@ -252,6 +252,10 @@
"type": "float",
"min": 0,
"tooltip": true
},
"massEmailMaxPerHourCount": {
"type": "int",
"min": 0
}
}
}
@@ -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;
}
}
@@ -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 '<a href="#'+this.model.get('objectType')+'/view/'+this.model.get('objectId')+'">'+this.model.get('objectName')+'</a>';
}
return this.model.get('stringData') || '';
case 'Clicked':
return '<span class="text-success">' + this.model.get('stringData') + '</span>';
if (this.model.get('objectId') && this.model.get('objectType') && this.model.get('objectName')) {
return '<a href="#'+this.model.get('objectType')+'/view/'+this.model.get('objectId')+'">'+this.model.get('objectName')+'</a>';
}
return '<span>' + (this.model.get('stringData') || '') + '</span>';
case 'Opted Out':
return '<span class="text-muted">' + this.model.get('stringData') + '</span>';
return '<s>' + this.model.get('stringData') + '</s>';
case 'Bounced':
var emailAddress = this.model.get('stringData');
var type = this.model.get('stringAdditionalData');