[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/metamta/storage/ -> PhabricatorMetaMTAMail.php (source)

   1  <?php
   2  
   3  /**
   4   * @task recipients   Managing Recipients
   5   */
   6  final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
   7  
   8    const STATUS_QUEUE = 'queued';
   9    const STATUS_SENT  = 'sent';
  10    const STATUS_FAIL  = 'fail';
  11    const STATUS_VOID  = 'void';
  12  
  13    const RETRY_DELAY   = 5;
  14  
  15    protected $parameters;
  16    protected $status;
  17    protected $message;
  18    protected $relatedPHID;
  19  
  20    private $recipientExpansionMap;
  21  
  22    public function __construct() {
  23  
  24      $this->status     = self::STATUS_QUEUE;
  25      $this->parameters = array();
  26  
  27      parent::__construct();
  28    }
  29  
  30    public function getConfiguration() {
  31      return array(
  32        self::CONFIG_SERIALIZATION => array(
  33          'parameters'  => self::SERIALIZATION_JSON,
  34        ),
  35        self::CONFIG_COLUMN_SCHEMA => array(
  36          'status' => 'text32',
  37          'relatedPHID' => 'phid?',
  38  
  39          // T6203/NULLABILITY
  40          // This should just be empty if there's no body.
  41          'message' => 'text?',
  42        ),
  43        self::CONFIG_KEY_SCHEMA => array(
  44          'status' => array(
  45            'columns' => array('status'),
  46          ),
  47          'relatedPHID' => array(
  48            'columns' => array('relatedPHID'),
  49          ),
  50          'key_created' => array(
  51            'columns' => array('dateCreated'),
  52          ),
  53        ),
  54      ) + parent::getConfiguration();
  55    }
  56  
  57    protected function setParam($param, $value) {
  58      $this->parameters[$param] = $value;
  59      return $this;
  60    }
  61  
  62    protected function getParam($param, $default = null) {
  63      return idx($this->parameters, $param, $default);
  64    }
  65  
  66    /**
  67     * Set tags (@{class:MetaMTANotificationType} constants) which identify the
  68     * content of this mail in a general way. These tags are used to allow users
  69     * to opt out of receiving certain types of mail, like updates when a task's
  70     * projects change.
  71     *
  72     * @param list<const> List of @{class:MetaMTANotificationType} constants.
  73     * @return this
  74     */
  75    public function setMailTags(array $tags) {
  76      $this->setParam('mailtags', array_unique($tags));
  77      return $this;
  78    }
  79  
  80    public function getMailTags() {
  81      return $this->getParam('mailtags', array());
  82    }
  83  
  84    /**
  85     * In Gmail, conversations will be broken if you reply to a thread and the
  86     * server sends back a response without referencing your Message-ID, even if
  87     * it references a Message-ID earlier in the thread. To avoid this, use the
  88     * parent email's message ID explicitly if it's available. This overwrites the
  89     * "In-Reply-To" and "References" headers we would otherwise generate. This
  90     * needs to be set whenever an action is triggered by an email message. See
  91     * T251 for more details.
  92     *
  93     * @param   string The "Message-ID" of the email which precedes this one.
  94     * @return  this
  95     */
  96    public function setParentMessageID($id) {
  97      $this->setParam('parent-message-id', $id);
  98      return $this;
  99    }
 100  
 101    public function getParentMessageID() {
 102      return $this->getParam('parent-message-id');
 103    }
 104  
 105    public function getSubject() {
 106      return $this->getParam('subject');
 107    }
 108  
 109    public function addTos(array $phids) {
 110      $phids = array_unique($phids);
 111      $this->setParam('to', $phids);
 112      return $this;
 113    }
 114  
 115    public function addRawTos(array $raw_email) {
 116  
 117      // Strip addresses down to bare emails, since the MailAdapter API currently
 118      // requires we pass it just the address (like `[email protected]`), not
 119      // a full string like `"Abraham Lincoln" <[email protected]>`.
 120      foreach ($raw_email as $key => $email) {
 121        $object = new PhutilEmailAddress($email);
 122        $raw_email[$key] = $object->getAddress();
 123      }
 124  
 125      $this->setParam('raw-to', $raw_email);
 126      return $this;
 127    }
 128  
 129    public function addCCs(array $phids) {
 130      $phids = array_unique($phids);
 131      $this->setParam('cc', $phids);
 132      return $this;
 133    }
 134  
 135    public function setExcludeMailRecipientPHIDs(array $exclude) {
 136      $this->setParam('exclude', $exclude);
 137      return $this;
 138    }
 139  
 140    private function getExcludeMailRecipientPHIDs() {
 141      return $this->getParam('exclude', array());
 142    }
 143  
 144    public function getTranslation(array $objects) {
 145      $default_translation = PhabricatorEnv::getEnvConfig('translation.provider');
 146      $return = null;
 147      $recipients = array_merge(
 148        idx($this->parameters, 'to', array()),
 149        idx($this->parameters, 'cc', array()));
 150      foreach (array_select_keys($objects, $recipients) as $object) {
 151        $translation = null;
 152        if ($object instanceof PhabricatorUser) {
 153          $translation = $object->getTranslation();
 154        }
 155        if (!$translation) {
 156          $translation = $default_translation;
 157        }
 158        if ($return && $translation != $return) {
 159          return $default_translation;
 160        }
 161        $return = $translation;
 162      }
 163  
 164      if (!$return) {
 165        $return = $default_translation;
 166      }
 167  
 168      return $return;
 169    }
 170  
 171    public function addPHIDHeaders($name, array $phids) {
 172      foreach ($phids as $phid) {
 173        $this->addHeader($name, '<'.$phid.'>');
 174      }
 175      return $this;
 176    }
 177  
 178    public function addHeader($name, $value) {
 179      $this->parameters['headers'][] = array($name, $value);
 180      return $this;
 181    }
 182  
 183    public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
 184      $this->parameters['attachments'][] = $attachment->toDictionary();
 185      return $this;
 186    }
 187  
 188    public function getAttachments() {
 189      $dicts = $this->getParam('attachments');
 190  
 191      $result = array();
 192      foreach ($dicts as $dict) {
 193        $result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict);
 194      }
 195      return $result;
 196    }
 197  
 198    public function setAttachments(array $attachments) {
 199      assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
 200      $this->setParam('attachments', mpull($attachments, 'toDictionary'));
 201      return $this;
 202    }
 203  
 204    public function setFrom($from) {
 205      $this->setParam('from', $from);
 206      return $this;
 207    }
 208  
 209    public function setReplyTo($reply_to) {
 210      $this->setParam('reply-to', $reply_to);
 211      return $this;
 212    }
 213  
 214    public function setSubject($subject) {
 215      $this->setParam('subject', $subject);
 216      return $this;
 217    }
 218  
 219    public function setSubjectPrefix($prefix) {
 220      $this->setParam('subject-prefix', $prefix);
 221      return $this;
 222    }
 223  
 224    public function setVarySubjectPrefix($prefix) {
 225      $this->setParam('vary-subject-prefix', $prefix);
 226      return $this;
 227    }
 228  
 229    public function setBody($body) {
 230      $this->setParam('body', $body);
 231      return $this;
 232    }
 233  
 234    public function setHTMLBody($html) {
 235      $this->setParam('html-body', $html);
 236      return $this;
 237    }
 238  
 239    public function getBody() {
 240      return $this->getParam('body');
 241    }
 242  
 243    public function getHTMLBody() {
 244      return $this->getParam('html-body');
 245    }
 246  
 247    public function setIsErrorEmail($is_error) {
 248      $this->setParam('is-error', $is_error);
 249      return $this;
 250    }
 251  
 252    public function getIsErrorEmail() {
 253      return $this->getParam('is-error', false);
 254    }
 255  
 256    public function getToPHIDs() {
 257      return $this->getParam('to', array());
 258    }
 259  
 260    public function getRawToAddresses() {
 261      return $this->getParam('raw-to', array());
 262    }
 263  
 264    public function getCcPHIDs() {
 265      return $this->getParam('cc', array());
 266    }
 267  
 268    /**
 269     * Force delivery of a message, even if recipients have preferences which
 270     * would otherwise drop the message.
 271     *
 272     * This is primarily intended to let users who don't want any email still
 273     * receive things like password resets.
 274     *
 275     * @param bool  True to force delivery despite user preferences.
 276     * @return this
 277     */
 278    public function setForceDelivery($force) {
 279      $this->setParam('force', $force);
 280      return $this;
 281    }
 282  
 283    public function getForceDelivery() {
 284      return $this->getParam('force', false);
 285    }
 286  
 287    /**
 288     * Flag that this is an auto-generated bulk message and should have bulk
 289     * headers added to it if appropriate. Broadly, this means some flavor of
 290     * "Precedence: bulk" or similar, but is implementation and configuration
 291     * dependent.
 292     *
 293     * @param bool  True if the mail is automated bulk mail.
 294     * @return this
 295     */
 296    public function setIsBulk($is_bulk) {
 297      $this->setParam('is-bulk', $is_bulk);
 298      return $this;
 299    }
 300  
 301    /**
 302     * Use this method to set an ID used for message threading. MetaMTA will
 303     * set appropriate headers (Message-ID, In-Reply-To, References and
 304     * Thread-Index) based on the capabilities of the underlying mailer.
 305     *
 306     * @param string  Unique identifier, appropriate for use in a Message-ID,
 307     *                In-Reply-To or References headers.
 308     * @param bool    If true, indicates this is the first message in the thread.
 309     * @return this
 310     */
 311    public function setThreadID($thread_id, $is_first_message = false) {
 312      $this->setParam('thread-id', $thread_id);
 313      $this->setParam('is-first-message', $is_first_message);
 314      return $this;
 315    }
 316  
 317    /**
 318     * Save a newly created mail to the database. The mail will eventually be
 319     * delivered by the MetaMTA daemon.
 320     *
 321     * @return this
 322     */
 323    public function saveAndSend() {
 324      return $this->save();
 325    }
 326  
 327    public function save() {
 328      if ($this->getID()) {
 329        return parent::save();
 330      }
 331  
 332      // NOTE: When mail is sent from CLI scripts that run tasks in-process, we
 333      // may re-enter this method from within scheduleTask(). The implementation
 334      // is intended to avoid anything awkward if we end up reentering this
 335      // method.
 336  
 337      $this->openTransaction();
 338        // Save to generate a task ID.
 339        $result = parent::save();
 340  
 341        // Queue a task to send this mail.
 342        $mailer_task = PhabricatorWorker::scheduleTask(
 343          'PhabricatorMetaMTAWorker',
 344          $this->getID(),
 345          PhabricatorWorker::PRIORITY_ALERTS);
 346  
 347      $this->saveTransaction();
 348  
 349      return $result;
 350    }
 351  
 352    public function buildDefaultMailer() {
 353      return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
 354    }
 355  
 356  
 357    /**
 358     * Attempt to deliver an email immediately, in this process.
 359     *
 360     * @param bool  Try to deliver this email even if it has already been
 361     *              delivered or is in backoff after a failed delivery attempt.
 362     * @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
 363     *              instead of the default.
 364     *
 365     * @return void
 366     */
 367    public function sendNow(
 368      $force_send = false,
 369      PhabricatorMailImplementationAdapter $mailer = null) {
 370  
 371      if ($mailer === null) {
 372        $mailer = $this->buildDefaultMailer();
 373      }
 374  
 375      if (!$force_send) {
 376        if ($this->getStatus() != self::STATUS_QUEUE) {
 377          throw new Exception('Trying to send an already-sent mail!');
 378        }
 379      }
 380  
 381      try {
 382        $params = $this->parameters;
 383  
 384        $actors = $this->loadAllActors();
 385        $deliverable_actors = $this->filterDeliverableActors($actors);
 386  
 387        $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
 388        if (empty($params['from'])) {
 389          $mailer->setFrom($default_from);
 390        }
 391  
 392        $is_first = idx($params, 'is-first-message');
 393        unset($params['is-first-message']);
 394  
 395        $is_threaded = (bool)idx($params, 'thread-id');
 396  
 397        $reply_to_name = idx($params, 'reply-to-name', '');
 398        unset($params['reply-to-name']);
 399  
 400        $add_cc = array();
 401        $add_to = array();
 402  
 403        // Only try to use preferences if everything is multiplexed, so we
 404        // get consistent behavior.
 405        $use_prefs = self::shouldMultiplexAllMail();
 406  
 407        $prefs = null;
 408        if ($use_prefs) {
 409  
 410          // If multiplexing is enabled, some recipients will be in "Cc"
 411          // rather than "To". We'll move them to "To" later (or supply a
 412          // dummy "To") but need to look for the recipient in either the
 413          // "To" or "Cc" fields here.
 414          $target_phid = head(idx($params, 'to', array()));
 415          if (!$target_phid) {
 416            $target_phid = head(idx($params, 'cc', array()));
 417          }
 418  
 419          if ($target_phid) {
 420            $user = id(new PhabricatorUser())->loadOneWhere(
 421              'phid = %s',
 422              $target_phid);
 423            if ($user) {
 424              $prefs = $user->loadPreferences();
 425            }
 426          }
 427        }
 428  
 429        foreach ($params as $key => $value) {
 430          switch ($key) {
 431            case 'from':
 432              $from = $value;
 433              $actor_email = null;
 434              $actor_name = null;
 435              $actor = idx($actors, $from);
 436              if ($actor) {
 437                $actor_email = $actor->getEmailAddress();
 438                $actor_name = $actor->getName();
 439              }
 440              $can_send_as_user = $actor_email &&
 441                PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
 442  
 443              if ($can_send_as_user) {
 444                $mailer->setFrom($actor_email, $actor_name);
 445              } else {
 446                $from_email = coalesce($actor_email, $default_from);
 447                $from_name = coalesce($actor_name, pht('Phabricator'));
 448  
 449                if (empty($params['reply-to'])) {
 450                  $params['reply-to'] = $from_email;
 451                  $params['reply-to-name'] = $from_name;
 452                }
 453  
 454                $mailer->setFrom($default_from, $from_name);
 455              }
 456              break;
 457            case 'reply-to':
 458              $mailer->addReplyTo($value, $reply_to_name);
 459              break;
 460            case 'to':
 461              $to_phids = $this->expandRecipients($value);
 462              $to_actors = array_select_keys($deliverable_actors, $to_phids);
 463              $add_to = array_merge(
 464                $add_to,
 465                mpull($to_actors, 'getEmailAddress'));
 466              break;
 467            case 'raw-to':
 468              $add_to = array_merge($add_to, $value);
 469              break;
 470            case 'cc':
 471              $cc_phids = $this->expandRecipients($value);
 472              $cc_actors = array_select_keys($deliverable_actors, $cc_phids);
 473              $add_cc = array_merge(
 474                $add_cc,
 475                mpull($cc_actors, 'getEmailAddress'));
 476              break;
 477            case 'headers':
 478              foreach ($value as $pair) {
 479                list($header_key, $header_value) = $pair;
 480  
 481                // NOTE: If we have \n in a header, SES rejects the email.
 482                $header_value = str_replace("\n", ' ', $header_value);
 483  
 484                $mailer->addHeader($header_key, $header_value);
 485              }
 486              break;
 487            case 'attachments':
 488              $value = $this->getAttachments();
 489              foreach ($value as $attachment) {
 490                $mailer->addAttachment(
 491                  $attachment->getData(),
 492                  $attachment->getFilename(),
 493                  $attachment->getMimeType());
 494              }
 495              break;
 496            case 'subject':
 497              $subject = array();
 498  
 499              if ($is_threaded) {
 500                $add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix');
 501  
 502                if ($prefs) {
 503                  $add_re = $prefs->getPreference(
 504                    PhabricatorUserPreferences::PREFERENCE_RE_PREFIX,
 505                    $add_re);
 506                }
 507  
 508                if ($add_re) {
 509                  $subject[] = 'Re:';
 510                }
 511              }
 512  
 513              $subject[] = trim(idx($params, 'subject-prefix'));
 514  
 515              $vary_prefix = idx($params, 'vary-subject-prefix');
 516              if ($vary_prefix != '') {
 517                $use_subject = PhabricatorEnv::getEnvConfig(
 518                  'metamta.vary-subjects');
 519  
 520                if ($prefs) {
 521                  $use_subject = $prefs->getPreference(
 522                    PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT,
 523                    $use_subject);
 524                }
 525  
 526                if ($use_subject) {
 527                  $subject[] = $vary_prefix;
 528                }
 529              }
 530  
 531              $subject[] = $value;
 532  
 533              $mailer->setSubject(implode(' ', array_filter($subject)));
 534              break;
 535            case 'is-bulk':
 536              if ($value) {
 537                if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) {
 538                  $mailer->addHeader('Precedence', 'bulk');
 539                }
 540              }
 541              break;
 542            case 'thread-id':
 543  
 544              // NOTE: Gmail freaks out about In-Reply-To and References which
 545              // aren't in the form "<[email protected]>"; this is also required
 546              // by RFC 2822, although some clients are more liberal in what they
 547              // accept.
 548              $domain = PhabricatorEnv::getEnvConfig('metamta.domain');
 549              $value = '<'.$value.'@'.$domain.'>';
 550  
 551              if ($is_first && $mailer->supportsMessageIDHeader()) {
 552                $mailer->addHeader('Message-ID',  $value);
 553              } else {
 554                $in_reply_to = $value;
 555                $references = array($value);
 556                $parent_id = $this->getParentMessageID();
 557                if ($parent_id) {
 558                  $in_reply_to = $parent_id;
 559                  // By RFC 2822, the most immediate parent should appear last
 560                  // in the "References" header, so this order is intentional.
 561                  $references[] = $parent_id;
 562                }
 563                $references = implode(' ', $references);
 564                $mailer->addHeader('In-Reply-To', $in_reply_to);
 565                $mailer->addHeader('References',  $references);
 566              }
 567              $thread_index = $this->generateThreadIndex($value, $is_first);
 568              $mailer->addHeader('Thread-Index', $thread_index);
 569              break;
 570            case 'mailtags':
 571              // Handled below.
 572              break;
 573            case 'subject-prefix':
 574            case 'vary-subject-prefix':
 575              // Handled above.
 576              break;
 577            default:
 578              // Just discard.
 579          }
 580        }
 581  
 582        $body = idx($params, 'body', '');
 583        $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
 584        if (strlen($body) > $max) {
 585          $body = id(new PhutilUTF8StringTruncator())
 586            ->setMaximumBytes($max)
 587            ->truncateString($body);
 588          $body .= "\n";
 589          $body .= pht('(This email was truncated at %d bytes.)', $max);
 590        }
 591        $mailer->setBody($body);
 592  
 593        $html_emails = false;
 594        if ($use_prefs && $prefs) {
 595          $html_emails = $prefs->getPreference(
 596            PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS,
 597            $html_emails);
 598        }
 599  
 600        if ($html_emails && isset($params['html-body'])) {
 601          $mailer->setHTMLBody($params['html-body']);
 602        }
 603  
 604        if (!$add_to && !$add_cc) {
 605          $this->setStatus(self::STATUS_VOID);
 606          $this->setMessage(
 607            'Message has no valid recipients: all To/Cc are disabled, invalid, '.
 608            'or configured not to receive this mail.');
 609          return $this->save();
 610        }
 611  
 612        if ($this->getIsErrorEmail()) {
 613          $all_recipients = array_merge($add_to, $add_cc);
 614          if ($this->shouldRateLimitMail($all_recipients)) {
 615            $this->setStatus(self::STATUS_VOID);
 616            $this->setMessage(
 617              pht(
 618                'This is an error email, but one or more recipients have '.
 619                'exceeded the error email rate limit. Declining to deliver '.
 620                'message.'));
 621            return $this->save();
 622          }
 623        }
 624  
 625        $mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes');
 626        $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
 627  
 628        // Some clients respect this to suppress OOF and other auto-responses.
 629        $mailer->addHeader('X-Auto-Response-Suppress', 'All');
 630  
 631        // If the message has mailtags, filter out any recipients who don't want
 632        // to receive this type of mail.
 633        $mailtags = $this->getParam('mailtags');
 634        if ($mailtags) {
 635          $tag_header = array();
 636          foreach ($mailtags as $mailtag) {
 637            $tag_header[] = '<'.$mailtag.'>';
 638          }
 639          $tag_header = implode(', ', $tag_header);
 640          $mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header);
 641        }
 642  
 643        // Some mailers require a valid "To:" in order to deliver mail. If we
 644        // don't have any "To:", try to fill it in with a placeholder "To:".
 645        // If that also fails, move the "Cc:" line to "To:".
 646        if (!$add_to) {
 647          $placeholder_key = 'metamta.placeholder-to-recipient';
 648          $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
 649          if ($placeholder !== null) {
 650            $add_to = array($placeholder);
 651          } else {
 652            $add_to = $add_cc;
 653            $add_cc = array();
 654          }
 655        }
 656  
 657        $add_to = array_unique($add_to);
 658        $add_cc = array_diff(array_unique($add_cc), $add_to);
 659  
 660        $mailer->addTos($add_to);
 661        if ($add_cc) {
 662          $mailer->addCCs($add_cc);
 663        }
 664      } catch (Exception $ex) {
 665        $this
 666          ->setStatus(self::STATUS_FAIL)
 667          ->setMessage($ex->getMessage())
 668          ->save();
 669  
 670        throw $ex;
 671      }
 672  
 673      try {
 674        $ok = $mailer->send();
 675        if (!$ok) {
 676          // TODO: At some point, we should clean this up and make all mailers
 677          // throw.
 678          throw new Exception(
 679            pht('Mail adapter encountered an unexpected, unspecified failure.'));
 680        }
 681  
 682        $this->setStatus(self::STATUS_SENT);
 683        $this->save();
 684  
 685        return $this;
 686      } catch (PhabricatorMetaMTAPermanentFailureException $ex) {
 687        $this
 688          ->setStatus(self::STATUS_FAIL)
 689          ->setMessage($ex->getMessage())
 690          ->save();
 691  
 692        throw $ex;
 693      } catch (Exception $ex) {
 694        $this
 695          ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString())
 696          ->save();
 697  
 698        throw $ex;
 699      }
 700    }
 701  
 702    public static function getReadableStatus($status_code) {
 703      static $readable = array(
 704        self::STATUS_QUEUE => 'Queued for Delivery',
 705        self::STATUS_FAIL  => 'Delivery Failed',
 706        self::STATUS_SENT  => 'Sent',
 707        self::STATUS_VOID  => 'Void',
 708      );
 709      $status_code = coalesce($status_code, '?');
 710      return idx($readable, $status_code, $status_code);
 711    }
 712  
 713    private function generateThreadIndex($seed, $is_first_mail) {
 714      // When threading, Outlook ignores the 'References' and 'In-Reply-To'
 715      // headers that most clients use. Instead, it uses a custom 'Thread-Index'
 716      // header. The format of this header is something like this (from
 717      // camel-exchange-folder.c in Evolution Exchange):
 718  
 719      /* A new post to a folder gets a 27-byte-long thread index. (The value
 720       * is apparently unique but meaningless.) Each reply to a post gets a
 721       * 32-byte-long thread index whose first 27 bytes are the same as the
 722       * parent's thread index. Each reply to any of those gets a
 723       * 37-byte-long thread index, etc. The Thread-Index header contains a
 724       * base64 representation of this value.
 725       */
 726  
 727      // The specific implementation uses a 27-byte header for the first email
 728      // a recipient receives, and a random 5-byte suffix (32 bytes total)
 729      // thereafter. This means that all the replies are (incorrectly) siblings,
 730      // but it would be very difficult to keep track of the entire tree and this
 731      // gets us reasonable client behavior.
 732  
 733      $base = substr(md5($seed), 0, 27);
 734      if (!$is_first_mail) {
 735        // Not totally sure, but it seems like outlook orders replies by
 736        // thread-index rather than timestamp, so to get these to show up in the
 737        // right order we use the time as the last 4 bytes.
 738        $base .= ' '.pack('N', time());
 739      }
 740  
 741      return base64_encode($base);
 742    }
 743  
 744    public static function shouldMultiplexAllMail() {
 745      return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
 746    }
 747  
 748  
 749  /* -(  Managing Recipients  )------------------------------------------------ */
 750  
 751  
 752    /**
 753     * Get all of the recipients for this mail, after preference filters are
 754     * applied. This list has all objects to whom delivery will be attempted.
 755     *
 756     * @return  list<phid>  A list of all recipients to whom delivery will be
 757     *                      attempted.
 758     * @task recipients
 759     */
 760    public function buildRecipientList() {
 761      $actors = $this->loadActors(
 762        array_merge(
 763          $this->getToPHIDs(),
 764          $this->getCcPHIDs()));
 765      $actors = $this->filterDeliverableActors($actors);
 766      return mpull($actors, 'getPHID');
 767    }
 768  
 769    public function loadAllActors() {
 770      $actor_phids = array_merge(
 771        array($this->getParam('from')),
 772        $this->getToPHIDs(),
 773        $this->getCcPHIDs());
 774  
 775      $this->loadRecipientExpansions($actor_phids);
 776      $actor_phids = $this->expandRecipients($actor_phids);
 777  
 778      return $this->loadActors($actor_phids);
 779    }
 780  
 781    private function loadRecipientExpansions(array $phids) {
 782      $expansions = id(new PhabricatorMetaMTAMemberQuery())
 783        ->setViewer(PhabricatorUser::getOmnipotentUser())
 784        ->withPHIDs($phids)
 785        ->execute();
 786  
 787      $this->recipientExpansionMap = $expansions;
 788  
 789      return $this;
 790    }
 791  
 792    /**
 793     * Expand a list of recipient PHIDs (possibly including aggregate recipients
 794     * like projects) into a deaggregated list of individual recipient PHIDs.
 795     * For example, this will expand project PHIDs into a list of the project's
 796     * members.
 797     *
 798     * @param list<phid>  List of recipient PHIDs, possibly including aggregate
 799     *                    recipients.
 800     * @return list<phid> Deaggregated list of mailable recipients.
 801     */
 802    private function expandRecipients(array $phids) {
 803      if ($this->recipientExpansionMap === null) {
 804        throw new Exception(
 805          pht(
 806            'Call loadRecipientExpansions() before expandRecipients()!'));
 807      }
 808  
 809      $results = array();
 810      foreach ($phids as $phid) {
 811        if (!isset($this->recipientExpansionMap[$phid])) {
 812          $results[$phid] = $phid;
 813        } else {
 814          foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
 815            $results[$recipient_phid] = $recipient_phid;
 816          }
 817        }
 818      }
 819  
 820      return array_keys($results);
 821    }
 822  
 823    private function filterDeliverableActors(array $actors) {
 824      assert_instances_of($actors, 'PhabricatorMetaMTAActor');
 825      $deliverable_actors = array();
 826      foreach ($actors as $phid => $actor) {
 827        if ($actor->isDeliverable()) {
 828          $deliverable_actors[$phid] = $actor;
 829        }
 830      }
 831      return $deliverable_actors;
 832    }
 833  
 834    private function loadActors(array $actor_phids) {
 835      $actor_phids = array_filter($actor_phids);
 836      $viewer = PhabricatorUser::getOmnipotentUser();
 837  
 838      $actors = id(new PhabricatorMetaMTAActorQuery())
 839        ->setViewer($viewer)
 840        ->withPHIDs($actor_phids)
 841        ->execute();
 842  
 843      if (!$actors) {
 844        return array();
 845      }
 846  
 847      if ($this->getForceDelivery()) {
 848        // If we're forcing delivery, skip all the opt-out checks.
 849        return $actors;
 850      }
 851  
 852      // Exclude explicit recipients.
 853      foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
 854        $actor = idx($actors, $phid);
 855        if (!$actor) {
 856          continue;
 857        }
 858        $actor->setUndeliverable(
 859          pht(
 860            'This message is a response to another email message, and this '.
 861            'recipient received the original email message, so we are not '.
 862            'sending them this substantially similar message (for example, '.
 863            'the sender used "Reply All" instead of "Reply" in response to '.
 864            'mail from Phabricator).'));
 865      }
 866  
 867      // Exclude the actor if their preferences are set.
 868      $from_phid = $this->getParam('from');
 869      $from_actor = idx($actors, $from_phid);
 870      if ($from_actor) {
 871        $from_user = id(new PhabricatorPeopleQuery())
 872          ->setViewer($viewer)
 873          ->withPHIDs(array($from_phid))
 874          ->execute();
 875        $from_user = head($from_user);
 876        if ($from_user) {
 877          $pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL;
 878          $exclude_self = $from_user
 879            ->loadPreferences()
 880            ->getPreference($pref_key);
 881          if ($exclude_self) {
 882            $from_actor->setUndeliverable(
 883              pht(
 884                'This recipient is the user whose actions caused delivery of '.
 885                'this message, but they have set preferences so they do not '.
 886                'receive mail about their own actions (Settings > Email '.
 887                'Preferences > Self Actions).'));
 888          }
 889        }
 890      }
 891  
 892      $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
 893        'userPHID in (%Ls)',
 894        $actor_phids);
 895      $all_prefs = mpull($all_prefs, null, 'getUserPHID');
 896  
 897      // Exclude recipients who don't want any mail.
 898      foreach ($all_prefs as $phid => $prefs) {
 899        $exclude = $prefs->getPreference(
 900          PhabricatorUserPreferences::PREFERENCE_NO_MAIL,
 901          false);
 902        if ($exclude) {
 903          $actors[$phid]->setUndeliverable(
 904            pht(
 905              'This recipient has disabled all email notifications '.
 906              '(Settings > Email Preferences > Email Notifications).'));
 907        }
 908      }
 909  
 910      $value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL;
 911  
 912      // Exclude all recipients who have set preferences to not receive this type
 913      // of email (for example, a user who says they don't want emails about task
 914      // CC changes).
 915      $tags = $this->getParam('mailtags');
 916      if ($tags) {
 917        foreach ($all_prefs as $phid => $prefs) {
 918          $user_mailtags = $prefs->getPreference(
 919            PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
 920            array());
 921  
 922          // The user must have elected to receive mail for at least one
 923          // of the mailtags.
 924          $send = false;
 925          foreach ($tags as $tag) {
 926            if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
 927              $send = true;
 928              break;
 929            }
 930          }
 931  
 932          if (!$send) {
 933            $actors[$phid]->setUndeliverable(
 934              pht(
 935                'This mail has tags which control which users receive it, and '.
 936                'this recipient has not elected to receive mail with any of '.
 937                'the tags on this message (Settings > Email Preferences).'));
 938          }
 939        }
 940      }
 941  
 942      return $actors;
 943    }
 944  
 945    private function shouldRateLimitMail(array $all_recipients) {
 946      try {
 947        PhabricatorSystemActionEngine::willTakeAction(
 948          $all_recipients,
 949          new PhabricatorMetaMTAErrorMailAction(),
 950          1);
 951        return false;
 952      } catch (PhabricatorSystemActionRateLimitException $ex) {
 953        return true;
 954      }
 955    }
 956  
 957  
 958  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1