1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
37:
38: Yii::import('application.modules.marketing.models.*');
39: Yii::import('application.modules.docs.models.*');
40: Yii::import('application.components.util.StringUtil', true);
41:
42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58:
59: class CampaignMailingBehavior extends EmailDeliveryBehavior {
60:
61: 62: 63: 64: 65:
66: const EMLLOCK = 'campaign_emailing.lock';
67:
68: 69: 70:
71: const STATE_BULKLIMIT = 1;
72:
73: 74: 75:
76: const STATE_RACECOND = 2;
77:
78: 79: 80:
81: const STATE_NULLADDRESS = 3;
82:
83: 84: 85:
86: const STATE_DONOTEMAIL = 4;
87:
88: 89: 90:
91: const STATE_SENT = 5;
92:
93: 94: 95: 96: 97:
98: public static $batchTime;
99:
100: 101: 102:
103: public $_campaign;
104:
105: 106: 107: 108: 109:
110: private $_isNewsletter;
111:
112: 113: 114: 115:
116: private $_list;
117:
118: 119: 120:
121: private $_listItem;
122:
123: 124: 125: 126: 127: 128:
129: private $_recipient;
130:
131: 132: 133: 134:
135: public $fullStop = false;
136:
137: 138: 139: 140:
141: public $itemId;
142:
143: 144: 145: 146: 147:
148: public $stateChange = false;
149:
150: 151: 152: 153: 154: 155:
156: public $stateChangeType = 0;
157:
158:
159: 160: 161: 162:
163: public $undeliverable = false;
164:
165:
166:
167:
168:
169:
170:
171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191:
192: public static function prepareEmail (
193: Campaign $campaign, Contacts $contact, $replaceBreaks=true, $replaceUnsubToken=true) {
194:
195: $email = $contact->email;
196: $now = time();
197: $uniqueId = md5 (uniqid (mt_rand (), true));
198:
199:
200:
201: if ($replaceBreaks)
202: $emailBody = StringUtil::pregReplace('/<br>/', "<br>\n", $campaign->content);
203: else
204: $emailBody = $campaign->content;
205:
206:
207: try{
208: $attachments = $campaign->attachments;
209: if(sizeof($attachments) > 0){
210: $emailBody .= "<br>\n<br>\n";
211: $emailBody .= '<b>'.Yii::t('media', 'Attachments:')."</b><br>\n";
212: }
213: foreach($attachments as $attachment){
214: $media = $attachment->mediaFile;
215: if($media){
216: if($file = $media->getPath()){
217: if(file_exists($file)){
218: if($url = $media->getFullUrl()){
219: $emailBody .= CHtml::link($media->fileName, $media->fullUrl).
220: "<br>\n";
221: }
222: }
223: }
224: }
225: }
226: }catch(Exception $e){
227: throw $e;
228: }
229:
230:
231: $emailBody = Docs::replaceVariables($emailBody, $contact, array (
232: '{trackingKey}' => $uniqueId,
233: ));
234:
235:
236:
237: if ($campaign->enableRedirectLinks) {
238:
239:
240: $url = Yii::app()->controller->createAbsoluteUrl (
241: 'click', array ('uid' => $uniqueId, 'type' => 'click'));
242: $emailBody = StringUtil::pregReplaceCallback (
243: '/(<a[^>]*href=")([^"]*)("[^>]*>)/',
244: function (array $matches) use ($url) {
245: return $matches[1].$url.'&url='.urlencode ($matches[2]).''.
246: $matches[3];
247: }, $emailBody);
248: }
249:
250:
251:
252: if(!preg_match('/\{_unsub\}/', $campaign->content)){
253: $unsubText = "<br/>\n-----------------------<br/>\n".
254: Yii::t('marketing', 'To stop receiving these messages, click here').": {_unsub}";
255:
256: if(strpos($emailBody,'</body>')!==false) {
257: $emailBody = str_replace('</body>',$unsubText.'</body>',$emailBody);
258: } else {
259: $emailBody .= $unsubText;
260: }
261: }
262:
263:
264: $unsubUrl = Yii::app()->createExternalUrl('/marketing/marketing/click', array(
265: 'uid' => $uniqueId,
266: 'type' => 'unsub',
267: 'email' => $email
268: ));
269: $unsubLinkText = Yii::app()->settings->getDoNotEmailLinkText();
270: if ($replaceUnsubToken) {
271: $emailBody = StringUtil::pregReplace (
272: '/\{_unsub\}/',
273: '<a href="'.$unsubUrl.'">'.Yii::t('marketing', $unsubLinkText).'</a>',
274: $emailBody);
275: }
276:
277:
278: $user = User::model()->findByAttributes(array('username' => $campaign->assignedTo));
279: $emailBody = Docs::replaceVariables($emailBody, null, array (
280: '{signature}' => ($user instanceof User) ?
281: Docs::replaceVariables ($user->profile->signature, $contact) : '',
282: ));
283:
284:
285: $subject = Docs::replaceVariables($campaign->subject, $contact);
286:
287:
288: $trackingImage = '<img src="'.Yii::app()->createExternalUrl(
289: '/marketing/marketing/click', array('uid' => $uniqueId, 'type' => 'open')).'"/>';
290: if(strpos($emailBody,'</body>')!==false) {
291: $emailBody = str_replace('</body>',$trackingImage.'</body>',$emailBody);
292: } else {
293: $emailBody .= $trackingImage;
294: }
295:
296: return array($subject, $emailBody, $uniqueId);
297: }
298:
299: public static function emailLockFile() {
300: return implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'runtime',self::EMLLOCK));
301: }
302:
303: public static function lockEmail($lock = true) {
304: $lf = self::emailLockFile();
305: if($lock)
306: file_put_contents($lf,time());
307: else
308: unlink($lf);
309: }
310:
311: 312: 313:
314: public static function emailIsLocked() {
315: $lf = self::emailLockFile();
316: if(file_exists($lf)) {
317: $lock = file_get_contents($lf);
318: if(time() - (int) $lock > 3600) {
319: unlink($lf);
320: return false;
321: } else
322: return true;
323: }
324: return false;
325: }
326:
327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341:
342: public static function deliverableItems($listId,$unsent = false) {
343: $where = ' WHERE
344: i.listId=:listId
345: AND i.unsubscribed=0
346: AND (c.doNotEmail!=1 OR c.doNotEmail IS NULL)
347: AND NOT ((c.email IS NULL OR c.email="") AND (i.emailAddress IS NULL OR i.emailAddress=""))';
348: if($unsent)
349: $where .= ' AND i.sent=0';
350: return Yii::app()->db->createCommand('SELECT
351: i.id,i.sent,i.uniqueId
352: FROM x2_list_items AS i
353: LEFT JOIN x2_contacts AS c ON c.id=i.contactId '.$where)
354: ->queryAll(true,array(':listId'=>$listId));
355: }
356:
357: public static function recordEmailSent(Campaign $campaign, Contacts $contact){
358: $action = new Actions;
359:
360: $action->scenario = 'noNotif';
361: $now = time();
362: $action->associationType = 'contacts';
363: $action->associationId = $contact->id;
364: $action->associationName = $contact->firstName.' '.$contact->lastName;
365: $action->visibility = $contact->visibility;
366: $action->type = 'email';
367: $action->assignedTo = $contact->assignedTo;
368: $action->createDate = $now;
369: $action->completeDate = $now;
370: $action->complete = 'Yes';
371: $action->actionDescription = '<b>'.Yii::t('marketing', 'Campaign').': '.$campaign->name."</b>\n\n"
372: .Yii::t('marketing', 'Subject').": ".Docs::replaceVariables($campaign->subject, $contact)."\n\n".Docs::replaceVariables($campaign->content, $contact);
373: if(!$action->save())
374: throw new CException('Campaing email action history record failed to save with validation errors: '.CJSON::encode($action->errors));
375: }
376:
377:
378:
379:
380:
381:
382:
383:
384: 385: 386: 387:
388: public function getCampaign() {
389: return $this->_campaign;
390: }
391:
392: 393: 394: 395: 396:
397: public function getCredId() {
398: return $this->campaign->sendAs;
399: }
400:
401: public function getIsNewsletter() {
402: if(!isset($this->_isNewsletter)) {
403: $this->_isNewsletter = empty($this->listItem->contactId);
404: }
405: return $this->_isNewsletter;
406: }
407:
408: 409: 410:
411: public function getList() {
412: if(!isset($this->_list)) {
413: $this->_list = $this->campaign->list;
414: }
415: }
416:
417: 418: 419:
420: public function getListItem() {
421: if(!isset($this->_listItem)) {
422: $this->_listItem = X2ListItem::model()->findByAttributes(array (
423: 'id' => $this->itemId,
424: ));
425: }
426: return $this->_listItem;
427: }
428:
429: 430: 431: 432:
433: public function getRecipient() {
434: if(!isset($this->_recipient)) {
435: if(!empty($this->listItem->contact))
436: $this->_recipient = $this->listItem->contact;
437: else {
438:
439: $this->_recipient = new Contacts;
440: $this->_recipient->email = $this->listItem->emailAddress;
441: }
442: }
443: return $this->_recipient;
444: }
445:
446: 447: 448: 449: 450: 451: 452: 453: 454:
455: public function mailIsStillDeliverable() {
456:
457: $admin = Yii::app()->settings;
458: if($admin->emailCountWillExceedLimit() && !empty($admin->emailStartTime)) {
459: $this->status['code'] = 0;
460: $t_now = time();
461: $t_remain = ($admin->emailStartTime + $admin->emailInterval) - $t_now;
462: $params = array();
463: if($t_remain > 60) {
464: $params['{units}'] = $t_remain >= 120 ? Yii::t('app','minutes') : Yii::t('app','minute');
465: $params['{t}'] = round($t_remain/60);
466: } else {
467: $params['{units}'] = $t_remain == 1 ? Yii::t('app','second') : Yii::t('app','seconds');
468: $params['{t}'] = $t_remain;
469: }
470:
471: $this->status['message'] = Yii::t('marketing', 'The email sending limit has been reached.').' '.Yii::t('marketing','Please try again in {t} {units}.',$params);
472: $this->fullStop = true;
473: $this->stateChange = true;
474: $this->stateChangeType = self::STATE_BULKLIMIT;
475: return false;
476: }
477:
478:
479:
480:
481:
482: $sendingItems = Yii::app()->db->createCommand()
483: ->update($this->listItem->tableName(), array('sending' => 1), 'id=:id AND sending=0', array(':id' => $this->listItem->id));
484:
485: $this->stateChange = $sendingItems == 0;
486: if($this->stateChange) {
487: $this->status['message'] = Yii::t('marketing','Skipping {email}; another concurrent send operation is handling delivery to this address.',array('{email}'=>$this->recipient->email));
488: $this->status['code'] = 0;
489: $this->stateChangeType = self::STATE_RACECOND;
490: return false;
491: }
492:
493:
494:
495:
496: if($this->stateChange = $this->stateChange || $this->recipient->email == null) {
497: $this->status['message'] = Yii::t('marketing','Skipping delivery for recipient {id}; email address has been set to blank.',array('{id}'=>$this->itemId));
498: $this->status['code'] = 0;
499: $this->stateChangeType = self::STATE_NULLADDRESS;
500: return false;
501: }
502:
503:
504: if($this->stateChange = $this->stateChange || $this->listItem->unsubscribed!=0 || $this->recipient->doNotEmail!=0) {
505: $this->status['message'] = Yii::t('marketing','Skipping {email}; the contact has unsubscribed.',array('{email}'=>$this->recipient->email));
506: $this->status['code'] = 0;
507: $this->stateChangeType = self::STATE_DONOTEMAIL;
508: return false;
509: }
510:
511:
512: $this->listItem->refresh();
513: if($this->stateChange = $this->stateChange || $this->listItem->sent !=0) {
514: $this->status['message'] = Yii::t('marketing','Email has already been sent to {address}',array('{address}'=>$this->recipient->email));
515: $this->status['code'] = 0;
516: $this->stateChangeType = self::STATE_SENT;
517: return false;
518: }
519:
520: return true;
521: }
522:
523:
524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537:
538: public function markEmailSent($uniqueId,$sent = true) {
539: $params = array(
540: ':listId' => $this->listItem->listId,
541: ':emailAddress' => $this->recipient->email,
542: ':email' => $this->recipient->email,
543: ':setEmail' => $this->recipient->email,
544: ':id' => $this->itemId,
545: ':sent' => $sent?time():0,
546: ':uniqueId' => $sent?$uniqueId:null,
547: );
548: $condition = 'i.id=:id OR (i.listId=:listId AND (i.emailAddress=:emailAddress OR c.email=:email))';
549: $columns = 'i.sent=:sent,i.uniqueId=:uniqueId,sending=0,emailAddress=:setEmail';
550: Yii::app()->db->createCommand('UPDATE x2_list_items AS i LEFT JOIN x2_contacts AS c ON c.id=i.contactId SET '.$columns.' WHERE '.$condition)->execute($params);
551: }
552:
553: 554: 555:
556: public function sendIndividualMail() {
557: if(!$this->mailIsStillDeliverable()) {
558: return;
559: }
560: $addresses = array(array('',$this->recipient->email));
561: $deliver = true;
562: try {
563: list($subject,$message,$uniqueId) = self::prepareEmail(
564: $this->campaign,$this->recipient);
565: } catch (StringUtilException $e) {
566: $this->fullStop = true;
567: $this->status['code'] = 500;
568: $this->status['exception'] = $e;
569: if ($e->getCode () === StringUtilException::PREG_REPLACE_CALLBACK_ERROR) {
570: $this->status['message'] = Yii::t('app', 'Email redirect link insertion failed');
571: } else {
572: $this->status['message'] = Yii::t('app', 'Failed to prepare email contents');
573: }
574: $deliver = false;
575: }
576:
577: if ($deliver) {
578: $unsubUrl = Yii::app()->createExternalUrl('/marketing/marketing/click', array(
579: 'uid' => $uniqueId,
580: 'type' => 'unsub',
581: 'email' => $this->recipient->email
582: ));
583: $this->deliverEmail($addresses, $subject, $message, array(), $unsubUrl);
584: }
585: if($this->status['code'] == 200) {
586:
587: $this->markEmailSent($uniqueId);
588: if(!$this->isNewsletter)
589: self::recordEmailSent($this->campaign,$this->recipient);
590: $this->status['message'] = Yii::t('marketing','Email sent successfully to {address}.',array('{address}' => $this->recipient->email));
591: } else if ($this->status['exception'] instanceof phpmailerException) {
592:
593: $this->status['message'] = Yii::t('marketing','Email could not be sent to {address}. The message given was: {message}',array(
594: '{address}'=>$this->recipient->email,
595: '{message}'=>$this->status['exception']->getMessage()
596: ));
597:
598: if($this->status['exception']->getCode() != PHPMailer::STOP_CRITICAL){
599: $this->undeliverable = true;
600: $this->markEmailSent(null, false);
601: }else{
602: $this->fullStop = true;
603: }
604: } else if($this->status['exception'] instanceof phpmailerException && $this->status['exception']->getCode() == PHPMailer::STOP_CRITICAL) {
605: } else {
606:
607: $this->listItem->sending = 0;
608: $this->listItem->update(array('sending'));
609: }
610:
611:
612: Yii::app()->settings->countEmail();
613:
614:
615: $this->campaign->lastActivity = time();
616:
617: if(count(self::deliverableItems($this->campaign->list->id, true)) == 0) {
618: $this->status['message'] = Yii::t('marketing','All emails sent.');
619: $this->campaign->active = 0;
620: $this->campaign->complete = 1;
621: $this->campaign->update(array('lastActivity','active','complete'));
622: } else {
623: $this->campaign->update(array('lastActivity'));
624: }
625: }
626:
627: public function setCampaign(Campaign $value) {
628: $this->_campaign = $value;
629: }
630:
631:
632:
633:
634:
635:
636: 637: 638: 639: 640: 641: 642: 643:
644: public static function sendMail($id = null, $t0 = null){
645: self::$batchTime = $t0 === null ? time() : $t0;
646: $admin = Yii::app()->settings;
647: $messages = array();
648: $totalSent = 0;
649: try{
650:
651: $campaigns = Campaign::model()->findAllByAttributes(
652: array('complete' => 0, 'active' => 1, 'type' => 'Email'), 'launchdate > 0 AND launchdate < :time', array(':time' => time()));
653: foreach($campaigns as $campaign){
654: try{
655: list($sent, $errors) = self::campaignMailing($campaign);
656: }catch(CampaignMailingException $e){
657: $totalSent += $e->return[0];
658: $messages = array_merge($messages, $e->return[1]);
659: $messages[] = Yii::t('marketing', 'Successful email sent').': '.$totalSent;
660: $wait = ($admin->emailInterval + $admin->emailStartTime) - time();
661: return array('wait' => $wait, 'messages' => $messages);
662: }
663: $messages = array_merge($messages, $errors);
664: $totalSent += $sent;
665: if(time() - self::$batchTime > Yii::app()->settings->batchTimeout)
666: break;
667:
668: }
669: if(count($campaigns) == 0){
670: $messages[] = Yii::t('marketing', 'There is no campaign email to send.');
671: }
672:
673: }catch(Exception $e){
674: $messages[] = $e->getMessage();
675: }
676: $messages[] = $totalSent == 0 ? Yii::t('marketing', 'No email sent.') : Yii::t('marketing', 'Successful email sent').': '.$totalSent;
677: $wait = ($admin->emailInterval + $admin->emailStartTime) - time();
678: return array('wait' => $wait, 'messages' => $messages);
679: }
680:
681: 682: 683: 684: 685: 686: 687: 688:
689: protected static function campaignMailing(Campaign $campaign, $limit = null){
690: $class = __CLASS__;
691: $totalSent = 0;
692: $errors = array();
693: $items = self::deliverableItems($campaign->list->id,true);
694: foreach($items as $item) {
695: $mailer = new $class;
696: $mailer->campaign = $campaign;
697: $mailer->itemId = $item['id'];
698: $mailer->sendIndividualMail();
699: if($mailer->fullStop) {
700: $errors[] = $mailer->status['message'];
701: throw new CampaignMailingException(array($totalSent,$errors));
702: } elseif($mailer->status['code'] != 200) {
703: $errors[] = $mailer->status['message'];
704: } else {
705: $totalSent++;
706: }
707: if(time() - self::$batchTime > Yii::app()->settings->batchTimeout) {
708: $errors[] = Yii::t('marketing','Batch timeout limit reached.');
709: break;
710: }
711: }
712: return array($totalSent, $errors);
713: }
714: }
715:
716: 717: 718: 719:
720: class CampaignMailingException extends CException {
721: public $return;
722:
723: public function __construct($return,$message=null, $code=0, $previous=null){
724: parent::__construct($message, $code, $previous);
725: $this->return = $return;
726: }
727: }
728:
729: ?>
730: