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

  • ActionMetaData
  • ActionText
  • Admin
  • AmorphousModel
  • ApiHook
  • APIModel
  • ChartSetting
  • ContactForm
  • ContactList
  • Credentials
  • Criteria
  • Dropdowns
  • Events
  • EventsData
  • Fields
  • FormLayout
  • Imports
  • InlineEmail
  • LeadRouting
  • Locations
  • LoginForm
  • Maps
  • Modules
  • Notes
  • Notification
  • PhoneNumber
  • Profile
  • Record
  • Relationships
  • Roles
  • RoleToPermission
  • RoleToUser
  • RoleToWorkflow
  • Rules
  • Session
  • SessionLog
  • Social
  • Tags
  • TempFile
  • Tips
  • Tours
  • TrackEmail
  • TriggerLog
  • URL
  • ViewLog
  • Widgets
  • X2List
  • X2ListCriterion
  • X2ListItem
  • X2Model
  • 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.docs.models.*');
  39: Yii::import('application.modules.actions.models.*');
  40: Yii::import('application.modules.contacts.models.*');
  41: Yii::import('application.modules.quotes.models.*');
  42: 
  43: /**
  44:  * InlineEmail class. InlineEmail is the data structure for taking in and
  45:  * processing data for outbound email, specifically from the inline email widget.
  46:  *
  47:  * It is used by the InlineEmailForm widget and site/inlineEmail, and is
  48:  * designed around this principle: that the email is being sent in some context
  49:  * that is dictated by a "target model". Special cases for behavior of the class
  50:  * have been built this way, i.e. when the target model is a {@link Quote}, the
  51:  * insertable attributes should include those of both associated contact and
  52:  * account as well as the quote, and when the email is sent, the action history
  53:  * record that gets created should appropriately describe the event happened,
  54:  * i.e. by saying that "Quote #X was issued by email" rather than merely "user X
  55:  * has sent contact Y an email."
  56:  *
  57:  * The following describes the scenarios of this model:
  58:  * - "custom" is used when a modified email has been submitted for processing or
  59:  *      sending
  60:  * - "template" is used when the form has been submitted to re-create the email
  61:  *      based on a template.
  62:  * - Blank/empty string is for when there's a new and blank email (i.e. initial
  63:  *      rendering of the inline email widget {@link InlineEmailForm})
  64:  *
  65:  * @property string $actionHeader (read-only) A mock-up of the email's header
  66:  *  fields to be inserted into the email actions' bodies, for display purposes.
  67:  * @property array $insertableAttributes (read-only) Attributes for the inline
  68:  *  email editor that can be inserted into the message.
  69:  * @property array $recipientContacts (read-only) an array of contact records
  70:  *  identified by recipient email address.
  71:  * @property array $recipients (read-only) an array of all recipients of the email.
  72:  * @property string $signature Signature of the user sending the email, if any
  73:  * @property X2Model $targetModel The model associated with this email, i.e.
  74:  *  Contacts or Quote
  75:  * @property Docs $templateModel (read-only) template, if any, to use.
  76:  * @property string $trackingImage (read-only) Markup for the tracking image to
  77:  *  be placed in the email
  78:  * @property string $uniqueId A unique ID used for the tracking record and
  79:  *  tracking image URL
  80:  * @package application.models
  81:  */
  82: class InlineEmail extends CFormModel {
  83:     // Enclosure comments:
  84: 
  85:     const SIGNATURETAG = 'Signature'; // for signature
  86:     const TRACKTAG = 'OpenedEmail'; // for the tracking image
  87:     const AHTAG = 'ActionHeader'; // for the inline action header
  88:     const UIDREGEX = '/uid.([0-9a-f]{32})/';
  89: 
  90:     /**
  91:      * @var string Email address of the addressees
  92:      */
  93:     public $to;
  94: 
  95:     /**
  96:      * @var string CC email address(es), if applicable
  97:      */
  98:     public $cc;
  99: 
 100:     /**
 101:      * @var string BCC email address(es), if applicable
 102:      */
 103:     public $bcc;
 104: 
 105:     /**
 106:      * @var string Email subject
 107:      */
 108:     public $subject;
 109: 
 110:     /**
 111:      * @var string Email body/content
 112:      */
 113:     public $message;
 114: 
 115:     /**
 116:      * @var strng Email Send Time
 117:      */
 118:     public $emailSendTime = '';
 119: 
 120:     /**
 121:      * @var int Email Send Time in unix timestamp format
 122:      */
 123:     public $emailSendTimeParsed = 0;
 124: 
 125:     /**
 126:      * @var integer Template ID
 127:      */
 128:     public $template = 0;
 129: 
 130:     /**
 131:      * Stores the name of the model associated with the email i.e. Contacts or Quote.
 132:      * @var string
 133:      */
 134:     public $modelName;
 135: 
 136:     /**
 137:      * @var integer
 138:      */
 139:     public $modelId;
 140: 
 141:     /**
 142:      *
 143:      * @var bool Asssociate emails with the linked Contact (true) or the record itself (false)
 144:      */
 145:     public $contactFlag = true;
 146: 
 147:     /**
 148:      * @var array
 149:      */
 150:     public $mailingList = array();
 151:     public $attachments = array();
 152:     public $emailBody = '';
 153:     public $preview = false;
 154:     public $stageEmail = false;
 155: 
 156:     /**
 157:      * @var bool $requireSubjectOnCustom Allows subject requirement to be bypassed.   
 158:      * TODO: remove this once scenario code is refactored
 159:      */
 160:     public $requireSubjectOnCustom = true; 
 161: 
 162: 
 163:      
 164: 
 165: 
 166:     private $_recipientContacts;
 167: 
 168:     /**
 169:      * Stores value of {@link actionHeader}
 170:      * @var string
 171:      */
 172:     private $_actionHeader;
 173: 
 174:     /**
 175:      * Stores value of {@link insertableAttributes}
 176:      * @var array
 177:      */
 178:     private $_insertableAttributes;
 179: 
 180:     /**
 181:      * Stores value of {@link recipients}
 182:      * @var array
 183:      */
 184:     private $_recipients;
 185: 
 186:     /**
 187:      * Stores value of {@link signature}
 188:      * @var string
 189:      */
 190:     private $_signature;
 191: 
 192:     /**
 193:      * Stores value of {@link targetModel}
 194:      * @var X2Model
 195:      */
 196:     private $_targetModel;
 197: 
 198:     /**
 199:      * Stores value of {@link templateModel}
 200:      */
 201:     private $_templateModel;
 202: 
 203:     /**
 204:      * Stores value of {@link trackingImage}
 205:      * @var string
 206:      */
 207:     private $_trackingImage;
 208: 
 209:     /**
 210:      * Stores value of {@link uniqueId}
 211:      * @var type
 212:      */
 213:     private $_uniqueId;
 214: 
 215:     /**
 216:      * Declares the validation rules. The rules state that username and password
 217:      * are required, and password needs to be authenticated.
 218:      * @return array
 219:      */
 220:     public function rules(){
 221:         $rules = array(
 222:             array('to', 'required', 'on' => 'custom'),
 223:             // array('modelName,modelId', 'required', 'on' => 'template'),
 224:             array('message', 'required', 'on' => 'custom'),
 225:             array('to,cc,bcc', 'parseMailingList'),
 226:             array('emailSendTime', 'date', 'allowEmpty' => true, 'timestampAttribute' => 'emailSendTimeParsed'),
 227:             array('to, cc, credId, bcc, message, template, modelId, modelName, subject', 'safe'),
 228:              
 229:         );
 230:         if ($this->requireSubjectOnCustom) {
 231:             $rules[] = array('subject', 'required', 'on' => 'custom');
 232:         }
 233:         return $rules;
 234:     }
 235: 
 236:     public function relations () {
 237:         return array(
 238:             'credentials' => array(self::BELONGS_TO, 'Credentials', array ('credId' => 'id')),
 239:         );
 240:     }
 241: 
 242:     /**
 243:      * Declares attribute labels.
 244:      * @return array
 245:      */
 246:     public function attributeLabels(){
 247:         return array(
 248:             'from' => Yii::t('app', 'From:'),
 249:             'to' => Yii::t('app', 'To:'),
 250:             'cc' => Yii::t('app', 'CC:'),
 251:             'bcc' => Yii::t('app', 'BCC:'),
 252:             'subject' => Yii::t('app', 'Subject:'),
 253:             'message' => Yii::t('app', 'Message:'),
 254:             'template' => Yii::t('app', 'Template:'),
 255:             'modelName' => Yii::t('app', 'Model Name'),
 256:             'modelId' => Yii::t('app', 'Model ID'),
 257:             'credId' => Yii::t('app','Send As:'),
 258:              
 259:         );
 260:     }
 261: 
 262:     public function behaviors() {
 263:         return array(
 264:             'emailDelivery' => array('class' => 'application.components.EmailDeliveryBehavior')
 265:         );
 266:     }
 267: 
 268:     /**
 269:      * Creates a pattern for finding or inserting content into the email body.
 270:      *
 271:      * @param string $name The name of the pattern to use. There should be a
 272:      *  constant defined that is the name in upper case followed by "TAG" that
 273:      *  specifies the name to use in comments that demarcate the inserted content.
 274:      * @param string $inside The content to be inserted between comments.
 275:      * @param bool $re Whether to return the pattern as a regular expression
 276:      * @param string $reFlags PCRE flags to use in the expression, if $re is enabled.
 277:      */
 278:     public static function insertedPattern($name, $inside, $re = 0, $reFlags = ''){
 279:         $tn = constant('self::'.strtoupper($name.'tag'));
 280:         $tag = "<!--Begin$tn-->~inside~<!--End$tn-->";
 281:         if($re)
 282:             $tag = '/'.preg_quote($tag)."/$reFlags";
 283:         return str_replace('~inside~', $inside, $tag);
 284:     }
 285: 
 286:     /**
 287:      * Magic getter for {@link actionHeader}
 288:      *
 289:      * Composes an informative header for the action record.
 290:      *
 291:      * @return type
 292:      */
 293:     public function getActionHeader(){
 294:         if(!isset($this->_actionHeader)){
 295: 
 296:             $recipientContacts = $this->recipientContacts;
 297: 
 298:             // Add email headers to the top of the action description's body
 299:             // so that the resulting recorded action has all the info of the
 300:             // original email.
 301:             $fromString = $this->from['address'];
 302:             if(!empty($this->from['name']))
 303:                 $fromString = '"'.$this->from['name'].'" <'.$fromString.'>';
 304: 
 305:             $header = CHtml::tag('strong', array(), Yii::t('app', 'Subject: ')).CHtml::encode($this->subject).'<br />';
 306:             $header .= CHtml::tag('strong', array(), Yii::t('app', 'From: ')).CHtml::encode($fromString).'<br />';
 307:             // Put in recipient lists, and if any correspond to contacts, make links
 308:             // to them in place of their names.
 309:             foreach(array('to', 'cc', 'bcc') as $recList){
 310:                 if(!empty($this->mailingList[$recList])){
 311:                     $header .= CHtml::tag('strong', array(), ucfirst($recList).': ');
 312:                     foreach($this->mailingList[$recList] as $target){
 313:                         if($recipientContacts[$target[1]] != null){
 314:                             $header .= $recipientContacts[$target[1]]->link;
 315:                         }else{
 316:                             $header .= CHtml::encode("\"{$target[0]}\"");
 317:                         }
 318:                         $header .= CHtml::encode(" <{$target[1]}>,");
 319:                     }
 320:                     $header = rtrim($header, ', ').'<br />';
 321:                 }
 322:             }
 323: 
 324:             // Include special quote information if it's a quote being issued or emailed to a random contact
 325:             if($this->modelName == 'Quote'){
 326:                 $header .= '<br /><hr />';
 327:                 $header .= CHtml::tag('strong', array(), Yii::t('quotes', $this->targetModel->type == 'invoice' ? 'Invoice' : 'Quote')).':';
 328:                 $header .= ' '.$this->targetModel->link.($this->targetModel->status ? ' ('.$this->targetModel->status.'), ' : ' ').Yii::t('app', 'Created').' '.$this->targetModel->renderAttribute('createDate').';';
 329:                 $header .= ' '.Yii::t('app', 'Updated').' '.$this->targetModel->renderAttribute('lastUpdated').' by '.$this->userProfile->fullName.'; ';
 330:                 $header .= ' '.Yii::t('quotes', 'Expires').' '.$this->targetModel->renderAttribute('expirationDate');
 331:                 $header .= '<br />';
 332:             }
 333: 
 334:             // Attachments info
 335:             if(!empty($this->attachments)){
 336:                 $header .= '<br /><hr />';
 337:                 $header .= CHtml::tag('strong', array(), Yii::t('media', 'Attachments:'))."<br />";
 338:                 $i = 0;
 339:                 foreach($this->attachments as $attachment){
 340:                     if ($i++) $header .= '<br />';
 341: 
 342:                     if ($attachment['type'] === 'temp') {
 343:                         // attempt to convert temporary file to media record
 344: 
 345:                         if ($this->modelId && $this->modelName) {
 346:                             $associationId = $this->modelId;
 347:                             $associationType = X2Model::getAssociationType ($this->modelName);
 348:                         } elseif ($contact = reset($recipientContacts)) {
 349: 
 350:                             $associationId = $contact->id;
 351:                             $associationType = 'contacts';
 352:                         }
 353:                         if (isset ($associationId) && 
 354:                             ($media = $attachment['model']->convertToMedia (array (
 355:                                 'associationType' => $associationType,
 356:                                 'associationId' => $associationId,
 357:                                 )))) {
 358: 
 359:                             $attachment['type'] = 'media';
 360:                             $attachment['id'] = $media->id;
 361:                         }
 362:                     }
 363: 
 364:                     if ($attachment['type'] === 'media' && 
 365:                         ($media = Media::model ()->findByPk ($attachment['id']))) {
 366:                         
 367:                         $header .= $media->getLink ().'&nbsp;|&nbsp;'.$media->getDownloadLink ();
 368:                     } else {
 369:                         $header .= CHtml::tag(
 370:                             'span', array('class' => 'email-attachment-text'),
 371:                             $attachment['filename']).'<br />';
 372:                     }
 373:                 }
 374:             }
 375: 
 376:             $this->_actionHeader = $header.'<br /><hr />';
 377:         }
 378:         return $this->_actionHeader;
 379:     }
 380: 
 381:     /**
 382:      * Magic getter for {@link insertableAttributes}.
 383:      *
 384:      * Herein is defined how the insertable attributes are put together for each
 385:      * different model class.
 386:      * @return array
 387:      */
 388:     public function getInsertableAttributes(){
 389:         if(!isset($this->_insertableAttributes)){
 390:             $ia = array(); // Insertable attributes
 391:             if($this->targetModel !== false){
 392:                 // Assemble the arrays to be used in putting together insertable attributes.
 393:                 //
 394:                 // What the labels will look like in the insertable attributes
 395:                 // dropdown. {attr} replaced with attribute name, {model}
 396:                 // replaced with model.
 397:                 $labelFormat = '{attr}';
 398:                 // The headers for each model/section, indexed by model class.
 399:                 $headers = array();
 400:                 // The active record objects corresponding to each model class.
 401:                 $models = array($this->modelName => $this->targetModel);
 402:                 switch($this->modelName){
 403:                     case 'Quote':
 404:                         // There will be many more models whose attributes we want
 405:                         // to insert, so prefix each one with the model name to
 406:                         // distinguish the current section:
 407:                         $labelFormat = '{model}: {attr}';
 408:                         $headers = array(
 409:                             'Accounts' => 'Account Attributes',
 410:                             'Quote' => 'Quote Attributes',
 411:                             'Contacts' => 'Contact Attributes',
 412:                         );
 413:                         $models = array_merge($models, array(
 414:                             'Accounts' => $this->targetModel->getLinkedModel('accountName'),
 415:                             'Contacts' => $this->targetModel->contact,
 416:                                 ));
 417:                         break;
 418:                     case 'Contacts':
 419:                         $headers = array(
 420:                             'Contacts' => 'Contact Attributes',
 421:                         );
 422:                         break;
 423:                     case 'Accounts':
 424:                         $labelFormat = '{model}: {attr}';
 425:                         $headers = array_merge($headers, array(
 426:                             'Accounts' => 'Account Attributes'
 427:                                 ));
 428:                         break;
 429:                     case 'Opportunity':
 430:                         $labelFormat = '{model}: {attr}';
 431:                         $headers = array(
 432:                             'Opportunity' => 'Opportunity Attributes',
 433:                         );
 434:                         // Grab the first associated contact and use it (since that
 435:                         // covers the most common use case of one contact, one opportunity)
 436:                         $contactIds = explode(' ', $this->targetModel->associatedContacts);
 437:                         if(!empty($contactIds[0])){
 438:                             $contact = Contacts::model()->findByPk($contactIds[0]);
 439:                             if(!empty($contact)){
 440:                                 $headers['Contacts'] = 'Contact Attributes';
 441:                                 $models['Contacts'] = $contact;
 442:                             }
 443:                         }
 444:                         // Obtain the account info as well, if available:
 445:                         if(!empty($this->targetModel->accountName)){
 446:                             $account = Accounts::model()->findAllByPk($this->targetModel->accountName);
 447:                             if(!empty($account)){
 448:                                 $headers['Accounts'] = 'Account Attributes';
 449:                                 $models['Accounts'] = $account;
 450:                             }
 451:                         }
 452:                         break;
 453:                     case 'Services':
 454:                         $labelFormat = '{model}: {attr}';
 455:                         $headers = array(
 456:                             'Cases' => 'Case Attributes',
 457:                             'Contacts' => 'Contact Attributes',
 458:                         );
 459:                         $models = array(
 460:                             'Cases' => $this->targetModel,
 461:                             'Contacts' => Contacts::model()->findByPk($this->targetModel->contactId),
 462:                         );
 463:                         break;
 464:                 }
 465: 
 466:                 $headers = array_map(function($e){
 467:                             return Yii::t('app', $e);
 468:                         }, $headers);
 469: 
 470:                 foreach($headers as $modelName => $title){
 471:                     $model = $models[$modelName];
 472:                     if($model instanceof CActiveRecord){
 473:                         $ia[$title] = array();
 474:                         $friendlyName = Yii::t('app', rtrim($modelName, 's'));
 475:                         foreach($model->attributeLabels() as $fieldName => $label){
 476:                             $attr = trim($model->renderAttribute($fieldName, false));
 477:                             $fullLabel = strtr($labelFormat, array(
 478:                                 '{model}' => $friendlyName,
 479:                                 '{attr}' => $label
 480:                                     ));
 481:                             if($attr !== '' && $attr != '&nbsp;')
 482:                                 $ia[$title][$fullLabel] = $attr;
 483:                         }
 484:                     }
 485:                 }
 486:             }
 487:             $this->_insertableAttributes = $ia;
 488:         }
 489:         return $this->_insertableAttributes;
 490:     }
 491: 
 492:     /**
 493:      * Magic getter for {@link recipientContacts}
 494:      */
 495:     public function getRecipientContacts(){
 496:         if(!isset($this->_recipientContacts)){
 497:             $contacts = array();
 498:             foreach($this->recipients as $target){
 499:                 $contacts[$target[1]] = Contacts::model()->findByEmail($target[1]);
 500:             }
 501:             $this->_recipientContacts = $contacts;
 502:         }
 503:         return $this->_recipientContacts;
 504:     }
 505: 
 506:     /**
 507:      * Magic getter for {@link recipients}
 508:      * @return array
 509:      */
 510:     public function getRecipients(){
 511:         if(empty($this->_recipients)){
 512:             $this->_recipients = array();
 513:             foreach(array('to', 'cc', 'bcc') as $recList){
 514:                 if(!empty($this->mailingList[$recList])){
 515:                     foreach($this->mailingList[$recList] as $target){
 516:                         $this->_recipients[] = $target;
 517:                     }
 518:                 }
 519:             }
 520:         }
 521:         return $this->_recipients;
 522:     }
 523: 
 524:     /**
 525:      * @return bool false if any one of the recipient contacts has their doNotEmail field set to 
 526:      *  true, true otherwise
 527:      */
 528:     public function checkDoNotEmailFields () {
 529:         $allRecipientContacts = array();
 530:         foreach($this->recipients as $target){
 531:             foreach (
 532:                 Contacts::model()->findAllByAttributes(
 533:                     array('email' => $target[1]),
 534:                     'visibility!=:private OR assignedTo!="Anyone"',
 535:                     array ( 
 536:                         ':private' => X2PermissionsBehavior::VISIBILITY_PRIVATE
 537:                     )
 538:                 ) as $contact) {
 539: 
 540:                 $allRecipientContacts[] = $contact;
 541:             }
 542:         }
 543:         if (array_reduce (
 544:             $allRecipientContacts,
 545:             function ($carry, $item) {
 546:                 return $carry || $item->doNotEmail;
 547:             }, false)) {
 548:         
 549:             return false; 
 550:         }
 551:         return true;
 552:     }
 553: 
 554: 
 555:     /**
 556:      * Magic getter for {@link signature}
 557:      *
 558:      * Retrieves the email signature from the preexisting body, or from the
 559:      * user's profile if none can be found.
 560:      *
 561:      * @return string
 562:      */
 563:     public function getSignature(){
 564:         if(!isset($this->_signature)){
 565:             $profile = $this->getUserProfile();
 566:             if(!empty($profile))
 567:                 $this->_signature = $this->getUserProfile()->getSignature(true);
 568:             else
 569:                 $this->_signature = null;
 570:         }
 571:         return $this->_signature;
 572:     }
 573: 
 574:     /**
 575:      * Magic getter for {@link targetModel}
 576:      */
 577:     public function getTargetModel(){
 578:         if(!isset($this->_targetModel)){
 579:             if(!empty ($this->modelId) && !empty ($this->modelName)){
 580:                 $this->_targetModel = X2Model::model($this->modelName)->findByPk($this->modelId);
 581:                 if($this->_targetModel === null)
 582:                     $this->_targetModel = false;
 583:             } else{
 584:                 $this->_targetModel = false;
 585:             }
 586: //          if(!(bool) $this->_targetModel)
 587: //              throw new Exception('InlineEmail used on a target model name and primary key that matched no existing record.');
 588:         }
 589:         return $this->_targetModel;
 590:     }
 591: 
 592:     public function setTargetModel(X2Model $model){
 593:         $this->_targetModel = $model;
 594:     }
 595: 
 596:     /**
 597:      * Magic getter for {@link templateModel}
 598:      * @return type
 599:      */
 600:     public function getTemplateModel($id = null){
 601:         $newTemp = !empty($id);
 602:         if($newTemp){
 603:             $this->template = $id;
 604:             $this->_templateModel = null;
 605:         }else{
 606:             $id = $this->template;
 607:         }
 608:         if(empty($this->_templateModel)){
 609:             $this->_templateModel = Docs::model()->findByPk($id);
 610:         }
 611:         return $this->_templateModel;
 612:     }
 613: 
 614:     /**
 615:      * Magic getter for {@link trackingImage}
 616:      * @return type
 617:      */
 618:     public function getTrackingImage(){
 619:         if(!isset($this->_uniqueId, $this->_trackingImage)){
 620:             $this->_trackingImage = null;
 621:             $trackUrl = null;
 622:             if(!Yii::app()->params->noSession){
 623:                 $trackUrl = Yii::app()->createExternalUrl('/actions/actions/emailOpened', array('uid' => $this->uniqueId, 'type' => 'open'));
 624:             }else{
 625:                 // This might be a console application! In that case, there's
 626:                 // no controller application component available.
 627:                 $url = rtrim(Yii::app()->absoluteBaseUrl,'/');
 628: 
 629:                 if(!empty($url))
 630:                     $trackUrl = "$url/index.php/actions/emailOpened?uid={$this->uniqueId}&type=open";
 631:                 else
 632:                     $trackUrl = null;
 633:             }
 634:             if($trackUrl != null)
 635:                 $this->_trackingImage = '<img src="'.$trackUrl.'"/>';
 636:         }
 637:         return $this->_trackingImage;
 638:     }
 639: 
 640:     /**
 641:      * Magic setter for {@link uniqueId}
 642:      */
 643:     public function getUniqueId(){
 644:         if(empty($this->_uniqueId))
 645:             $this->_uniqueId = md5(uniqid(rand(), true));
 646:         return $this->_uniqueId;
 647:     }
 648: 
 649:     /**
 650:      * Magic setter for {@link uniqueId}
 651:      * @param string $value
 652:      */
 653:     public function setUniqueId($value){
 654:         $this->_uniqueId = $value;
 655:     }
 656: 
 657:     /**
 658:      * Validation function for lists of email addresses.
 659:      *
 660:      * @param string $attribute
 661:      * @param array $params
 662:      */
 663:     public function parseMailingList($attribute, $params = array()){
 664:         // First, convert the mailing list into an array of addresses.
 665:         // Use EmailDeliveryBehavior's recipient header parsing method,
 666:         // addressHeaderToArray, to do the heavy lifting.
 667:         try {
 668:             $this->mailingList[$attribute] = self::addressHeaderToArray($this->$attribute);
 669:         } catch (CException $e) {
 670:             $this->addError($attribute, $e->getMessage());
 671:         }   
 672:     }
 673: 
 674:     /**
 675:      * Inserts a signature into the body, if none can be found.
 676:      * @param array $wrap Wrap the signature in tags (index 0 opens, index 1 closes)
 677:      */
 678:     public function insertSignature($wrap = array('<br /><br />', '')){
 679:         if(preg_match(self::insertedPattern('signature', '(.*)', 1, 'um'), $this->message, $matches)){
 680:             $this->_signature = $matches[1];
 681:         }else{
 682:             $sig = self::insertedPattern('signature', $this->signature);
 683:             if(count($wrap) >= 2){
 684:                 $sig = $wrap[0].$sig.$wrap[1];
 685:             }
 686:             if(strpos($this->message, '{signature}')){
 687:                 $this->message = str_replace('{signature}', $sig, $this->message);
 688:             }else if($this->scenario != 'custom'){
 689:                 $this->insertInBody($sig);
 690:             }
 691:         }
 692:         $this->insertInBody("<div>&nbsp;</div>");
 693:     }
 694: 
 695:     /**
 696:      * Search for an existing tracking image and insert a new one if none are present.
 697:      *
 698:      * Parses the tracking image and unique ID out of the body if there are any.
 699:      *
 700:      * The email will be tracked, but only if one and only one of the recipients
 701:      * corresponds to a contact in X2Engine (remember, the user can switch the
 702:      * recipient list at the last minute by modifying the "To:" field).
 703:      *
 704:      * Otherwise, there's absolutely no way of telling with any certainty who
 705:      * exactly opened the email (all recipients will be sent the same email,
 706:      * so any one of them could be the one who opens the email and accesses the
 707:      * email tracking image). Thus, in such cases, it is pointless to create an
 708:      * event/action that says "so-and so has opened an email" because who opened
 709:      * the email is ambiguous and practically unknowable, and thus impractical
 710:      * to create an email tracking record.
 711:      *
 712:      * @param bool $replace reset the image markup and unique ID, and replace
 713:      *  the existing tracking image.
 714:      */
 715:     public function insertTrackingImage($replace = false){
 716:         $recipientContacts = $this->recipientContacts;
 717:         if(count($recipientContacts) == 1){ 
 718: 
 719:             // Note, if there is more than one contact in the recipient list, it is
 720:             // impossible to distinguish who opened the email, because both will be
 721:             // sent the same email. Thus it will be disabled for this use case until
 722:             // we have time to re-write this class a bit so that it supports sending
 723:             // distinct email bodies for each different recipient (so that each can
 724:             // have its own different tracking image)
 725:             $theContact = reset($recipientContacts);
 726:             if(!empty($theContact)){ // The one person who was sent an email is an existing contact
 727:                 $insertNew = true;
 728:                 $pattern = self::insertedPattern('track', '(<img.*\/>)', 1, 'u');
 729:                 if(preg_match($pattern, $this->message, $matchImg)){
 730:                     if($replace){
 731:                         // Reset unique ID and insert a new tracking image with a new unique ID
 732:                         $this->_trackingImage = null;
 733:                         $this->_uniqueId = null;
 734:                         $this->message = replace_string(
 735:                             $matchImg[0], self::insertedPattern('track', $this->trackingImage), 
 736:                             $this->message);
 737:                     }else{
 738:                         $this->_trackingImage = $matchImg[1];
 739:                         if(preg_match(self::UIDREGEX, $this->_trackingImage, $matchId)){
 740:                             $this->_uniqueId = $matchId[1];
 741:                             $insertNew = false;
 742:                         }
 743:                     }
 744:                 }
 745:                 if($insertNew){
 746:                     $this->insertInBody(self::insertedPattern('track', $this->trackingImage));
 747:                 }
 748:             }
 749:         }
 750:     }
 751: 
 752:     public static function extractTrackingUid($body) {
 753:         $pattern = self::insertedPattern('track', '(<img.*\/>)', 1, 'u');
 754:         if (preg_match ($pattern, $body, $matchImg)) {
 755:             if (preg_match (self::UIDREGEX, $matchImg[1], $matchId)) {
 756:                 return $matchId[1];
 757:             }
 758:         }
 759:     }
 760: 
 761:     /**
 762:      * Inserts something near the end of the body in the HTML email.
 763:      *
 764:      * @param string $content The markup/text to be inserted.
 765:      * @param bool $beginning True to insert at the beginning, false to insert at the end.
 766:      * @param bool $return True to modify {@link message}; false to return the modified body instead.
 767:      */
 768:     public function insertInBody($content, $beginning = 0, $return = 0){
 769:         if($beginning)
 770:             $newBody = preg_replace('/(?:<body[^>]*>)/','$0{content}',$this->message);
 771:         else
 772:             $newBody = str_replace('</body>', '{content}</body>', $this->message);
 773:         $newBody = str_replace('{content}',$content,$newBody);
 774:         if($return)
 775:             return $newBody;
 776:         else
 777:             $this->message = $newBody;
 778:     }
 779: 
 780:     /**
 781:      * Generate a blank HTML document.
 782:      *
 783:      * @param string $content Optional content to start with.
 784:      */
 785:     public static function emptyBody($content = null){
 786:         return "<html><head></head><body>$content</body></html>";
 787:     }
 788: 
 789:     /**
 790:      * Prepare the email body for sending or customization by the user.
 791:      */
 792:     public function prepareBody($postReplace = 0){
 793:         if(!$this->validate()){
 794:             return false;
 795:         }
 796:         // Replace the existing body, if any, with a template, i.e. for initial
 797:         // set-up or an automated email.
 798:         if($this->scenario === 'template' ){
 799:             // Get the template and associated model
 800: 
 801:             if(!empty($this->templateModel)){
 802:                 if ($this->templateModel->emailTo !== null) {
 803:                     $this->to = Docs::replaceVariables(
 804:                         $this->templateModel->emailTo, $this->targetModel, array (), false, false);
 805:                 }
 806:                 // Replace variables in the subject and body of the email
 807:                 $this->subject = Docs::replaceVariables($this->templateModel->subject, $this->targetModel);
 808:                 // if(!empty($this->targetModel)) {
 809:                 $this->message = Docs::replaceVariables($this->templateModel->text, $this->targetModel, array('{signature}' => self::insertedPattern('signature', $this->signature)));
 810:                 // } else {
 811:                 // $this->insertInBody('<span style="color:red">'.Yii::t('app','Error: attempted using a template, but the referenced model was not found.').'</span>');
 812:                 // }
 813:             }else{
 814:                 // No template?
 815:                 $this->message = self::emptyBody();
 816:                 $this->insertSignature();
 817:             }
 818:         }else if($postReplace){
 819:             $this->subject = Docs::replaceVariables($this->subject, $this->targetModel);
 820:             $this->message = Docs::replaceVariables($this->message, $this->targetModel);
 821:         }
 822: 
 823:         return true;
 824:     }
 825: 
 826:     /**
 827:      * Performs a send (or stage, or some other action).
 828:      *
 829:      * The tracking image is inserted at the very last moment before sending, so
 830:      * that there is no chance of the user altering the body and deleting it
 831:      * accidentally.
 832:      *
 833:      * @return array
 834:      */
 835:     public function send($createEvent = true){
 836:         $this->insertTrackingImage();
 837:         $this->status = $this->deliver();
 838:         if($this->status['code'] == '200') {
 839:             $this->recordEmailSent($createEvent); // Save all the actions and events
 840:             $this->clearTemporaryFiles ($this->attachments);
 841:         }
 842:         return $this->status;
 843:     }
 844: 
 845:     /**
 846:      * Save the tracking record for this email, but only if an image was inserted.
 847:      *
 848:      * @param integer $actionId ID of the email-type action corresponding to the record.
 849:      */
 850:     public function trackEmail($actionId){
 851:         if(isset($this->_uniqueId)){
 852:             $track = new TrackEmail;
 853:             $track->actionId = $actionId;
 854:             $track->uniqueId = $this->uniqueId;
 855:             $track->save();
 856:         }
 857:     }
 858: 
 859:     /**
 860:      * Make records of the email in every shape and form.
 861:      *
 862:      * This method is to be called only once the email has been sent.
 863:      *
 864:      * The basic principle behind what all is happening here: emails are getting
 865:      * sent to people. Since the "To:" field in the inline email form is not
 866:      * read-only, the emails could be sent to completely different people. Thus,
 867:      * creating action and event records must be based exclusively on who the
 868:      * email is addressed to and not the model from whose view the inline email
 869:      * form (if that's how this model is being used) is submitted.
 870:      */
 871:     public function recordEmailSent($makeEvent = true){
 872:          
 873: 
 874:         // The email record, with action header for display purposes:
 875:         $emailRecordBody = $this->insertInBody(self::insertedPattern('ah', $this->actionHeader), 1, 1);
 876:         $now = time();
 877:         $recipientContacts = array_filter($this->recipientContacts);
 878: 
 879:         if(!empty($this->targetModel)){
 880:             $model = $this->targetModel;
 881:             if((bool) $model){
 882:                 if($model->hasAttribute('lastActivity')){
 883:                     $model->lastActivity = $now;
 884:                     $model->save();
 885:                 }
 886:             }
 887: 
 888:             $action = new Actions;
 889:             // These attributes will be the same regardless of the type of
 890:             // email being sent:
 891:             $action->completedBy = $this->userProfile->username;
 892:             $action->createDate = $now;
 893:             $action->dueDate = $now;
 894:             $action->subject = $this->subject;
 895:             $action->completeDate = $now;
 896:             $action->complete = 'Yes';
 897:             $action->actionDescription = $emailRecordBody;
 898:             
 899: 
 900:             // These attributes are context-sensitive and subject to change:
 901:             $action->associationId = $model->id;
 902:             $action->associationType = $model->module;
 903:             $action->type = 'email';
 904:             $action->visibility = isset($model->visibility) ? $model->visibility : 1;
 905:             $action->assignedTo = $this->userProfile->username;
 906:             if($this->modelName == 'Quote'){
 907:                 // Is an email being sent to the primary
 908:                 // contact on the quote? If so, the user is "issuing" the quote or
 909:                 // invoice, and it should have a special type.
 910:                 if(!empty($this->targetModel->contact)){
 911:                     if(array_key_exists($this->targetModel->contact->email, $recipientContacts)){
 912:                         $action->associationType = lcfirst(get_class($model));
 913:                         $action->associationId = $model->id;
 914:                         $action->type .= '_'.($model->type == 'invoice' ? 'invoice' : 'quote');
 915:                         $action->visibility = 1;
 916:                         $action->assignedTo = $model->assignedTo;
 917:                     }
 918:                 }
 919:             }
 920: 
 921:             if($makeEvent && $action->save()){
 922:                 $this->trackEmail($action->id);
 923:                 // Create a corresponding event record. Note that special cases
 924:                 // may have to be written in the method Events->getText to
 925:                 // accommodate special association types apart from contacts,
 926:                 // in addition to special-case-handling here.
 927:                 if($makeEvent){
 928:                     $event = new Events;
 929:                     $event->type = 'email_sent';
 930:                     $event->subtype = 'email';
 931:                     $event->associationType = $model->myModelName;
 932:                     $event->associationId = $model->id;
 933:                     $event->user = $this->userProfile->username;
 934: 
 935:                     if($this->modelName == 'Quote'){
 936:                         // Special "quote issued" or "invoice issued" event:
 937:                         $event->subtype = 'quote';
 938:                         if($this->targetModel->type == 'invoice')
 939:                             $event->subtype = 'invoice';
 940:                         $event->associationType = $this->modelName;
 941:                         $event->associationId = $this->modelId;
 942:                     }
 943:                     $event->save();
 944:                 }
 945:             }
 946:         }
 947: 
 948:         // Create action history events and event feed events for all contacts that were in the 
 949:         // recipient list:
 950:         if($this->contactFlag){
 951:             foreach($recipientContacts as $email => $contact){
 952:                 $contact->lastActivity = $now;
 953:                 $contact->update(array('lastActivity'));
 954: 
 955:                 $skip = false;
 956:                 $skipEvent = false;
 957:                 if($this->targetModel && get_class ($this->targetModel) === 'Contacts'){
 958:                     $skip = $this->targetModel->id == $contact->id;
 959:                 }else if($this->modelName == 'Quote'){
 960:                     // An event has already been made for issuing the quote and
 961:                     // so another event would be redundant.
 962:                     $skipEvent = $this->targetModel->associatedContacts == $contact->nameId;
 963:                 }
 964:                 if($skip)
 965:                 // Only save the action history item/event if this hasn't
 966:                 // already been done.
 967:                     continue;
 968: 
 969:                 // These attributes will be the same regardless of the type of
 970:                 // email being sent:
 971:                 $action = new Actions;
 972:                 $action->completedBy = $this->userProfile->username;
 973:                 $action->createDate = $now;
 974:                 $action->dueDate = $now;
 975:                 $action->completeDate = $now;
 976:                 $action->complete = 'Yes';
 977: 
 978:                 // These attributes are context-sensitive and subject to change:
 979:                 $action->associationId = $contact->id;
 980:                 $action->associationType = 'contacts';
 981:                 $action->type = 'email';
 982:                 $action->visibility = isset($contact->visibility) ? $contact->visibility : 1;
 983:                 $action->assignedTo = $this->userProfile->username;
 984: 
 985:                 // Set the action's text to the modified email body
 986:                 $action->actionDescription = $emailRecordBody;
 987:                 // We don't really care about changelog events for emails; they're
 988:                 // set in stone anyways.
 989:                 $action->disableBehavior('changelog');
 990: 
 991:                 if($action->save()){
 992:                     // Now create an event for it:
 993:                     if($makeEvent && !$skipEvent){
 994:                         $event = new Events;
 995:                         $event->type = 'email_sent';
 996:                         $event->subtype = 'email';
 997:                         $event->associationType = $contact->myModelName;
 998:                         $event->associationId = $contact->id;
 999:                         $event->user = $this->userProfile->username;
1000:                         $event->save();
1001:                     }
1002:                 }
1003:             } // Loop over contacts
1004:         } // Conditional statement: do all this only if the flag to perform action history creation for all contacts has been set
1005:         // At this stage, if email tracking is to take place, "$action" should
1006:         // refer to the action history item of the one and only recipient contact,
1007:         // because there has been only one element in the recipient array to loop
1008:         // over. If the target model is a contact, and the one recipient is the
1009:         // contact itself, the action will be as declared before the above loop,
1010:         // and it will thus still be properly associated with that contact.
1011:     }
1012: 
1013:     public function deliver() {
1014:         return $this->asa('emailDelivery')->deliverEmail($this->mailingList,$this->subject,$this->message,$this->attachments);
1015:     }
1016: 
1017:     /**
1018:      * Insert a "Do Not Email" link into the body of the email message. The link contains the 
1019:      * contact's trackingKey in it's get parameters. When clicked, the contact's doNotEmail field
1020:      * will be set to 1.
1021:      * @param Contacts $contact
1022:      */
1023:     public function appendDoNotEmailLink (Contacts $contact) {
1024:         // Insert unsubscribe link placeholder in the email body if there is
1025:         // none already:
1026:         if(!preg_match('/\{doNotEmailLink\}/', $this->message)){
1027:             $doNotEmailLinkText = "<br/>\n-----------------------<br/>\n"
1028:                     .Yii::t('app', 
1029:                         'To stop receiving emails from this sender, click here').
1030:                     ": {doNotEmailLink}";
1031:             // Insert
1032:             if(strpos($this->message,'</body>')!==false) {
1033:                 $this->message = str_replace(
1034:                     '</body>',$doNotEmailLinkText.'</body>',$this->message);
1035:             } else {
1036:                 $this->message .= $doNotEmailLinkText;
1037:             }
1038:         }
1039: 
1040:         // Insert do not email link(s):
1041:         $doNotEmailUrl = Yii::app()->createExternalUrl(
1042:             '/marketing/marketing/doNotEmailLinkClick', array(
1043:                 'x2_key' => $contact->trackingKey,
1044:             ));
1045:         if (Yii::app()->settings->doNotEmailLinkText !== null) {
1046:             $linkText = Yii::app()->settings->doNotEmailLinkText;
1047:         } else {
1048:             $linkText = Admin::getDoNotEmailLinkDefaultText ();
1049:         }
1050:         $this->message = preg_replace(
1051:             '/\{doNotEmailLink\}/', '<a href="'.$doNotEmailUrl.'">'.
1052:             $linkText.'</a>', $this->message);
1053:     }
1054: 
1055: 
1056: }
1057: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0