actorName; } public function setActorName($actor_name) { $this->actorName = $actor_name; return $this; } abstract protected function renderSubject(); abstract protected function renderBody(); public function setXHeraldRulesHeader($header) { $this->heraldRulesHeader = $header; return $this; } public function send() { $to_phids = $this->getToPHIDs(); if (!$to_phids) { throw new Exception('No "To:" users provided!'); } $message_id = $this->getMessageID(); $cc_phids = $this->getCCPHIDs(); $subject = $this->buildSubject(); $body = $this->buildBody(); $mail = new PhabricatorMetaMTAMail(); if ($this->getActorID()) { $mail->setFrom($this->getActorID()); $mail->setReplyTo($this->getReplyHandlerEmailAddress()); } else { $mail->setFrom($this->getReplyHandlerEmailAddress()); } $mail ->addTos($to_phids) ->addCCs($cc_phids) ->setSubject($subject) ->setBody($body) ->setIsHTML($this->shouldMarkMailAsHTML()) ->addHeader('Thread-Topic', $this->getRevision()->getTitle()) ->addHeader('Thread-Index', $this->generateThreadIndex()); if ($this->isFirstMailAboutRevision()) { $mail->addHeader('Message-ID', $message_id); } else { $mail->addHeader('In-Reply-To', $message_id); $mail->addHeader('References', $message_id); } if ($this->heraldRulesHeader) { $mail->addHeader('X-Herald-Rules', $this->heraldRulesHeader); } $mail->setRelatedPHID($this->getRevision()->getPHID()); // Save this to the MetaMTA queue for later delivery to the MTA. $mail->save(); } protected function buildSubject() { return self::SUBJECT_PREFIX.' '.$this->renderSubject(); } protected function shouldMarkMailAsHTML() { return false; } protected function buildBody() { $actions = array(); $body = $this->renderBody(); $body .= <<getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) { $xscript_uri = $this->getHeraldTranscriptURI(); $body .= <<getRevision()->getPHID(); $server = 'todo.example.com'; return "differential+{$phid}@{$server}"; } protected function formatText($text) { $text = explode("\n", $text); foreach ($text as &$line) { $line = rtrim(' '.$line); } unset($line); return implode("\n", $text); } public function setToPHIDs(array $to) { $this->to = $this->filterContactPHIDs($to); return $this; } public function setCCPHIDs(array $cc) { $this->cc = $this->filterContactPHIDs($cc); return $this; } protected function filterContactPHIDs(array $phids) { return $phids; // TODO: actually do this? // Differential revisions use Subscriptions for CCs, so any arbitrary // PHID can end up CC'd to them. Only try to actually send email PHIDs // which have ToolsHandle types that are marked emailable. If we don't // filter here, sending the email will fail. /* $handles = array(); prep(new ToolsHandleData($phids, $handles)); foreach ($handles as $phid => $handle) { if (!$handle->isEmailable()) { unset($handles[$phid]); } } return array_keys($handles); */ } protected function getToPHIDs() { return $this->to; } protected function getCCPHIDs() { return $this->cc; } public function setActorID($actor_id) { $this->actorID = $actor_id; return $this; } public function getActorID() { return $this->actorID; } public function setRevision($revision) { $this->revision = $revision; return $this; } public function getRevision() { return $this->revision; } protected function getMessageID() { $phid = $this->getRevision()->getPHID(); // TODO return ""; } public function setFeedback($feedback) { $this->feedback = $feedback; return $this; } public function getFeedback() { return $this->feedback; } public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->changesets; } public function setInlineComments(array $inline_comments) { $this->inlineComments = $inline_comments; return $this; } public function getInlineComments() { return $this->inlineComments; } public function renderRevisionDetailLink() { $uri = $this->getRevisionURI(); return "REVISION DETAIL\n {$uri}"; } public function getRevisionURI() { // TODO return 'http://local.aphront.com/D'.$this->getRevision()->getID(); } public function setIsFirstMailToRecipients($first) { $this->isFirstMailToRecipients = $first; return $this; } public function isFirstMailToRecipients() { return $this->isFirstMailToRecipients; } public function setIsFirstMailAboutRevision($first) { $this->isFirstMailAboutRevision = $first; return $this; } public function isFirstMailAboutRevision() { return $this->isFirstMailAboutRevision; } protected function generateThreadIndex() { // When threading, Outlook ignores the 'References' and 'In-Reply-To' // headers that most clients use. Instead, it uses a custom 'Thread-Index' // header. The format of this header is something like this (from // camel-exchange-folder.c in Evolution Exchange): /* A new post to a folder gets a 27-byte-long thread index. (The value * is apparently unique but meaningless.) Each reply to a post gets a * 32-byte-long thread index whose first 27 bytes are the same as the * parent's thread index. Each reply to any of those gets a * 37-byte-long thread index, etc. The Thread-Index header contains a * base64 representation of this value. */ // The specific implementation uses a 27-byte header for the first email // a recipient receives, and a random 5-byte suffix (32 bytes total) // thereafter. This means that all the replies are (incorrectly) siblings, // but it would be very difficult to keep track of the entire tree and this // gets us reasonable client behavior. $base = substr(md5($this->getRevision()->getPHID()), 0, 27); if (!$this->isFirstMailAboutRevision()) { // not totally sure, but it seems like outlook orders replies by // thread-index rather than timestamp, so to get these to show up in the // right order we use the time as the last 4 bytes. $base .= ' ' . pack("N", time()); } return base64_encode($base); } public function setHeraldTranscriptURI($herald_transcript_uri) { $this->heraldTranscriptURI = $herald_transcript_uri; return $this; } public function getHeraldTranscriptURI() { return $this->heraldTranscriptURI; } }