Path: blob/master/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
12256 views
<?php12/**3* @task recipients Managing Recipients4*/5final class PhabricatorMetaMTAMail6extends PhabricatorMetaMTADAO7implements8PhabricatorPolicyInterface,9PhabricatorDestructibleInterface {1011const RETRY_DELAY = 5;1213protected $actorPHID;14protected $parameters = array();15protected $status;16protected $message;17protected $relatedPHID;1819private $recipientExpansionMap;20private $routingMap;2122public function __construct() {2324$this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE;25$this->parameters = array(26'sensitive' => true,27'mustEncrypt' => false,28);2930parent::__construct();31}3233protected function getConfiguration() {34return array(35self::CONFIG_AUX_PHID => true,36self::CONFIG_SERIALIZATION => array(37'parameters' => self::SERIALIZATION_JSON,38),39self::CONFIG_COLUMN_SCHEMA => array(40'actorPHID' => 'phid?',41'status' => 'text32',42'relatedPHID' => 'phid?',4344// T6203/NULLABILITY45// This should just be empty if there's no body.46'message' => 'text?',47),48self::CONFIG_KEY_SCHEMA => array(49'status' => array(50'columns' => array('status'),51),52'key_actorPHID' => array(53'columns' => array('actorPHID'),54),55'relatedPHID' => array(56'columns' => array('relatedPHID'),57),58'key_created' => array(59'columns' => array('dateCreated'),60),61),62) + parent::getConfiguration();63}6465public function generatePHID() {66return PhabricatorPHID::generateNewPHID(67PhabricatorMetaMTAMailPHIDType::TYPECONST);68}6970protected function setParam($param, $value) {71$this->parameters[$param] = $value;72return $this;73}7475protected function getParam($param, $default = null) {76// Some old mail was saved without parameters because no parameters were77// set or encoding failed. Recover in these cases so we can perform78// mail migrations, see T9251.79if (!is_array($this->parameters)) {80$this->parameters = array();81}8283return idx($this->parameters, $param, $default);84}8586/**87* These tags are used to allow users to opt out of receiving certain types88* of mail, like updates when a task's projects change.89*90* @param list<const>91* @return this92*/93public function setMailTags(array $tags) {94$this->setParam('mailtags', array_unique($tags));95return $this;96}9798public function getMailTags() {99return $this->getParam('mailtags', array());100}101102/**103* In Gmail, conversations will be broken if you reply to a thread and the104* server sends back a response without referencing your Message-ID, even if105* it references a Message-ID earlier in the thread. To avoid this, use the106* parent email's message ID explicitly if it's available. This overwrites the107* "In-Reply-To" and "References" headers we would otherwise generate. This108* needs to be set whenever an action is triggered by an email message. See109* T251 for more details.110*111* @param string The "Message-ID" of the email which precedes this one.112* @return this113*/114public function setParentMessageID($id) {115$this->setParam('parent-message-id', $id);116return $this;117}118119public function getParentMessageID() {120return $this->getParam('parent-message-id');121}122123public function getSubject() {124return $this->getParam('subject');125}126127public function addTos(array $phids) {128$phids = array_unique($phids);129$this->setParam('to', $phids);130return $this;131}132133public function addRawTos(array $raw_email) {134135// Strip addresses down to bare emails, since the MailAdapter API currently136// requires we pass it just the address (like `[email protected]`), not137// a full string like `"Abraham Lincoln" <[email protected]>`.138foreach ($raw_email as $key => $email) {139$object = new PhutilEmailAddress($email);140$raw_email[$key] = $object->getAddress();141}142143$this->setParam('raw-to', $raw_email);144return $this;145}146147public function addCCs(array $phids) {148$phids = array_unique($phids);149$this->setParam('cc', $phids);150return $this;151}152153public function setExcludeMailRecipientPHIDs(array $exclude) {154$this->setParam('exclude', $exclude);155return $this;156}157158private function getExcludeMailRecipientPHIDs() {159return $this->getParam('exclude', array());160}161162public function setMutedPHIDs(array $muted) {163$this->setParam('muted', $muted);164return $this;165}166167private function getMutedPHIDs() {168return $this->getParam('muted', array());169}170171public function setForceHeraldMailRecipientPHIDs(array $force) {172$this->setParam('herald-force-recipients', $force);173return $this;174}175176private function getForceHeraldMailRecipientPHIDs() {177return $this->getParam('herald-force-recipients', array());178}179180public function addPHIDHeaders($name, array $phids) {181$phids = array_unique($phids);182foreach ($phids as $phid) {183$this->addHeader($name, '<'.$phid.'>');184}185return $this;186}187188public function addHeader($name, $value) {189$this->parameters['headers'][] = array($name, $value);190return $this;191}192193public function getHeaders() {194return $this->getParam('headers', array());195}196197public function addAttachment(PhabricatorMailAttachment $attachment) {198$this->parameters['attachments'][] = $attachment->toDictionary();199return $this;200}201202public function getAttachments() {203$dicts = $this->getParam('attachments', array());204205$result = array();206foreach ($dicts as $dict) {207$result[] = PhabricatorMailAttachment::newFromDictionary($dict);208}209return $result;210}211212public function getAttachmentFilePHIDs() {213$file_phids = array();214215$dictionaries = $this->getParam('attachments');216if ($dictionaries) {217foreach ($dictionaries as $dictionary) {218$file_phid = idx($dictionary, 'filePHID');219if ($file_phid) {220$file_phids[] = $file_phid;221}222}223}224225return $file_phids;226}227228public function loadAttachedFiles(PhabricatorUser $viewer) {229$file_phids = $this->getAttachmentFilePHIDs();230231if (!$file_phids) {232return array();233}234235return id(new PhabricatorFileQuery())236->setViewer($viewer)237->withPHIDs($file_phids)238->execute();239}240241public function setAttachments(array $attachments) {242assert_instances_of($attachments, 'PhabricatorMailAttachment');243$this->setParam('attachments', mpull($attachments, 'toDictionary'));244return $this;245}246247public function setFrom($from) {248$this->setParam('from', $from);249$this->setActorPHID($from);250return $this;251}252253public function getFrom() {254return $this->getParam('from');255}256257public function setRawFrom($raw_email, $raw_name) {258$this->setParam('raw-from', array($raw_email, $raw_name));259return $this;260}261262public function getRawFrom() {263return $this->getParam('raw-from');264}265266public function setReplyTo($reply_to) {267$this->setParam('reply-to', $reply_to);268return $this;269}270271public function getReplyTo() {272return $this->getParam('reply-to');273}274275public function setSubject($subject) {276$this->setParam('subject', $subject);277return $this;278}279280public function setSubjectPrefix($prefix) {281$this->setParam('subject-prefix', $prefix);282return $this;283}284285public function getSubjectPrefix() {286return $this->getParam('subject-prefix');287}288289public function setVarySubjectPrefix($prefix) {290$this->setParam('vary-subject-prefix', $prefix);291return $this;292}293294public function getVarySubjectPrefix() {295return $this->getParam('vary-subject-prefix');296}297298public function setBody($body) {299$this->setParam('body', $body);300return $this;301}302303public function setSensitiveContent($bool) {304$this->setParam('sensitive', $bool);305return $this;306}307308public function hasSensitiveContent() {309return $this->getParam('sensitive', true);310}311312public function setMustEncrypt($bool) {313return $this->setParam('mustEncrypt', $bool);314}315316public function getMustEncrypt() {317return $this->getParam('mustEncrypt', false);318}319320public function setMustEncryptURI($uri) {321return $this->setParam('mustEncrypt.uri', $uri);322}323324public function getMustEncryptURI() {325return $this->getParam('mustEncrypt.uri');326}327328public function setMustEncryptSubject($subject) {329return $this->setParam('mustEncrypt.subject', $subject);330}331332public function getMustEncryptSubject() {333return $this->getParam('mustEncrypt.subject');334}335336public function setMustEncryptReasons(array $reasons) {337return $this->setParam('mustEncryptReasons', $reasons);338}339340public function getMustEncryptReasons() {341return $this->getParam('mustEncryptReasons', array());342}343344public function setMailStamps(array $stamps) {345return $this->setParam('stamps', $stamps);346}347348public function getMailStamps() {349return $this->getParam('stamps', array());350}351352public function setMailStampMetadata($metadata) {353return $this->setParam('stampMetadata', $metadata);354}355356public function getMailStampMetadata() {357return $this->getParam('stampMetadata', array());358}359360public function getMailerKey() {361return $this->getParam('mailer.key');362}363364public function setTryMailers(array $mailers) {365return $this->setParam('mailers.try', $mailers);366}367368public function setHTMLBody($html) {369$this->setParam('html-body', $html);370return $this;371}372373public function getBody() {374return $this->getParam('body');375}376377public function getHTMLBody() {378return $this->getParam('html-body');379}380381public function setIsErrorEmail($is_error) {382$this->setParam('is-error', $is_error);383return $this;384}385386public function getIsErrorEmail() {387return $this->getParam('is-error', false);388}389390public function getToPHIDs() {391return $this->getParam('to', array());392}393394public function getRawToAddresses() {395return $this->getParam('raw-to', array());396}397398public function getCcPHIDs() {399return $this->getParam('cc', array());400}401402public function setMessageType($message_type) {403return $this->setParam('message.type', $message_type);404}405406public function getMessageType() {407return $this->getParam(408'message.type',409PhabricatorMailEmailMessage::MESSAGETYPE);410}411412413414/**415* Force delivery of a message, even if recipients have preferences which416* would otherwise drop the message.417*418* This is primarily intended to let users who don't want any email still419* receive things like password resets.420*421* @param bool True to force delivery despite user preferences.422* @return this423*/424public function setForceDelivery($force) {425$this->setParam('force', $force);426return $this;427}428429public function getForceDelivery() {430return $this->getParam('force', false);431}432433/**434* Flag that this is an auto-generated bulk message and should have bulk435* headers added to it if appropriate. Broadly, this means some flavor of436* "Precedence: bulk" or similar, but is implementation and configuration437* dependent.438*439* @param bool True if the mail is automated bulk mail.440* @return this441*/442public function setIsBulk($is_bulk) {443$this->setParam('is-bulk', $is_bulk);444return $this;445}446447public function getIsBulk() {448return $this->getParam('is-bulk');449}450451/**452* Use this method to set an ID used for message threading. MetaMTA will453* set appropriate headers (Message-ID, In-Reply-To, References and454* Thread-Index) based on the capabilities of the underlying mailer.455*456* @param string Unique identifier, appropriate for use in a Message-ID,457* In-Reply-To or References headers.458* @param bool If true, indicates this is the first message in the thread.459* @return this460*/461public function setThreadID($thread_id, $is_first_message = false) {462$this->setParam('thread-id', $thread_id);463$this->setParam('is-first-message', $is_first_message);464return $this;465}466467public function getThreadID() {468return $this->getParam('thread-id');469}470471public function getIsFirstMessage() {472return (bool)$this->getParam('is-first-message');473}474475/**476* Save a newly created mail to the database. The mail will eventually be477* delivered by the MetaMTA daemon.478*479* @return this480*/481public function saveAndSend() {482return $this->save();483}484485/**486* @return this487*/488public function save() {489if ($this->getID()) {490return parent::save();491}492493// NOTE: When mail is sent from CLI scripts that run tasks in-process, we494// may re-enter this method from within scheduleTask(). The implementation495// is intended to avoid anything awkward if we end up reentering this496// method.497498$this->openTransaction();499// Save to generate a mail ID and PHID.500$result = parent::save();501502// Write the recipient edges.503$editor = new PhabricatorEdgeEditor();504$edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;505$recipient_phids = array_merge(506$this->getToPHIDs(),507$this->getCcPHIDs());508$expanded_phids = $this->expandRecipients($recipient_phids);509$all_phids = array_unique(array_merge(510$recipient_phids,511$expanded_phids));512foreach ($all_phids as $curr_phid) {513$editor->addEdge($this->getPHID(), $edge_type, $curr_phid);514}515$editor->save();516517$this->saveTransaction();518519// Queue a task to send this mail.520$mailer_task = PhabricatorWorker::scheduleTask(521'PhabricatorMetaMTAWorker',522$this->getID(),523array(524'priority' => PhabricatorWorker::PRIORITY_ALERTS,525));526527return $result;528}529530/**531* Attempt to deliver an email immediately, in this process.532*533* @return void534*/535public function sendNow() {536if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {537throw new Exception(pht('Trying to send an already-sent mail!'));538}539540$mailers = self::newMailers(541array(542'outbound' => true,543'media' => array(544$this->getMessageType(),545),546));547548$try_mailers = $this->getParam('mailers.try');549if ($try_mailers) {550$mailers = mpull($mailers, null, 'getKey');551$mailers = array_select_keys($mailers, $try_mailers);552}553554return $this->sendWithMailers($mailers);555}556557public static function newMailers(array $constraints) {558PhutilTypeSpec::checkMap(559$constraints,560array(561'types' => 'optional list<string>',562'inbound' => 'optional bool',563'outbound' => 'optional bool',564'media' => 'optional list<string>',565));566567$mailers = array();568569$config = PhabricatorEnv::getEnvConfig('cluster.mailers');570571$adapters = PhabricatorMailAdapter::getAllAdapters();572$next_priority = -1;573574foreach ($config as $spec) {575$type = $spec['type'];576if (!isset($adapters[$type])) {577throw new Exception(578pht(579'Unknown mailer ("%s")!',580$type));581}582583$key = $spec['key'];584$mailer = id(clone $adapters[$type])585->setKey($key);586587$priority = idx($spec, 'priority');588if (!$priority) {589$priority = $next_priority;590$next_priority--;591}592$mailer->setPriority($priority);593594$defaults = $mailer->newDefaultOptions();595$options = idx($spec, 'options', array()) + $defaults;596$mailer->setOptions($options);597598$mailer->setSupportsInbound(idx($spec, 'inbound', true));599$mailer->setSupportsOutbound(idx($spec, 'outbound', true));600601$media = idx($spec, 'media');602if ($media !== null) {603$mailer->setMedia($media);604}605606$mailers[] = $mailer;607}608609// Remove mailers with the wrong types.610if (isset($constraints['types'])) {611$types = $constraints['types'];612$types = array_fuse($types);613foreach ($mailers as $key => $mailer) {614$mailer_type = $mailer->getAdapterType();615if (!isset($types[$mailer_type])) {616unset($mailers[$key]);617}618}619}620621// If we're only looking for inbound mailers, remove mailers with inbound622// support disabled.623if (!empty($constraints['inbound'])) {624foreach ($mailers as $key => $mailer) {625if (!$mailer->getSupportsInbound()) {626unset($mailers[$key]);627}628}629}630631// If we're only looking for outbound mailers, remove mailers with outbound632// support disabled.633if (!empty($constraints['outbound'])) {634foreach ($mailers as $key => $mailer) {635if (!$mailer->getSupportsOutbound()) {636unset($mailers[$key]);637}638}639}640641// Select only the mailers which can transmit messages with requested media642// types.643if (!empty($constraints['media'])) {644foreach ($mailers as $key => $mailer) {645$supports_any = false;646foreach ($constraints['media'] as $medium) {647if ($mailer->supportsMessageType($medium)) {648$supports_any = true;649break;650}651}652653if (!$supports_any) {654unset($mailers[$key]);655}656}657}658659$sorted = array();660$groups = mgroup($mailers, 'getPriority');661krsort($groups);662foreach ($groups as $group) {663// Reorder services within the same priority group randomly.664shuffle($group);665foreach ($group as $mailer) {666$sorted[] = $mailer;667}668}669670return $sorted;671}672673public function sendWithMailers(array $mailers) {674if (!$mailers) {675$any_mailers = self::newMailers(array());676677// NOTE: We can end up here with some custom list of "$mailers", like678// from a unit test. In that case, this message could be misleading. We679// can't really tell if the caller made up the list, so just assume they680// aren't tricking us.681682if ($any_mailers) {683$void_message = pht(684'No configured mailers support sending outbound mail.');685} else {686$void_message = pht(687'No mailers are configured.');688}689690return $this691->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)692->setMessage($void_message)693->save();694}695696$actors = $this->loadAllActors();697698// If we're sending one mail to everyone, some recipients will be in699// "Cc" rather than "To". We'll move them to "To" later (or supply a700// dummy "To") but need to look for the recipient in either the701// "To" or "Cc" fields here.702$target_phid = head($this->getToPHIDs());703if (!$target_phid) {704$target_phid = head($this->getCcPHIDs());705}706$preferences = $this->loadPreferences($target_phid);707708// Attach any files we're about to send to this message, so the recipients709// can view them.710$viewer = PhabricatorUser::getOmnipotentUser();711$files = $this->loadAttachedFiles($viewer);712foreach ($files as $file) {713$file->attachToObject($this->getPHID());714}715716$type_map = PhabricatorMailExternalMessage::getAllMessageTypes();717$type = idx($type_map, $this->getMessageType());718if (!$type) {719throw new Exception(720pht(721'Unable to send message with unknown message type "%s".',722$type));723}724725$exceptions = array();726foreach ($mailers as $mailer) {727try {728$message = $type->newMailMessageEngine()729->setMailer($mailer)730->setMail($this)731->setActors($actors)732->setPreferences($preferences)733->newMessage($mailer);734} catch (Exception $ex) {735$exceptions[] = $ex;736continue;737}738739if (!$message) {740// If we don't get a message back, that means the mail doesn't actually741// need to be sent (for example, because recipients have declined to742// receive the mail). Void it and return.743return $this744->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)745->save();746}747748try {749$mailer->sendMessage($message);750} catch (PhabricatorMetaMTAPermanentFailureException $ex) {751// If any mailer raises a permanent failure, stop trying to send the752// mail with other mailers.753$this754->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)755->setMessage($ex->getMessage())756->save();757758throw $ex;759} catch (Exception $ex) {760$exceptions[] = $ex;761continue;762}763764// Keep track of which mailer actually ended up accepting the message.765$mailer_key = $mailer->getKey();766if ($mailer_key !== null) {767$this->setParam('mailer.key', $mailer_key);768}769770// Now that we sent the message, store the final deliverability outcomes771// and reasoning so we can explain why things happened the way they did.772$actor_list = array();773foreach ($actors as $actor) {774$actor_list[$actor->getPHID()] = array(775'deliverable' => $actor->isDeliverable(),776'reasons' => $actor->getDeliverabilityReasons(),777);778}779$this->setParam('actors.sent', $actor_list);780$this->setParam('routing.sent', $this->getParam('routing'));781$this->setParam('routingmap.sent', $this->getRoutingRuleMap());782783return $this784->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT)785->save();786}787788// If we make it here, no mailer could send the mail but no mailer failed789// permanently either. We update the error message for the mail, but leave790// it in the current status (usually, STATUS_QUEUE) and try again later.791792$messages = array();793foreach ($exceptions as $ex) {794$messages[] = $ex->getMessage();795}796$messages = implode("\n\n", $messages);797798$this799->setMessage($messages)800->save();801802if (count($exceptions) === 1) {803throw head($exceptions);804}805806throw new PhutilAggregateException(807pht('Encountered multiple exceptions while transmitting mail.'),808$exceptions);809}810811812public static function shouldMailEachRecipient() {813return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');814}815816817/* -( Managing Recipients )------------------------------------------------ */818819820/**821* Get all of the recipients for this mail, after preference filters are822* applied. This list has all objects to whom delivery will be attempted.823*824* Note that this expands recipients into their members, because delivery825* is never directly attempted to aggregate actors like projects.826*827* @return list<phid> A list of all recipients to whom delivery will be828* attempted.829* @task recipients830*/831public function buildRecipientList() {832$actors = $this->loadAllActors();833$actors = $this->filterDeliverableActors($actors);834return mpull($actors, 'getPHID');835}836837public function loadAllActors() {838$actor_phids = $this->getExpandedRecipientPHIDs();839return $this->loadActors($actor_phids);840}841842public function getExpandedRecipientPHIDs() {843$actor_phids = $this->getAllActorPHIDs();844return $this->expandRecipients($actor_phids);845}846847private function getAllActorPHIDs() {848return array_merge(849array($this->getParam('from')),850$this->getToPHIDs(),851$this->getCcPHIDs());852}853854/**855* Expand a list of recipient PHIDs (possibly including aggregate recipients856* like projects) into a deaggregated list of individual recipient PHIDs.857* For example, this will expand project PHIDs into a list of the project's858* members.859*860* @param list<phid> List of recipient PHIDs, possibly including aggregate861* recipients.862* @return list<phid> Deaggregated list of mailable recipients.863*/864public function expandRecipients(array $phids) {865if ($this->recipientExpansionMap === null) {866$all_phids = $this->getAllActorPHIDs();867$this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())868->setViewer(PhabricatorUser::getOmnipotentUser())869->withPHIDs($all_phids)870->execute();871}872873$results = array();874foreach ($phids as $phid) {875foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {876$results[$recipient_phid] = $recipient_phid;877}878}879880return array_keys($results);881}882883private function filterDeliverableActors(array $actors) {884assert_instances_of($actors, 'PhabricatorMetaMTAActor');885$deliverable_actors = array();886foreach ($actors as $phid => $actor) {887if ($actor->isDeliverable()) {888$deliverable_actors[$phid] = $actor;889}890}891return $deliverable_actors;892}893894private function loadActors(array $actor_phids) {895$actor_phids = array_filter($actor_phids);896$viewer = PhabricatorUser::getOmnipotentUser();897898$actors = id(new PhabricatorMetaMTAActorQuery())899->setViewer($viewer)900->withPHIDs($actor_phids)901->execute();902903if (!$actors) {904return array();905}906907if ($this->getForceDelivery()) {908// If we're forcing delivery, skip all the opt-out checks. We don't909// bother annotating reasoning on the mail in this case because it should910// always be obvious why the mail hit this rule (e.g., it is a password911// reset mail).912foreach ($actors as $actor) {913$actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);914}915return $actors;916}917918// Exclude explicit recipients.919foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {920$actor = idx($actors, $phid);921if (!$actor) {922continue;923}924$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);925}926927// Before running more rules, save a list of the actors who were928// deliverable before we started running preference-based rules. This stops929// us from trying to send mail to disabled users just because a Herald rule930// added them, for example.931$deliverable = array();932foreach ($actors as $phid => $actor) {933if ($actor->isDeliverable()) {934$deliverable[] = $phid;935}936}937938// Exclude muted recipients. We're doing this after saving deliverability939// so that Herald "Send me an email" actions can still punch through a940// mute.941942foreach ($this->getMutedPHIDs() as $muted_phid) {943$muted_actor = idx($actors, $muted_phid);944if (!$muted_actor) {945continue;946}947$muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);948}949950// For the rest of the rules, order matters. We're going to run all the951// possible rules in order from weakest to strongest, and let the strongest952// matching rule win. The weaker rules leave annotations behind which help953// users understand why the mail was routed the way it was.954955// Exclude the actor if their preferences are set.956$from_phid = $this->getParam('from');957$from_actor = idx($actors, $from_phid);958if ($from_actor) {959$from_user = id(new PhabricatorPeopleQuery())960->setViewer($viewer)961->withPHIDs(array($from_phid))962->needUserSettings(true)963->execute();964$from_user = head($from_user);965if ($from_user) {966$pref_key = PhabricatorEmailSelfActionsSetting::SETTINGKEY;967$exclude_self = $from_user->getUserSetting($pref_key);968if ($exclude_self) {969$from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);970}971}972}973974$all_prefs = id(new PhabricatorUserPreferencesQuery())975->setViewer(PhabricatorUser::getOmnipotentUser())976->withUserPHIDs($actor_phids)977->needSyntheticPreferences(true)978->execute();979$all_prefs = mpull($all_prefs, null, 'getUserPHID');980981$value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;982983// Exclude all recipients who have set preferences to not receive this type984// of email (for example, a user who says they don't want emails about task985// CC changes).986$tags = $this->getParam('mailtags');987if ($tags) {988foreach ($all_prefs as $phid => $prefs) {989$user_mailtags = $prefs->getSettingValue(990PhabricatorEmailTagsSetting::SETTINGKEY);991992// The user must have elected to receive mail for at least one993// of the mailtags.994$send = false;995foreach ($tags as $tag) {996if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {997$send = true;998break;999}1000}10011002if (!$send) {1003$actors[$phid]->setUndeliverable(1004PhabricatorMetaMTAActor::REASON_MAILTAGS);1005}1006}1007}10081009foreach ($deliverable as $phid) {1010switch ($this->getRoutingRule($phid)) {1011case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:1012$actors[$phid]->setUndeliverable(1013PhabricatorMetaMTAActor::REASON_ROUTE_AS_NOTIFICATION);1014break;1015case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:1016$actors[$phid]->setDeliverable(1017PhabricatorMetaMTAActor::REASON_ROUTE_AS_MAIL);1018break;1019default:1020// No change.1021break;1022}1023}10241025// If recipients were initially deliverable and were added by "Send me an1026// email" Herald rules, annotate them as such and make them deliverable1027// again, overriding any changes made by the "self mail" and "mail tags"1028// settings.1029$force_recipients = $this->getForceHeraldMailRecipientPHIDs();1030$force_recipients = array_fuse($force_recipients);1031if ($force_recipients) {1032foreach ($deliverable as $phid) {1033if (isset($force_recipients[$phid])) {1034$actors[$phid]->setDeliverable(1035PhabricatorMetaMTAActor::REASON_FORCE_HERALD);1036}1037}1038}10391040// Exclude recipients who don't want any mail. This rule is very strong1041// and runs last.1042foreach ($all_prefs as $phid => $prefs) {1043$exclude = $prefs->getSettingValue(1044PhabricatorEmailNotificationsSetting::SETTINGKEY);1045if ($exclude) {1046$actors[$phid]->setUndeliverable(1047PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);1048}1049}10501051// Unless delivery was forced earlier (password resets, confirmation mail),1052// never send mail to unverified addresses.1053foreach ($actors as $phid => $actor) {1054if ($actor->getIsVerified()) {1055continue;1056}10571058$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNVERIFIED);1059}10601061return $actors;1062}10631064public function getDeliveredHeaders() {1065return $this->getParam('headers.sent');1066}10671068public function setDeliveredHeaders(array $headers) {1069$headers = $this->flattenHeaders($headers);1070return $this->setParam('headers.sent', $headers);1071}10721073public function getUnfilteredHeaders() {1074$unfiltered = $this->getParam('headers.unfiltered');10751076if ($unfiltered === null) {1077// Older versions of Phabricator did not filter headers, and thus did1078// not record unfiltered headers. If we don't have unfiltered header1079// data just return the delivered headers for compatibility.1080return $this->getDeliveredHeaders();1081}10821083return $unfiltered;1084}10851086public function setUnfilteredHeaders(array $headers) {1087$headers = $this->flattenHeaders($headers);1088return $this->setParam('headers.unfiltered', $headers);1089}10901091private function flattenHeaders(array $headers) {1092assert_instances_of($headers, 'PhabricatorMailHeader');10931094$list = array();1095foreach ($list as $header) {1096$list[] = array(1097$header->getName(),1098$header->getValue(),1099);1100}11011102return $list;1103}11041105public function getDeliveredActors() {1106return $this->getParam('actors.sent');1107}11081109public function getDeliveredRoutingRules() {1110return $this->getParam('routing.sent');1111}11121113public function getDeliveredRoutingMap() {1114return $this->getParam('routingmap.sent');1115}11161117public function getDeliveredBody() {1118return $this->getParam('body.sent');1119}11201121public function setDeliveredBody($body) {1122return $this->setParam('body.sent', $body);1123}11241125public function getURI() {1126return '/mail/detail/'.$this->getID().'/';1127}112811291130/* -( Routing )------------------------------------------------------------ */113111321133public function addRoutingRule($routing_rule, $phids, $reason_phid) {1134$routing = $this->getParam('routing', array());1135$routing[] = array(1136'routingRule' => $routing_rule,1137'phids' => $phids,1138'reasonPHID' => $reason_phid,1139);1140$this->setParam('routing', $routing);11411142// Throw the routing map away so we rebuild it.1143$this->routingMap = null;11441145return $this;1146}11471148private function getRoutingRule($phid) {1149$map = $this->getRoutingRuleMap();11501151$info = idx($map, $phid, idx($map, 'default'));1152if ($info) {1153return idx($info, 'rule');1154}11551156return null;1157}11581159private function getRoutingRuleMap() {1160if ($this->routingMap === null) {1161$map = array();11621163$routing = $this->getParam('routing', array());1164foreach ($routing as $route) {1165$phids = $route['phids'];1166if ($phids === null) {1167$phids = array('default');1168}11691170foreach ($phids as $phid) {1171$new_rule = $route['routingRule'];11721173$current_rule = idx($map, $phid);1174if ($current_rule === null) {1175$is_stronger = true;1176} else {1177$is_stronger = PhabricatorMailRoutingRule::isStrongerThan(1178$new_rule,1179$current_rule);1180}11811182if ($is_stronger) {1183$map[$phid] = array(1184'rule' => $new_rule,1185'reason' => $route['reasonPHID'],1186);1187}1188}1189}11901191$this->routingMap = $map;1192}11931194return $this->routingMap;1195}11961197/* -( Preferences )-------------------------------------------------------- */119811991200private function loadPreferences($target_phid) {1201$viewer = PhabricatorUser::getOmnipotentUser();12021203if (self::shouldMailEachRecipient()) {1204$preferences = id(new PhabricatorUserPreferencesQuery())1205->setViewer($viewer)1206->withUserPHIDs(array($target_phid))1207->needSyntheticPreferences(true)1208->executeOne();1209if ($preferences) {1210return $preferences;1211}1212}12131214return PhabricatorUserPreferences::loadGlobalPreferences($viewer);1215}12161217public function shouldRenderMailStampsInBody($viewer) {1218$preferences = $this->loadPreferences($viewer->getPHID());1219$value = $preferences->getSettingValue(1220PhabricatorEmailStampsSetting::SETTINGKEY);12211222return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS);1223}122412251226/* -( PhabricatorPolicyInterface )----------------------------------------- */122712281229public function getCapabilities() {1230return array(1231PhabricatorPolicyCapability::CAN_VIEW,1232);1233}12341235public function getPolicy($capability) {1236return PhabricatorPolicies::POLICY_NOONE;1237}12381239public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {1240$actor_phids = $this->getExpandedRecipientPHIDs();1241return in_array($viewer->getPHID(), $actor_phids);1242}12431244public function describeAutomaticCapability($capability) {1245return pht(1246'The mail sender and message recipients can always see the mail.');1247}124812491250/* -( PhabricatorDestructibleInterface )----------------------------------- */125112521253public function destroyObjectPermanently(1254PhabricatorDestructionEngine $engine) {12551256$files = $this->loadAttachedFiles($engine->getViewer());1257foreach ($files as $file) {1258$engine->destroyObject($file);1259}12601261$this->delete();1262}12631264}126512661267