PageRenderTime 74ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/metamta/storage/PhabricatorMetaMTAMail.php

http://github.com/facebook/phabricator
PHP | 1265 lines | 870 code | 225 blank | 170 comment | 57 complexity | 13a277e72ff7b02aa31bae92a4433652 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * @task recipients Managing Recipients
  4. */
  5. final class PhabricatorMetaMTAMail
  6. extends PhabricatorMetaMTADAO
  7. implements
  8. PhabricatorPolicyInterface,
  9. PhabricatorDestructibleInterface {
  10. const RETRY_DELAY = 5;
  11. protected $actorPHID;
  12. protected $parameters = array();
  13. protected $status;
  14. protected $message;
  15. protected $relatedPHID;
  16. private $recipientExpansionMap;
  17. private $routingMap;
  18. public function __construct() {
  19. $this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE;
  20. $this->parameters = array(
  21. 'sensitive' => true,
  22. 'mustEncrypt' => false,
  23. );
  24. parent::__construct();
  25. }
  26. protected function getConfiguration() {
  27. return array(
  28. self::CONFIG_AUX_PHID => true,
  29. self::CONFIG_SERIALIZATION => array(
  30. 'parameters' => self::SERIALIZATION_JSON,
  31. ),
  32. self::CONFIG_COLUMN_SCHEMA => array(
  33. 'actorPHID' => 'phid?',
  34. 'status' => 'text32',
  35. 'relatedPHID' => 'phid?',
  36. // T6203/NULLABILITY
  37. // This should just be empty if there's no body.
  38. 'message' => 'text?',
  39. ),
  40. self::CONFIG_KEY_SCHEMA => array(
  41. 'status' => array(
  42. 'columns' => array('status'),
  43. ),
  44. 'key_actorPHID' => array(
  45. 'columns' => array('actorPHID'),
  46. ),
  47. 'relatedPHID' => array(
  48. 'columns' => array('relatedPHID'),
  49. ),
  50. 'key_created' => array(
  51. 'columns' => array('dateCreated'),
  52. ),
  53. ),
  54. ) + parent::getConfiguration();
  55. }
  56. public function generatePHID() {
  57. return PhabricatorPHID::generateNewPHID(
  58. PhabricatorMetaMTAMailPHIDType::TYPECONST);
  59. }
  60. protected function setParam($param, $value) {
  61. $this->parameters[$param] = $value;
  62. return $this;
  63. }
  64. protected function getParam($param, $default = null) {
  65. // Some old mail was saved without parameters because no parameters were
  66. // set or encoding failed. Recover in these cases so we can perform
  67. // mail migrations, see T9251.
  68. if (!is_array($this->parameters)) {
  69. $this->parameters = array();
  70. }
  71. return idx($this->parameters, $param, $default);
  72. }
  73. /**
  74. * These tags are used to allow users to opt out of receiving certain types
  75. * of mail, like updates when a task's projects change.
  76. *
  77. * @param list<const>
  78. * @return this
  79. */
  80. public function setMailTags(array $tags) {
  81. $this->setParam('mailtags', array_unique($tags));
  82. return $this;
  83. }
  84. public function getMailTags() {
  85. return $this->getParam('mailtags', array());
  86. }
  87. /**
  88. * In Gmail, conversations will be broken if you reply to a thread and the
  89. * server sends back a response without referencing your Message-ID, even if
  90. * it references a Message-ID earlier in the thread. To avoid this, use the
  91. * parent email's message ID explicitly if it's available. This overwrites the
  92. * "In-Reply-To" and "References" headers we would otherwise generate. This
  93. * needs to be set whenever an action is triggered by an email message. See
  94. * T251 for more details.
  95. *
  96. * @param string The "Message-ID" of the email which precedes this one.
  97. * @return this
  98. */
  99. public function setParentMessageID($id) {
  100. $this->setParam('parent-message-id', $id);
  101. return $this;
  102. }
  103. public function getParentMessageID() {
  104. return $this->getParam('parent-message-id');
  105. }
  106. public function getSubject() {
  107. return $this->getParam('subject');
  108. }
  109. public function addTos(array $phids) {
  110. $phids = array_unique($phids);
  111. $this->setParam('to', $phids);
  112. return $this;
  113. }
  114. public function addRawTos(array $raw_email) {
  115. // Strip addresses down to bare emails, since the MailAdapter API currently
  116. // requires we pass it just the address (like `alincoln@logcabin.org`), not
  117. // a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
  118. foreach ($raw_email as $key => $email) {
  119. $object = new PhutilEmailAddress($email);
  120. $raw_email[$key] = $object->getAddress();
  121. }
  122. $this->setParam('raw-to', $raw_email);
  123. return $this;
  124. }
  125. public function addCCs(array $phids) {
  126. $phids = array_unique($phids);
  127. $this->setParam('cc', $phids);
  128. return $this;
  129. }
  130. public function setExcludeMailRecipientPHIDs(array $exclude) {
  131. $this->setParam('exclude', $exclude);
  132. return $this;
  133. }
  134. private function getExcludeMailRecipientPHIDs() {
  135. return $this->getParam('exclude', array());
  136. }
  137. public function setMutedPHIDs(array $muted) {
  138. $this->setParam('muted', $muted);
  139. return $this;
  140. }
  141. private function getMutedPHIDs() {
  142. return $this->getParam('muted', array());
  143. }
  144. public function setForceHeraldMailRecipientPHIDs(array $force) {
  145. $this->setParam('herald-force-recipients', $force);
  146. return $this;
  147. }
  148. private function getForceHeraldMailRecipientPHIDs() {
  149. return $this->getParam('herald-force-recipients', array());
  150. }
  151. public function addPHIDHeaders($name, array $phids) {
  152. $phids = array_unique($phids);
  153. foreach ($phids as $phid) {
  154. $this->addHeader($name, '<'.$phid.'>');
  155. }
  156. return $this;
  157. }
  158. public function addHeader($name, $value) {
  159. $this->parameters['headers'][] = array($name, $value);
  160. return $this;
  161. }
  162. public function getHeaders() {
  163. return $this->getParam('headers', array());
  164. }
  165. public function addAttachment(PhabricatorMailAttachment $attachment) {
  166. $this->parameters['attachments'][] = $attachment->toDictionary();
  167. return $this;
  168. }
  169. public function getAttachments() {
  170. $dicts = $this->getParam('attachments', array());
  171. $result = array();
  172. foreach ($dicts as $dict) {
  173. $result[] = PhabricatorMailAttachment::newFromDictionary($dict);
  174. }
  175. return $result;
  176. }
  177. public function getAttachmentFilePHIDs() {
  178. $file_phids = array();
  179. $dictionaries = $this->getParam('attachments');
  180. if ($dictionaries) {
  181. foreach ($dictionaries as $dictionary) {
  182. $file_phid = idx($dictionary, 'filePHID');
  183. if ($file_phid) {
  184. $file_phids[] = $file_phid;
  185. }
  186. }
  187. }
  188. return $file_phids;
  189. }
  190. public function loadAttachedFiles(PhabricatorUser $viewer) {
  191. $file_phids = $this->getAttachmentFilePHIDs();
  192. if (!$file_phids) {
  193. return array();
  194. }
  195. return id(new PhabricatorFileQuery())
  196. ->setViewer($viewer)
  197. ->withPHIDs($file_phids)
  198. ->execute();
  199. }
  200. public function setAttachments(array $attachments) {
  201. assert_instances_of($attachments, 'PhabricatorMailAttachment');
  202. $this->setParam('attachments', mpull($attachments, 'toDictionary'));
  203. return $this;
  204. }
  205. public function setFrom($from) {
  206. $this->setParam('from', $from);
  207. $this->setActorPHID($from);
  208. return $this;
  209. }
  210. public function getFrom() {
  211. return $this->getParam('from');
  212. }
  213. public function setRawFrom($raw_email, $raw_name) {
  214. $this->setParam('raw-from', array($raw_email, $raw_name));
  215. return $this;
  216. }
  217. public function getRawFrom() {
  218. return $this->getParam('raw-from');
  219. }
  220. public function setReplyTo($reply_to) {
  221. $this->setParam('reply-to', $reply_to);
  222. return $this;
  223. }
  224. public function getReplyTo() {
  225. return $this->getParam('reply-to');
  226. }
  227. public function setSubject($subject) {
  228. $this->setParam('subject', $subject);
  229. return $this;
  230. }
  231. public function setSubjectPrefix($prefix) {
  232. $this->setParam('subject-prefix', $prefix);
  233. return $this;
  234. }
  235. public function getSubjectPrefix() {
  236. return $this->getParam('subject-prefix');
  237. }
  238. public function setVarySubjectPrefix($prefix) {
  239. $this->setParam('vary-subject-prefix', $prefix);
  240. return $this;
  241. }
  242. public function getVarySubjectPrefix() {
  243. return $this->getParam('vary-subject-prefix');
  244. }
  245. public function setBody($body) {
  246. $this->setParam('body', $body);
  247. return $this;
  248. }
  249. public function setSensitiveContent($bool) {
  250. $this->setParam('sensitive', $bool);
  251. return $this;
  252. }
  253. public function hasSensitiveContent() {
  254. return $this->getParam('sensitive', true);
  255. }
  256. public function setMustEncrypt($bool) {
  257. return $this->setParam('mustEncrypt', $bool);
  258. }
  259. public function getMustEncrypt() {
  260. return $this->getParam('mustEncrypt', false);
  261. }
  262. public function setMustEncryptURI($uri) {
  263. return $this->setParam('mustEncrypt.uri', $uri);
  264. }
  265. public function getMustEncryptURI() {
  266. return $this->getParam('mustEncrypt.uri');
  267. }
  268. public function setMustEncryptSubject($subject) {
  269. return $this->setParam('mustEncrypt.subject', $subject);
  270. }
  271. public function getMustEncryptSubject() {
  272. return $this->getParam('mustEncrypt.subject');
  273. }
  274. public function setMustEncryptReasons(array $reasons) {
  275. return $this->setParam('mustEncryptReasons', $reasons);
  276. }
  277. public function getMustEncryptReasons() {
  278. return $this->getParam('mustEncryptReasons', array());
  279. }
  280. public function setMailStamps(array $stamps) {
  281. return $this->setParam('stamps', $stamps);
  282. }
  283. public function getMailStamps() {
  284. return $this->getParam('stamps', array());
  285. }
  286. public function setMailStampMetadata($metadata) {
  287. return $this->setParam('stampMetadata', $metadata);
  288. }
  289. public function getMailStampMetadata() {
  290. return $this->getParam('stampMetadata', array());
  291. }
  292. public function getMailerKey() {
  293. return $this->getParam('mailer.key');
  294. }
  295. public function setTryMailers(array $mailers) {
  296. return $this->setParam('mailers.try', $mailers);
  297. }
  298. public function setHTMLBody($html) {
  299. $this->setParam('html-body', $html);
  300. return $this;
  301. }
  302. public function getBody() {
  303. return $this->getParam('body');
  304. }
  305. public function getHTMLBody() {
  306. return $this->getParam('html-body');
  307. }
  308. public function setIsErrorEmail($is_error) {
  309. $this->setParam('is-error', $is_error);
  310. return $this;
  311. }
  312. public function getIsErrorEmail() {
  313. return $this->getParam('is-error', false);
  314. }
  315. public function getToPHIDs() {
  316. return $this->getParam('to', array());
  317. }
  318. public function getRawToAddresses() {
  319. return $this->getParam('raw-to', array());
  320. }
  321. public function getCcPHIDs() {
  322. return $this->getParam('cc', array());
  323. }
  324. public function setMessageType($message_type) {
  325. return $this->setParam('message.type', $message_type);
  326. }
  327. public function getMessageType() {
  328. return $this->getParam(
  329. 'message.type',
  330. PhabricatorMailEmailMessage::MESSAGETYPE);
  331. }
  332. /**
  333. * Force delivery of a message, even if recipients have preferences which
  334. * would otherwise drop the message.
  335. *
  336. * This is primarily intended to let users who don't want any email still
  337. * receive things like password resets.
  338. *
  339. * @param bool True to force delivery despite user preferences.
  340. * @return this
  341. */
  342. public function setForceDelivery($force) {
  343. $this->setParam('force', $force);
  344. return $this;
  345. }
  346. public function getForceDelivery() {
  347. return $this->getParam('force', false);
  348. }
  349. /**
  350. * Flag that this is an auto-generated bulk message and should have bulk
  351. * headers added to it if appropriate. Broadly, this means some flavor of
  352. * "Precedence: bulk" or similar, but is implementation and configuration
  353. * dependent.
  354. *
  355. * @param bool True if the mail is automated bulk mail.
  356. * @return this
  357. */
  358. public function setIsBulk($is_bulk) {
  359. $this->setParam('is-bulk', $is_bulk);
  360. return $this;
  361. }
  362. public function getIsBulk() {
  363. return $this->getParam('is-bulk');
  364. }
  365. /**
  366. * Use this method to set an ID used for message threading. MetaMTA will
  367. * set appropriate headers (Message-ID, In-Reply-To, References and
  368. * Thread-Index) based on the capabilities of the underlying mailer.
  369. *
  370. * @param string Unique identifier, appropriate for use in a Message-ID,
  371. * In-Reply-To or References headers.
  372. * @param bool If true, indicates this is the first message in the thread.
  373. * @return this
  374. */
  375. public function setThreadID($thread_id, $is_first_message = false) {
  376. $this->setParam('thread-id', $thread_id);
  377. $this->setParam('is-first-message', $is_first_message);
  378. return $this;
  379. }
  380. public function getThreadID() {
  381. return $this->getParam('thread-id');
  382. }
  383. public function getIsFirstMessage() {
  384. return (bool)$this->getParam('is-first-message');
  385. }
  386. /**
  387. * Save a newly created mail to the database. The mail will eventually be
  388. * delivered by the MetaMTA daemon.
  389. *
  390. * @return this
  391. */
  392. public function saveAndSend() {
  393. return $this->save();
  394. }
  395. /**
  396. * @return this
  397. */
  398. public function save() {
  399. if ($this->getID()) {
  400. return parent::save();
  401. }
  402. // NOTE: When mail is sent from CLI scripts that run tasks in-process, we
  403. // may re-enter this method from within scheduleTask(). The implementation
  404. // is intended to avoid anything awkward if we end up reentering this
  405. // method.
  406. $this->openTransaction();
  407. // Save to generate a mail ID and PHID.
  408. $result = parent::save();
  409. // Write the recipient edges.
  410. $editor = new PhabricatorEdgeEditor();
  411. $edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
  412. $recipient_phids = array_merge(
  413. $this->getToPHIDs(),
  414. $this->getCcPHIDs());
  415. $expanded_phids = $this->expandRecipients($recipient_phids);
  416. $all_phids = array_unique(array_merge(
  417. $recipient_phids,
  418. $expanded_phids));
  419. foreach ($all_phids as $curr_phid) {
  420. $editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
  421. }
  422. $editor->save();
  423. $this->saveTransaction();
  424. // Queue a task to send this mail.
  425. $mailer_task = PhabricatorWorker::scheduleTask(
  426. 'PhabricatorMetaMTAWorker',
  427. $this->getID(),
  428. array(
  429. 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
  430. ));
  431. return $result;
  432. }
  433. /**
  434. * Attempt to deliver an email immediately, in this process.
  435. *
  436. * @return void
  437. */
  438. public function sendNow() {
  439. if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
  440. throw new Exception(pht('Trying to send an already-sent mail!'));
  441. }
  442. $mailers = self::newMailers(
  443. array(
  444. 'outbound' => true,
  445. 'media' => array(
  446. $this->getMessageType(),
  447. ),
  448. ));
  449. $try_mailers = $this->getParam('mailers.try');
  450. if ($try_mailers) {
  451. $mailers = mpull($mailers, null, 'getKey');
  452. $mailers = array_select_keys($mailers, $try_mailers);
  453. }
  454. return $this->sendWithMailers($mailers);
  455. }
  456. public static function newMailers(array $constraints) {
  457. PhutilTypeSpec::checkMap(
  458. $constraints,
  459. array(
  460. 'types' => 'optional list<string>',
  461. 'inbound' => 'optional bool',
  462. 'outbound' => 'optional bool',
  463. 'media' => 'optional list<string>',
  464. ));
  465. $mailers = array();
  466. $config = PhabricatorEnv::getEnvConfig('cluster.mailers');
  467. $adapters = PhabricatorMailAdapter::getAllAdapters();
  468. $next_priority = -1;
  469. foreach ($config as $spec) {
  470. $type = $spec['type'];
  471. if (!isset($adapters[$type])) {
  472. throw new Exception(
  473. pht(
  474. 'Unknown mailer ("%s")!',
  475. $type));
  476. }
  477. $key = $spec['key'];
  478. $mailer = id(clone $adapters[$type])
  479. ->setKey($key);
  480. $priority = idx($spec, 'priority');
  481. if (!$priority) {
  482. $priority = $next_priority;
  483. $next_priority--;
  484. }
  485. $mailer->setPriority($priority);
  486. $defaults = $mailer->newDefaultOptions();
  487. $options = idx($spec, 'options', array()) + $defaults;
  488. $mailer->setOptions($options);
  489. $mailer->setSupportsInbound(idx($spec, 'inbound', true));
  490. $mailer->setSupportsOutbound(idx($spec, 'outbound', true));
  491. $media = idx($spec, 'media');
  492. if ($media !== null) {
  493. $mailer->setMedia($media);
  494. }
  495. $mailers[] = $mailer;
  496. }
  497. // Remove mailers with the wrong types.
  498. if (isset($constraints['types'])) {
  499. $types = $constraints['types'];
  500. $types = array_fuse($types);
  501. foreach ($mailers as $key => $mailer) {
  502. $mailer_type = $mailer->getAdapterType();
  503. if (!isset($types[$mailer_type])) {
  504. unset($mailers[$key]);
  505. }
  506. }
  507. }
  508. // If we're only looking for inbound mailers, remove mailers with inbound
  509. // support disabled.
  510. if (!empty($constraints['inbound'])) {
  511. foreach ($mailers as $key => $mailer) {
  512. if (!$mailer->getSupportsInbound()) {
  513. unset($mailers[$key]);
  514. }
  515. }
  516. }
  517. // If we're only looking for outbound mailers, remove mailers with outbound
  518. // support disabled.
  519. if (!empty($constraints['outbound'])) {
  520. foreach ($mailers as $key => $mailer) {
  521. if (!$mailer->getSupportsOutbound()) {
  522. unset($mailers[$key]);
  523. }
  524. }
  525. }
  526. // Select only the mailers which can transmit messages with requested media
  527. // types.
  528. if (!empty($constraints['media'])) {
  529. foreach ($mailers as $key => $mailer) {
  530. $supports_any = false;
  531. foreach ($constraints['media'] as $medium) {
  532. if ($mailer->supportsMessageType($medium)) {
  533. $supports_any = true;
  534. break;
  535. }
  536. }
  537. if (!$supports_any) {
  538. unset($mailers[$key]);
  539. }
  540. }
  541. }
  542. $sorted = array();
  543. $groups = mgroup($mailers, 'getPriority');
  544. krsort($groups);
  545. foreach ($groups as $group) {
  546. // Reorder services within the same priority group randomly.
  547. shuffle($group);
  548. foreach ($group as $mailer) {
  549. $sorted[] = $mailer;
  550. }
  551. }
  552. return $sorted;
  553. }
  554. public function sendWithMailers(array $mailers) {
  555. if (!$mailers) {
  556. $any_mailers = self::newMailers(array());
  557. // NOTE: We can end up here with some custom list of "$mailers", like
  558. // from a unit test. In that case, this message could be misleading. We
  559. // can't really tell if the caller made up the list, so just assume they
  560. // aren't tricking us.
  561. if ($any_mailers) {
  562. $void_message = pht(
  563. 'No configured mailers support sending outbound mail.');
  564. } else {
  565. $void_message = pht(
  566. 'No mailers are configured.');
  567. }
  568. return $this
  569. ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
  570. ->setMessage($void_message)
  571. ->save();
  572. }
  573. $actors = $this->loadAllActors();
  574. // If we're sending one mail to everyone, some recipients will be in
  575. // "Cc" rather than "To". We'll move them to "To" later (or supply a
  576. // dummy "To") but need to look for the recipient in either the
  577. // "To" or "Cc" fields here.
  578. $target_phid = head($this->getToPHIDs());
  579. if (!$target_phid) {
  580. $target_phid = head($this->getCcPHIDs());
  581. }
  582. $preferences = $this->loadPreferences($target_phid);
  583. // Attach any files we're about to send to this message, so the recipients
  584. // can view them.
  585. $viewer = PhabricatorUser::getOmnipotentUser();
  586. $files = $this->loadAttachedFiles($viewer);
  587. foreach ($files as $file) {
  588. $file->attachToObject($this->getPHID());
  589. }
  590. $type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
  591. $type = idx($type_map, $this->getMessageType());
  592. if (!$type) {
  593. throw new Exception(
  594. pht(
  595. 'Unable to send message with unknown message type "%s".',
  596. $type));
  597. }
  598. $exceptions = array();
  599. foreach ($mailers as $mailer) {
  600. try {
  601. $message = $type->newMailMessageEngine()
  602. ->setMailer($mailer)
  603. ->setMail($this)
  604. ->setActors($actors)
  605. ->setPreferences($preferences)
  606. ->newMessage($mailer);
  607. } catch (Exception $ex) {
  608. $exceptions[] = $ex;
  609. continue;
  610. }
  611. if (!$message) {
  612. // If we don't get a message back, that means the mail doesn't actually
  613. // need to be sent (for example, because recipients have declined to
  614. // receive the mail). Void it and return.
  615. return $this
  616. ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
  617. ->save();
  618. }
  619. try {
  620. $mailer->sendMessage($message);
  621. } catch (PhabricatorMetaMTAPermanentFailureException $ex) {
  622. // If any mailer raises a permanent failure, stop trying to send the
  623. // mail with other mailers.
  624. $this
  625. ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
  626. ->setMessage($ex->getMessage())
  627. ->save();
  628. throw $ex;
  629. } catch (Exception $ex) {
  630. $exceptions[] = $ex;
  631. continue;
  632. }
  633. // Keep track of which mailer actually ended up accepting the message.
  634. $mailer_key = $mailer->getKey();
  635. if ($mailer_key !== null) {
  636. $this->setParam('mailer.key', $mailer_key);
  637. }
  638. // Now that we sent the message, store the final deliverability outcomes
  639. // and reasoning so we can explain why things happened the way they did.
  640. $actor_list = array();
  641. foreach ($actors as $actor) {
  642. $actor_list[$actor->getPHID()] = array(
  643. 'deliverable' => $actor->isDeliverable(),
  644. 'reasons' => $actor->getDeliverabilityReasons(),
  645. );
  646. }
  647. $this->setParam('actors.sent', $actor_list);
  648. $this->setParam('routing.sent', $this->getParam('routing'));
  649. $this->setParam('routingmap.sent', $this->getRoutingRuleMap());
  650. return $this
  651. ->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT)
  652. ->save();
  653. }
  654. // If we make it here, no mailer could send the mail but no mailer failed
  655. // permanently either. We update the error message for the mail, but leave
  656. // it in the current status (usually, STATUS_QUEUE) and try again later.
  657. $messages = array();
  658. foreach ($exceptions as $ex) {
  659. $messages[] = $ex->getMessage();
  660. }
  661. $messages = implode("\n\n", $messages);
  662. $this
  663. ->setMessage($messages)
  664. ->save();
  665. if (count($exceptions) === 1) {
  666. throw head($exceptions);
  667. }
  668. throw new PhutilAggregateException(
  669. pht('Encountered multiple exceptions while transmitting mail.'),
  670. $exceptions);
  671. }
  672. public static function shouldMailEachRecipient() {
  673. return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
  674. }
  675. /* -( Managing Recipients )------------------------------------------------ */
  676. /**
  677. * Get all of the recipients for this mail, after preference filters are
  678. * applied. This list has all objects to whom delivery will be attempted.
  679. *
  680. * Note that this expands recipients into their members, because delivery
  681. * is never directly attempted to aggregate actors like projects.
  682. *
  683. * @return list<phid> A list of all recipients to whom delivery will be
  684. * attempted.
  685. * @task recipients
  686. */
  687. public function buildRecipientList() {
  688. $actors = $this->loadAllActors();
  689. $actors = $this->filterDeliverableActors($actors);
  690. return mpull($actors, 'getPHID');
  691. }
  692. public function loadAllActors() {
  693. $actor_phids = $this->getExpandedRecipientPHIDs();
  694. return $this->loadActors($actor_phids);
  695. }
  696. public function getExpandedRecipientPHIDs() {
  697. $actor_phids = $this->getAllActorPHIDs();
  698. return $this->expandRecipients($actor_phids);
  699. }
  700. private function getAllActorPHIDs() {
  701. return array_merge(
  702. array($this->getParam('from')),
  703. $this->getToPHIDs(),
  704. $this->getCcPHIDs());
  705. }
  706. /**
  707. * Expand a list of recipient PHIDs (possibly including aggregate recipients
  708. * like projects) into a deaggregated list of individual recipient PHIDs.
  709. * For example, this will expand project PHIDs into a list of the project's
  710. * members.
  711. *
  712. * @param list<phid> List of recipient PHIDs, possibly including aggregate
  713. * recipients.
  714. * @return list<phid> Deaggregated list of mailable recipients.
  715. */
  716. public function expandRecipients(array $phids) {
  717. if ($this->recipientExpansionMap === null) {
  718. $all_phids = $this->getAllActorPHIDs();
  719. $this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
  720. ->setViewer(PhabricatorUser::getOmnipotentUser())
  721. ->withPHIDs($all_phids)
  722. ->execute();
  723. }
  724. $results = array();
  725. foreach ($phids as $phid) {
  726. foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
  727. $results[$recipient_phid] = $recipient_phid;
  728. }
  729. }
  730. return array_keys($results);
  731. }
  732. private function filterDeliverableActors(array $actors) {
  733. assert_instances_of($actors, 'PhabricatorMetaMTAActor');
  734. $deliverable_actors = array();
  735. foreach ($actors as $phid => $actor) {
  736. if ($actor->isDeliverable()) {
  737. $deliverable_actors[$phid] = $actor;
  738. }
  739. }
  740. return $deliverable_actors;
  741. }
  742. private function loadActors(array $actor_phids) {
  743. $actor_phids = array_filter($actor_phids);
  744. $viewer = PhabricatorUser::getOmnipotentUser();
  745. $actors = id(new PhabricatorMetaMTAActorQuery())
  746. ->setViewer($viewer)
  747. ->withPHIDs($actor_phids)
  748. ->execute();
  749. if (!$actors) {
  750. return array();
  751. }
  752. if ($this->getForceDelivery()) {
  753. // If we're forcing delivery, skip all the opt-out checks. We don't
  754. // bother annotating reasoning on the mail in this case because it should
  755. // always be obvious why the mail hit this rule (e.g., it is a password
  756. // reset mail).
  757. foreach ($actors as $actor) {
  758. $actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
  759. }
  760. return $actors;
  761. }
  762. // Exclude explicit recipients.
  763. foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
  764. $actor = idx($actors, $phid);
  765. if (!$actor) {
  766. continue;
  767. }
  768. $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
  769. }
  770. // Before running more rules, save a list of the actors who were
  771. // deliverable before we started running preference-based rules. This stops
  772. // us from trying to send mail to disabled users just because a Herald rule
  773. // added them, for example.
  774. $deliverable = array();
  775. foreach ($actors as $phid => $actor) {
  776. if ($actor->isDeliverable()) {
  777. $deliverable[] = $phid;
  778. }
  779. }
  780. // Exclude muted recipients. We're doing this after saving deliverability
  781. // so that Herald "Send me an email" actions can still punch through a
  782. // mute.
  783. foreach ($this->getMutedPHIDs() as $muted_phid) {
  784. $muted_actor = idx($actors, $muted_phid);
  785. if (!$muted_actor) {
  786. continue;
  787. }
  788. $muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);
  789. }
  790. // For the rest of the rules, order matters. We're going to run all the
  791. // possible rules in order from weakest to strongest, and let the strongest
  792. // matching rule win. The weaker rules leave annotations behind which help
  793. // users understand why the mail was routed the way it was.
  794. // Exclude the actor if their preferences are set.
  795. $from_phid = $this->getParam('from');
  796. $from_actor = idx($actors, $from_phid);
  797. if ($from_actor) {
  798. $from_user = id(new PhabricatorPeopleQuery())
  799. ->setViewer($viewer)
  800. ->withPHIDs(array($from_phid))
  801. ->needUserSettings(true)
  802. ->execute();
  803. $from_user = head($from_user);
  804. if ($from_user) {
  805. $pref_key = PhabricatorEmailSelfActionsSetting::SETTINGKEY;
  806. $exclude_self = $from_user->getUserSetting($pref_key);
  807. if ($exclude_self) {
  808. $from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
  809. }
  810. }
  811. }
  812. $all_prefs = id(new PhabricatorUserPreferencesQuery())
  813. ->setViewer(PhabricatorUser::getOmnipotentUser())
  814. ->withUserPHIDs($actor_phids)
  815. ->needSyntheticPreferences(true)
  816. ->execute();
  817. $all_prefs = mpull($all_prefs, null, 'getUserPHID');
  818. $value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
  819. // Exclude all recipients who have set preferences to not receive this type
  820. // of email (for example, a user who says they don't want emails about task
  821. // CC changes).
  822. $tags = $this->getParam('mailtags');
  823. if ($tags) {
  824. foreach ($all_prefs as $phid => $prefs) {
  825. $user_mailtags = $prefs->getSettingValue(
  826. PhabricatorEmailTagsSetting::SETTINGKEY);
  827. // The user must have elected to receive mail for at least one
  828. // of the mailtags.
  829. $send = false;
  830. foreach ($tags as $tag) {
  831. if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
  832. $send = true;
  833. break;
  834. }
  835. }
  836. if (!$send) {
  837. $actors[$phid]->setUndeliverable(
  838. PhabricatorMetaMTAActor::REASON_MAILTAGS);
  839. }
  840. }
  841. }
  842. foreach ($deliverable as $phid) {
  843. switch ($this->getRoutingRule($phid)) {
  844. case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
  845. $actors[$phid]->setUndeliverable(
  846. PhabricatorMetaMTAActor::REASON_ROUTE_AS_NOTIFICATION);
  847. break;
  848. case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
  849. $actors[$phid]->setDeliverable(
  850. PhabricatorMetaMTAActor::REASON_ROUTE_AS_MAIL);
  851. break;
  852. default:
  853. // No change.
  854. break;
  855. }
  856. }
  857. // If recipients were initially deliverable and were added by "Send me an
  858. // email" Herald rules, annotate them as such and make them deliverable
  859. // again, overriding any changes made by the "self mail" and "mail tags"
  860. // settings.
  861. $force_recipients = $this->getForceHeraldMailRecipientPHIDs();
  862. $force_recipients = array_fuse($force_recipients);
  863. if ($force_recipients) {
  864. foreach ($deliverable as $phid) {
  865. if (isset($force_recipients[$phid])) {
  866. $actors[$phid]->setDeliverable(
  867. PhabricatorMetaMTAActor::REASON_FORCE_HERALD);
  868. }
  869. }
  870. }
  871. // Exclude recipients who don't want any mail. This rule is very strong
  872. // and runs last.
  873. foreach ($all_prefs as $phid => $prefs) {
  874. $exclude = $prefs->getSettingValue(
  875. PhabricatorEmailNotificationsSetting::SETTINGKEY);
  876. if ($exclude) {
  877. $actors[$phid]->setUndeliverable(
  878. PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
  879. }
  880. }
  881. // Unless delivery was forced earlier (password resets, confirmation mail),
  882. // never send mail to unverified addresses.
  883. foreach ($actors as $phid => $actor) {
  884. if ($actor->getIsVerified()) {
  885. continue;
  886. }
  887. $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNVERIFIED);
  888. }
  889. return $actors;
  890. }
  891. public function getDeliveredHeaders() {
  892. return $this->getParam('headers.sent');
  893. }
  894. public function setDeliveredHeaders(array $headers) {
  895. $headers = $this->flattenHeaders($headers);
  896. return $this->setParam('headers.sent', $headers);
  897. }
  898. public function getUnfilteredHeaders() {
  899. $unfiltered = $this->getParam('headers.unfiltered');
  900. if ($unfiltered === null) {
  901. // Older versions of Phabricator did not filter headers, and thus did
  902. // not record unfiltered headers. If we don't have unfiltered header
  903. // data just return the delivered headers for compatibility.
  904. return $this->getDeliveredHeaders();
  905. }
  906. return $unfiltered;
  907. }
  908. public function setUnfilteredHeaders(array $headers) {
  909. $headers = $this->flattenHeaders($headers);
  910. return $this->setParam('headers.unfiltered', $headers);
  911. }
  912. private function flattenHeaders(array $headers) {
  913. assert_instances_of($headers, 'PhabricatorMailHeader');
  914. $list = array();
  915. foreach ($list as $header) {
  916. $list[] = array(
  917. $header->getName(),
  918. $header->getValue(),
  919. );
  920. }
  921. return $list;
  922. }
  923. public function getDeliveredActors() {
  924. return $this->getParam('actors.sent');
  925. }
  926. public function getDeliveredRoutingRules() {
  927. return $this->getParam('routing.sent');
  928. }
  929. public function getDeliveredRoutingMap() {
  930. return $this->getParam('routingmap.sent');
  931. }
  932. public function getDeliveredBody() {
  933. return $this->getParam('body.sent');
  934. }
  935. public function setDeliveredBody($body) {
  936. return $this->setParam('body.sent', $body);
  937. }
  938. public function getURI() {
  939. return '/mail/detail/'.$this->getID().'/';
  940. }
  941. /* -( Routing )------------------------------------------------------------ */
  942. public function addRoutingRule($routing_rule, $phids, $reason_phid) {
  943. $routing = $this->getParam('routing', array());
  944. $routing[] = array(
  945. 'routingRule' => $routing_rule,
  946. 'phids' => $phids,
  947. 'reasonPHID' => $reason_phid,
  948. );
  949. $this->setParam('routing', $routing);
  950. // Throw the routing map away so we rebuild it.
  951. $this->routingMap = null;
  952. return $this;
  953. }
  954. private function getRoutingRule($phid) {
  955. $map = $this->getRoutingRuleMap();
  956. $info = idx($map, $phid, idx($map, 'default'));
  957. if ($info) {
  958. return idx($info, 'rule');
  959. }
  960. return null;
  961. }
  962. private function getRoutingRuleMap() {
  963. if ($this->routingMap === null) {
  964. $map = array();
  965. $routing = $this->getParam('routing', array());
  966. foreach ($routing as $route) {
  967. $phids = $route['phids'];
  968. if ($phids === null) {
  969. $phids = array('default');
  970. }
  971. foreach ($phids as $phid) {
  972. $new_rule = $route['routingRule'];
  973. $current_rule = idx($map, $phid);
  974. if ($current_rule === null) {
  975. $is_stronger = true;
  976. } else {
  977. $is_stronger = PhabricatorMailRoutingRule::isStrongerThan(
  978. $new_rule,
  979. $current_rule);
  980. }
  981. if ($is_stronger) {
  982. $map[$phid] = array(
  983. 'rule' => $new_rule,
  984. 'reason' => $route['reasonPHID'],
  985. );
  986. }
  987. }
  988. }
  989. $this->routingMap = $map;
  990. }
  991. return $this->routingMap;
  992. }
  993. /* -( Preferences )-------------------------------------------------------- */
  994. private function loadPreferences($target_phid) {
  995. $viewer = PhabricatorUser::getOmnipotentUser();
  996. if (self::shouldMailEachRecipient()) {
  997. $preferences = id(new PhabricatorUserPreferencesQuery())
  998. ->setViewer($viewer)
  999. ->withUserPHIDs(array($target_phid))
  1000. ->needSyntheticPreferences(true)
  1001. ->executeOne();
  1002. if ($preferences) {
  1003. return $preferences;
  1004. }
  1005. }
  1006. return PhabricatorUserPreferences::loadGlobalPreferences($viewer);
  1007. }
  1008. public function shouldRenderMailStampsInBody($viewer) {
  1009. $preferences = $this->loadPreferences($viewer->getPHID());
  1010. $value = $preferences->getSettingValue(
  1011. PhabricatorEmailStampsSetting::SETTINGKEY);
  1012. return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS);
  1013. }
  1014. /* -( PhabricatorPolicyInterface )----------------------------------------- */
  1015. public function getCapabilities() {
  1016. return array(
  1017. PhabricatorPolicyCapability::CAN_VIEW,
  1018. );
  1019. }
  1020. public function getPolicy($capability) {
  1021. return PhabricatorPolicies::POLICY_NOONE;
  1022. }
  1023. public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
  1024. $actor_phids = $this->getExpandedRecipientPHIDs();
  1025. return in_array($viewer->getPHID(), $actor_phids);
  1026. }
  1027. public function describeAutomaticCapability($capability) {
  1028. return pht(
  1029. 'The mail sender and message recipients can always see the mail.');
  1030. }
  1031. /* -( PhabricatorDestructibleInterface )----------------------------------- */
  1032. public function destroyObjectPermanently(
  1033. PhabricatorDestructionEngine $engine) {
  1034. $files = $this->loadAttachedFiles($engine->getViewer());
  1035. foreach ($files as $file) {
  1036. $engine->destroyObject($file);
  1037. }
  1038. $this->delete();
  1039. }
  1040. }