Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • ActionFormModel
  • ArrayUtil
  • ArrayValidator
  • AssociatedMediaBehavior
  • AuxLib
  • Changelog
  • DetailView
  • EncryptUtilTmp
  • EventsWidgetFieldFormatter
  • FailedLogins
  • FieldFormatter
  • FieldFormatterBase
  • FieldInputRenderer
  • FileFieldBehavior
  • FiltersForm
  • FilterUtil
  • FineDiff
  • FineDiffCopyOp
  • FineDiffDeleteOp
  • FineDiffInsertOp
  • FineDiffOp
  • FineDiffOps
  • FineDiffReplaceOp
  • GlobalCSSFormModel
  • GlobalImportFormModel
  • GoogleAuthenticator
  • JSONFieldsBehavior
  • JSONResponse
  • MediaFieldFormatter
  • MediaSelector
  • MobileActiveRecordFieldFormatter
  • MobileActivityFeed
  • MobileChartDashboard
  • MobileFieldFormatter
  • MobileFieldInputRenderer
  • ModuleModelNameValidator
  • MultiChildNode
  • MultiTypeAutocomplete
  • PasswordUtil
  • ProductFeature
  • ProfileWidgetLayout
  • QueryParamGenerator
  • RecordLimitBehavior
  • RecordView
  • RecordViewWidgetLayout
  • RelationshipsGridModel
  • RelationshipsJoin
  • RepairUserDataCommand
  • RequestUtil
  • RequiredIfNotSetValidator
  • ResponseUtil
  • RunMigrationScriptCommand
  • ServiceWebFormDesigner
  • Settings
  • StringUtil
  • TestEmailAction
  • TestEmailActionForm
  • ThemeGenerator
  • TimerUtil
  • TopicsFieldFormatter
  • TopicsWidgetLayout
  • TransactionalViewFieldFormatter
  • UrlUtil
  • ValidLinkValidator
  • WebFormDesigner
  • WebLeadFormDesigner
  • X2ActiveRecordBehavior
  • X2ActiveRecordFieldFormatter
  • X2ButtonColumn
  • X2ConditionList
  • X2ConsoleCommand
  • X2ControllerBehavior
  • X2DataColumn
  • X2DuplicateBehavior
  • X2Flashes
  • X2GridViewFieldFormatter
  • X2IPAddress
  • X2LeadsDataColumn
  • X2MergeableBehavior
  • X2MessageSource
  • X2MobileControllerBehavior
  • X2MobileProfileControllerBehavior
  • X2MobileQuotesControllerBehavior
  • X2MobileSiteControllerBehavior
  • X2MobileTopicsControllerBehavior
  • X2ModelConversionBehavior
  • X2ModelConversionWidget
  • X2ModelForeignKeyValidator
  • X2ModelUniqueIndexValidator
  • X2NonWebUser
  • X2StaticDropdown
  • X2StaticField
  • X2StaticFieldsBehavior
  • X2UrlManager
  • X2Validator
  • X2WidgetBehavior

Interfaces

  • AdminOwnedCredentials

Exceptions

  • CampaignMailingException
  • CodeExchangeException
  • GetCredentialsException
  • NoRefreshTokenException
  • NoUserIdException
  • StringUtilException

Functions

  • checkCurrency
  • checkDNS
  • checkServerVar
  • checkTimezone
  • decodeQuotes
  • echoIcons
  • encodeQuotes
  • exceptionForError
  • getField
  • getLanguageName
  • getModuleTitle
  • handleReqError
  • handleReqException
  • installer_t
  • installer_tr
  • isAllowedDir
  • mediaMigrationRrmdir
  • migrateMediaDir
  • printGraph
  • printR
  • renderFields
  • reqShutdown
  • RIP
  • translateOptions
  • tryGetRemote
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: 
  3: /*****************************************************************************************
  4:  * X2Engine Open Source Edition is a customer relationship management program developed by
  5:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
  6:  * 
  7:  * This program is free software; you can redistribute it and/or modify it under
  8:  * the terms of the GNU Affero General Public License version 3 as published by the
  9:  * Free Software Foundation with the addition of the following permission added
 10:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 11:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
 12:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 13:  * 
 14:  * This program is distributed in the hope that it will be useful, but WITHOUT
 15:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 16:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 17:  * details.
 18:  * 
 19:  * You should have received a copy of the GNU Affero General Public License along with
 20:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
 21:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 22:  * 02110-1301 USA.
 23:  * 
 24:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
 25:  * California 95067, USA. or at email address contact@x2engine.com.
 26:  * 
 27:  * The interactive user interfaces in modified source and object code versions
 28:  * of this program must display Appropriate Legal Notices, as required under
 29:  * Section 5 of the GNU Affero General Public License version 3.
 30:  * 
 31:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 32:  * these Appropriate Legal Notices must retain the display of the "Powered by
 33:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
 34:  * technical reasons, the Appropriate Legal Notices must display the words
 35:  * "Powered by X2Engine".
 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:  * Behavior class for email delivery in email marketing campaigns.
 44:  *
 45:  * Static methods are used for batch emailing; all non-static methods assume that
 46:  * an individual email is being sent.
 47:  *
 48:  * @property Campaign $campaign Campaign model for the current email
 49:  * @property boolean $isNewsletter True if sending to a newsletter list (not a contacts list)
 50:  * @property X2List $list The list corresponding to the current campaign being operated on
 51:  * @property X2ListItem $listItem List item of the
 52:  * @property Contacts $recipient The contact of the current recipient that the
 53:  *  email is being sent to. If it's not a campaign, but a newsletter, this will
 54:  *  be an ad-hoc contact model with its email address set to that of the list
 55:  *  item.
 56:  * @package application.modules.marketing.components
 57:  * @author Demitri Morgan <demitri@x2engine.com>
 58:  */
 59: class CampaignMailingBehavior extends EmailDeliveryBehavior {
 60: 
 61:     /**
 62:      * Filename of lock file in protected/runtime, to signal that emailing is
 63:      * already in progress and other processes should not attempt to send email
 64:      * (as this may result in race conditions and duplicate emails)
 65:      */
 66:     const EMLLOCK = 'campaign_emailing.lock';
 67: 
 68:     /**
 69:      * Error code for the bulk limit being reached
 70:      */
 71:     const STATE_BULKLIMIT = 1;
 72: 
 73:     /**
 74:      * Error code for an email already sending.
 75:      */
 76:     const STATE_RACECOND = 2;
 77: 
 78:     /**
 79:      * Error code for an item whose address has suddenly gone blank
 80:      */
 81:     const STATE_NULLADDRESS = 3;
 82: 
 83:     /**
 84:      * Error code for an unsubscribed / do-not-email contact
 85:      */
 86:     const STATE_DONOTEMAIL = 4;
 87: 
 88:     /**
 89:      * Error code for another email process beating us to the punch
 90:      */
 91:     const STATE_SENT = 5;
 92: 
 93:     /**
 94:      * Stores the time that the batch operation started (when calling this
 95:      * class' methods statically)
 96:      * @var type
 97:      */
 98:     public static $batchTime;
 99: 
100:     /**
101:      * @var Campaign The current campaign model being operated on
102:      */
103:     public $_campaign;
104: 
105:     /**
106:      * True if the campaign is getting sent to a web list (not corresponding to
107:      * contacts).
108:      * @var boolean
109:      */
110:     private $_isNewsletter;
111: 
112:     /**
113:      * List model corresponding to the campaign.
114:      * @var type X2List
115:      */
116:     private $_list;
117: 
118:     /**
119:      * List item model
120:      */
121:     private $_listItem;
122: 
123:     /**
124:      * Contact record corresponding to the recipient of the current mail being
125:      * delivered.
126:      * 
127:      * @var Contacts
128:      */
129:     private $_recipient;
130: 
131:     /**
132:      * Whether the campaign mailing process should halt as soon as possible
133:      * @var type 
134:      */
135:     public $fullStop = false;
136: 
137:     /**
138:      * The ID of the campaign list item corresponding to the current recipient.
139:      * @var integer
140:      */
141:     public $itemId;
142: 
143:     /**
144:      * Indicates whether the mail cannot be sent due to a recent change in the
145:      * list item or contact record.
146:      * @var boolean
147:      */
148:     public $stateChange = false;
149: 
150:     /**
151:      * Indicates the type of state change that should block email delivery.
152:      * This purpose is not relegated to {@link status} ("code" element) because
153:      * that array is intended for PHPMailer codes.
154:      * @var integer
155:      */
156:     public $stateChangeType = 0;
157: 
158: 
159:     /**
160:      * Whether the current email could not be delivered due to bad RCPT or something
161:      * that's not a critical PHPMailer error
162:      */
163:     public $undeliverable = false;
164: 
165:     /////////////////////////
166:     // INDEPENDENT METHODS //
167:     /////////////////////////
168:     //
169:     // Used whether bulk-sending or individually sending
170: 
171:     /**
172:      * Prepares the subject and body of a campaign email.
173:      *
174:      * Any and all features of the campaign that are dynamically added at the
175:      * last minute to the email body are added in this method right before
176:      * the sending of the email.
177:      *
178:      * Returns an array; 
179:      * 
180:      * First element: email subject
181:      * Second element: email body
182:      * Third element: unique ID assigned to the current email
183:      *
184:      * @param Campaign $campaign Campaign of the current email being sent
185:      * @param Contacts $contact Contact to whom the email is being sent
186:      * @param type $email
187:      * @param bool $replaceBreaks used for unit testing
188:      * @param bool $replaceUnsubToken used for unit testing
189:      * @return type
190:      * @throws Exception
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:         // Add some newlines to prevent hitting 998 line length limit in
200:         // phpmailer and rfc2821
201:         if ($replaceBreaks)
202:             $emailBody = StringUtil::pregReplace('/<br>/', "<br>\n", $campaign->content);
203:         else
204:             $emailBody = $campaign->content;
205: 
206:         // Add links to attachments
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)){ // check file exists
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:         // Replacement in body
231:         $emailBody = Docs::replaceVariables($emailBody, $contact, array (
232:             '{trackingKey}' => $uniqueId, // Use the campaign key, not the general contact key
233:         ));
234: 
235:         // transform links after attribute replacement but before signature and unsubscribe link 
236:         // insertion
237:         if ($campaign->enableRedirectLinks) {
238: 
239:             // Replace links with tracking links
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:         // Insert unsubscribe link placeholder in the email body if there is
251:         // none already:
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:             // Insert
256:             if(strpos($emailBody,'</body>')!==false) {
257:                 $emailBody = str_replace('</body>',$unsubText.'</body>',$emailBody);
258:             } else {
259:                 $emailBody .= $unsubText;
260:             }
261:         }
262: 
263:         // Insert unsubscribe link(s):
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:         // Get the assignee of the campaign, for signature replacement.
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:         // Replacement in subject
285:         $subject = Docs::replaceVariables($campaign->subject, $contact);
286: 
287:         // Add the transparent tracking image:
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:     * Mediates lockfile checking.
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) { // No operation should take longer than an hour
319:                 unlink($lf);
320:                 return false;
321:             } else
322:                 return true;
323:         }
324:         return false;
325:     }
326: 
327:     /**
328:      * For a given list ID, find all contact/list item entries such that sending
329:      * is possible, is permissible, or has happened.
330:      *
331:      * The criteria are:
332:      * - x2_list_item.listId matches the list being operated on
333:      * - x2_list_item.unsubscribed and x2_contacts.doNotEmail are both zero
334:      *  (contact has not specified that email is unwelcome)
335:      * - One of x2_list_item.emailAddress or x2_contacts.email is non-empty, so
336:      *  that there is actually an email address to send to
337:      *
338:      * @param integer $listId The ID of the list operating on
339:      * @param boolean $unsent Constrain (if true) the query to unsent entries.
340:      * @return array An array containing the "id", "sent" and "uniqueId" columns.
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:         // Disable the unsightly notifications for loads of emails:
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:     // INDIVIDUAL DELIVERY METHODS //
379:     /////////////////////////////////
380:     //
381:     // When used as a behavior, the class is geared towards sending individual
382:     // emails.
383: 
384:     /**
385:      * Campaign model for this e-mail
386:      * @return type
387:      */
388:     public function getCampaign() {
389:         return $this->_campaign;
390:     }
391: 
392:     /**
393:      * Credentials record to be used. Overrides {@link EmailDeliveryBehavior::sendAs}
394:      * in order to configure SMTP delivery.
395:      * @return type
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:      * Getter for {@link list}
410:      */
411:     public function getList() {
412:         if(!isset($this->_list)) {
413:             $this->_list = $this->campaign->list;
414:         }
415:     }
416: 
417:     /**
418:      * Getter for {@link listItem}
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:      * Getter for {@link recipient}
431:      * @return type
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:                 // Newsletter
439:                 $this->_recipient = new Contacts;
440:                 $this->_recipient->email = $this->listItem->emailAddress;
441:             }
442:         }
443:         return $this->_recipient;
444:     }
445: 
446:     /**
447:      * One final check for whether the mail should be sent, and enable the
448:      * 'sending' flag.
449:      * 
450:      * This is a safeguard for the use case of batch emailing when a user
451:      * subscribes or a value in the database changes between loading the list
452:      * items to deliver and when the actual delivery takes place.
453:      * @return bool True if we're clear to send; false otherwise.
454:      */
455:     public function mailIsStillDeliverable() {
456:         // Check if the batch limit has been reached:
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:         // Sending flag check:
479:         //
480:         // Perform the update operation to flip the flag, and if zero rows were
481:         // affected, that indicates it's already sending.
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:         // If no rows matched, the message is being sent right now.
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:         // Additional checks
494:         //
495:         // Email hasn't been set blank:
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:         // Contact unsubscribed suddenly
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:         // Another mailing process sent it already:
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:      * Records the date of delivery and marks the list record with the unique id.
526:      *
527:      * This method will not just update the current list item; it selects all
528:      * list items if their email address and list ID are the same. This is to
529:      * avoid sending duplicate messages.
530:      *
531:      * If mail is non-deliverable, it should still be marked as sent but with a
532:      * null unique ID, to designate it as a bad email address.
533:      * 
534:      * @param type $uniqueId
535:      * @param bool $unsent If false, perform the opposite operation (mark as not
536:      *  currently sending).
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:      * Send an email.
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:             // Successfully sent email. Mark as sent.
587:             $this->markEmailSent($uniqueId);
588:             if(!$this->isNewsletter) // Create action history records; sent to contact list
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:             // Undeliverable mail. Mark as sent but without unique ID, designating it as a bad address
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:             // Mark as "not currently working on sending"...One way or another, it's done.
607:             $this->listItem->sending = 0;
608:             $this->listItem->update(array('sending'));
609:         }
610: 
611:         // Keep track of this email as part of bulk emailing
612:         Yii::app()->settings->countEmail();
613: 
614:         // Update the last activity on the campaign
615:         $this->campaign->lastActivity = time();
616:         // Finally, if the campaign is totally done, mark as complete.
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:     // BULK MAILING METHODS //
634:     //////////////////////////
635: 
636:    /**
637:     * Send mail for any active campaigns, in a batch.
638:     *
639:     * This method is made public and static to allow it to be called from elsewhere,
640:     * without instantiation.
641:     *
642:     * @param integer $id The ID of the campaign to return status messages for
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:             // Get all campaigns that could use mailing
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:      * Send mail for one campaign
683:      *
684:      * @param Campaign $campaign The campaign to send
685:      * @param integer $limit The maximum number of emails to send
686:      *
687:      * @return Array [0]=> The number of emails sent, [1]=> array of applicable error messages
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:  * Campaign mailing instant halt exception class that retains data regarding the
718:  * current operation.
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: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0