diff --git a/src/applications/differential/DifferentialReplyHandler.php b/src/applications/differential/DifferentialReplyHandler.php index 49e03155a1..50b2c4099d 100644 --- a/src/applications/differential/DifferentialReplyHandler.php +++ b/src/applications/differential/DifferentialReplyHandler.php @@ -1,188 +1,190 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'D'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('D'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.differential.reply-handler-domain'); } /* * Generate text like the following from the supported commands. * " * * ACTIONS * Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim. * * " */ public function getReplyHandlerInstructions() { if (!$this->supportsReplies()) { return null; } $supported_commands = $this->getSupportedCommands(); $text = ''; if (empty($supported_commands)) { return $text; } $comment_command_printed = false; if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) { $text .= 'Reply to comment'; $comment_command_printed = true; $supported_commands = array_diff( $supported_commands, array(DifferentialAction::ACTION_COMMENT)); } if (!empty($supported_commands)) { if ($comment_command_printed) { $text .= ', or '; } $modified_commands = array(); foreach ($supported_commands as $command) { $modified_commands[] = '!'.$command; } $text .= implode(', ', $modified_commands); } $text .= "."; return $text; } public function getSupportedCommands() { $actions = array( DifferentialAction::ACTION_COMMENT, DifferentialAction::ACTION_REJECT, DifferentialAction::ACTION_ABANDON, DifferentialAction::ACTION_RECLAIM, DifferentialAction::ACTION_RESIGN, DifferentialAction::ACTION_RETHINK, 'unsubscribe', ); if (PhabricatorEnv::getEnvConfig('differential.enable-email-accept')) { $actions[] = DifferentialAction::ACTION_ACCEPT; } return $actions; } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $this->receivedMail = $mail; - $this->handleAction($mail->getCleanTextBody()); + $this->handleAction($mail->getCleanTextBody(), $mail->getAttachments()); } - public function handleAction($body) { + public function handleAction($body, array $attachments) { // all commands start with a bang and separated from the body by a newline // to make sure that actual feedback text couldn't trigger an action. // unrecognized commands will be parsed as part of the comment. $command = DifferentialAction::ACTION_COMMENT; $supported_commands = $this->getSupportedCommands(); $regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/"; $matches = array(); if (preg_match($regex, $body, $matches)) { $command = $matches[1]; $body = trim(str_replace('!' . $command, '', $body)); } $actor = $this->getActor(); if (!$actor) { throw new Exception('No actor is set for the reply action.'); } switch ($command) { case 'unsubscribe': $this->unsubscribeUser($this->getMailReceiver(), $actor); // TODO: Send the user a confirmation email? return null; } + $body = $this->enhanceBodyWithAttachments($body, $attachments); + try { $editor = new DifferentialCommentEditor( $this->getMailReceiver(), $command); $editor->setActor($actor); $editor->setExcludeMailRecipientPHIDs( $this->getExcludeMailRecipientPHIDs()); // NOTE: We have to be careful about this because Facebook's // implementation jumps straight into handleAction() and will not have // a PhabricatorMetaMTAReceivedMail object. if ($this->receivedMail) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $this->receivedMail->getID(), )); $editor->setContentSource($content_source); $editor->setParentMessageID($this->receivedMail->getMessageID()); } $editor->setMessage($body); $comment = $editor->save(); return $comment->getID(); } catch (Exception $ex) { $exception_mail = new DifferentialExceptionMail( $this->getMailReceiver(), $ex, $this->receivedMail->getRawTextBody()); $exception_mail->setToPHIDs(array($this->getActor()->getPHID())); $exception_mail->send(); throw $ex; } } private function unsubscribeUser( DifferentialRevision $revision, PhabricatorUser $user) { $revision->loadRelationships(); DifferentialRevisionEditor::removeCCAndUpdateRevision( $revision, $user->getPHID(), $user->getPHID()); } } diff --git a/src/applications/maniphest/ManiphestReplyHandler.php b/src/applications/maniphest/ManiphestReplyHandler.php index 423f36e1b4..5eb9ab23f7 100644 --- a/src/applications/maniphest/ManiphestReplyHandler.php +++ b/src/applications/maniphest/ManiphestReplyHandler.php @@ -1,188 +1,182 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'T'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('T'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.maniphest.reply-handler-domain'); } public function getReplyHandlerInstructions() { if ($this->supportsReplies()) { return "Reply to comment or attach files, or !close, !claim, or ". "!unsubscribe."; } else { return null; } } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { // NOTE: We'll drop in here on both the "reply to a task" and "create a // new task" workflows! Make sure you test both if you make changes! $task = $this->getMailReceiver(); $is_new_task = !$task->getID(); $user = $this->getActor(); $body = $mail->getCleanTextBody(); $body = trim($body); + $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments()); $xactions = array(); - $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $mail->getID(), )); $template = new ManiphestTransaction(); $template->setContentSource($content_source); $template->setAuthorPHID($user->getPHID()); + if ($is_new_task) { // If this is a new task, create a "User created this task." transaction // and then set the title and description. $xaction = clone $template; $xaction->setTransactionType(ManiphestTransactionType::TYPE_STATUS); $xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN); $xactions[] = $xaction; $task->setAuthorPHID($user->getPHID()); $task->setTitle(nonempty($mail->getSubject(), 'Untitled Task')); $task->setDescription($body); $task->setPriority(ManiphestTaskPriority::getDefaultPriority()); } else { $lines = explode("\n", trim($body)); $first_line = head($lines); $command = null; $matches = null; if (preg_match('/^!(\w+)/', $first_line, $matches)) { $lines = array_slice($lines, 1); $body = implode("\n", $lines); $body = trim($body); $command = $matches[1]; } $ttype = ManiphestTransactionType::TYPE_NONE; $new_value = null; switch ($command) { case 'close': $ttype = ManiphestTransactionType::TYPE_STATUS; $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED; break; case 'claim': $ttype = ManiphestTransactionType::TYPE_OWNER; $new_value = $user->getPHID(); break; case 'unsubscribe': $ttype = ManiphestTransactionType::TYPE_CCS; $ccs = $task->getCCPHIDs(); foreach ($ccs as $k => $phid) { if ($phid == $user->getPHID()) { unset($ccs[$k]); } } $new_value = array_values($ccs); break; } $xaction = clone $template; $xaction->setTransactionType($ttype); $xaction->setNewValue($new_value); $xaction->setComments($body); $xactions[] = $xaction; } - // TODO: We should look at CCs on the mail and add them as CCs. - - $files = $mail->getAttachments(); - if ($files) { - $file_xaction = clone $template; - $file_xaction->setTransactionType(ManiphestTransactionType::TYPE_ATTACH); - - $phid_type = PhabricatorPHIDConstants::PHID_TYPE_FILE; - $new = $task->getAttached(); - foreach ($files as $file_phid) { - $new[$phid_type][$file_phid] = array(); - } - - $file_xaction->setNewValue($new); - $xactions[] = $file_xaction; + $ccs = $mail->loadCCPHIDs(); + if ($ccs) { + $old_ccs = $task->getCCPHIDs(); + $new_ccs = array_unique(array_merge($old_ccs, $ccs)); + $cc_xaction = clone $template; + $cc_xaction->setTransactionType(ManiphestTransactionType::TYPE_CCS); + $cc_xaction->setNewValue($new_ccs); + $xactions[] = $cc_xaction; } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'mail' => $mail, 'new' => $is_new_task, 'transactions' => $xactions, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $xactions = $event->getValue('transactions'); $editor = new ManiphestTransactionEditor(); $editor->setActor($user); $editor->setParentMessageID($mail->getMessageID()); $editor->setExcludeMailRecipientPHIDs( $this->getExcludeMailRecipientPHIDs()); $editor->applyTransactions($task, $xactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new_task, 'transactions' => $xactions, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); } } diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index 02429d9d36..7ea011f1a9 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -1,308 +1,332 @@ validateMailReceiver($mail_receiver); $this->mailReceiver = $mail_receiver; return $this; } final public function getMailReceiver() { return $this->mailReceiver; } final public function setActor(PhabricatorUser $actor) { $this->actor = $actor; return $this; } final public function getActor() { return $this->actor; } final public function setExcludeMailRecipientPHIDs(array $exclude) { $this->excludePHIDs = $exclude; return $this; } final public function getExcludeMailRecipientPHIDs() { return $this->excludePHIDs; } abstract public function validateMailReceiver($mail_receiver); abstract public function getPrivateReplyHandlerEmailAddress( PhabricatorObjectHandle $handle); abstract public function getReplyHandlerDomain(); abstract public function getReplyHandlerInstructions(); abstract protected function receiveEmail( PhabricatorMetaMTAReceivedMail $mail); public function processEmail(PhabricatorMetaMTAReceivedMail $mail) { $error = $this->sanityCheckEmail($mail); if ($error) { if ($this->shouldSendErrorEmail($mail)) { $this->sendErrorEmail($error, $mail); } return null; } return $this->receiveEmail($mail); } private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) { - $body = $mail->getCleanTextBody(); - if (empty($body)) { + $body = $mail->getCleanTextBody(); + $attachments = $mail->getAttachments(); + + if (empty($body) && empty($attachments)) { return 'Empty email body. Email should begin with an !action and / or '. 'text to comment. Inline replies and signatures are ignored.'; } return null; } /** * Only send an error email if the user is talking to just Phabricator. We * can assume if there is only one To address it is a Phabricator address * since this code is running and everything. */ private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) { return count($mail->getToAddresses() == 1) && count($mail->getCCAddresses() == 0); } private function sendErrorEmail($error, PhabricatorMetaMTAReceivedMail $mail) { $template = new PhabricatorMetaMTAMail(); $template->setSubject('Exception: unable to process your mail request'); $template->setBody($this->buildErrorMailBody($error, $mail)); $template->setRelatedPHID($mail->getRelatedPHID()); $phid = $this->getActor()->getPHID(); $tos = array( $phid => PhabricatorObjectHandleData::loadOneHandle($phid) ); $mails = $this->multiplexMail($template, $tos, array()); foreach ($mails as $email) { $email->saveAndSend(); } return true; } private function buildErrorMailBody($error, PhabricatorMetaMTAReceivedMail $mail) { $original_body = $mail->getRawTextBody(); $main_body = <<addRawSection($main_body); $body->addReplySection($this->getReplyHandlerInstructions()); return $body->render(); } public function supportsPrivateReplies() { return (bool)$this->getReplyHandlerDomain() && !$this->supportsPublicReplies(); } public function supportsPublicReplies() { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { return false; } if (!$this->getReplyHandlerDomain()) { return false; } return (bool)$this->getPublicReplyHandlerEmailAddress(); } final public function supportsReplies() { return $this->supportsPrivateReplies() || $this->supportsPublicReplies(); } public function getPublicReplyHandlerEmailAddress() { return null; } final public function getRecipientsSummary( array $to_handles, array $cc_handles) { assert_instances_of($to_handles, 'PhabricatorObjectHandle'); assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); $body = ''; if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { if ($to_handles) { $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n"; } if ($cc_handles) { $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n"; } } return $body; } final public function multiplexMail( PhabricatorMetaMTAMail $mail_template, array $to_handles, array $cc_handles) { assert_instances_of($to_handles, 'PhabricatorObjectHandle'); assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); $result = array(); // If MetaMTA is configured to always multiplex, skip the single-email // case. if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { // If private replies are not supported, simply send one email to all // recipients and CCs. This covers cases where we have no reply handler, // or we have a public reply handler. if (!$this->supportsPrivateReplies()) { $mail = clone $mail_template; $mail->addTos(mpull($to_handles, 'getPHID')); $mail->addCCs(mpull($cc_handles, 'getPHID')); if ($this->supportsPublicReplies()) { $reply_to = $this->getPublicReplyHandlerEmailAddress(); $mail->setReplyTo($reply_to); } $result[] = $mail; return $result; } } $tos = mpull($to_handles, null, 'getPHID'); $ccs = mpull($cc_handles, null, 'getPHID'); // Merge all the recipients together. TODO: We could keep the CCs as real // CCs and send to a "noreply@domain.com" type address, but keep it simple // for now. $recipients = $tos + $ccs; // When multiplexing mail, explicitly include To/Cc information in the // message body and headers. $mail_template = clone $mail_template; $mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos)); $mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs)); $body = $mail_template->getBody(); $body .= "\n"; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); foreach ($recipients as $phid => $recipient) { $mail = clone $mail_template; if (isset($to_handles[$phid])) { $mail->addTos(array($phid)); } else if (isset($cc_handles[$phid])) { $mail->addCCs(array($phid)); } else { // not good - they should be a to or a cc continue; } $mail->setBody($body); $reply_to = null; if (!$reply_to && $this->supportsPrivateReplies()) { $reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient); } if (!$reply_to && $this->supportsPublicReplies()) { $reply_to = $this->getPublicReplyHandlerEmailAddress(); } if ($reply_to) { $mail->setReplyTo($reply_to); } $result[] = $mail; } return $result; } protected function getDefaultPublicReplyHandlerEmailAddress($prefix) { $receiver = $this->getMailReceiver(); $receiver_id = $receiver->getID(); $domain = $this->getReplyHandlerDomain(); // We compute a hash using the object's own PHID to prevent an attacker // from blindly interacting with objects that they haven't ever received // mail about by just sending to D1@, D2@, etc... $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( $receiver->getMailKey(), $receiver->getPHID()); $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}"; return $this->getSingleReplyHandlerPrefix($address); } protected function getSingleReplyHandlerPrefix($address) { $single_handle_prefix = PhabricatorEnv::getEnvConfig( 'metamta.single-reply-handler-prefix'); return ($single_handle_prefix) ? $single_handle_prefix . '+' . $address : $address; } protected function getDefaultPrivateReplyHandlerEmailAddress( PhabricatorObjectHandle $handle, $prefix) { if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) { // You must be a real user to get a private reply handler address. return null; } $receiver = $this->getMailReceiver(); $receiver_id = $receiver->getID(); $user_id = $handle->getAlternateID(); $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( $receiver->getMailKey(), $handle->getPHID()); $domain = $this->getReplyHandlerDomain(); $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}"; return $this->getSingleReplyHandlerPrefix($address); } + final protected function enhanceBodyWithAttachments($body, + array $attachments) { + if (!$attachments) { + return $body; + } + + $files = id(new PhabricatorFile()) + ->loadAllWhere('phid in (%Ls)', $attachments); + + // if we have some text then double return before adding our file list + if ($body) { + $body .= "\n\n"; + } + + foreach ($files as $file) { + $file_str = sprintf('- {F%d, layout=link}', $file->getID()); + $body .= $file_str."\n"; + } + + return rtrim($body); + } + } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 2ddd902644..fcd6f60bde 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -1,392 +1,400 @@ array( 'headers' => self::SERIALIZATION_JSON, 'bodies' => self::SERIALIZATION_JSON, 'attachments' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setHeaders(array $headers) { // Normalize headers to lowercase. $normalized = array(); foreach ($headers as $name => $value) { $normalized[strtolower($name)] = $value; } $this->headers = $normalized; return $this; } public function getMessageID() { return idx($this->headers, 'message-id'); } public function getSubject() { return idx($this->headers, 'subject'); } public function getCCAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'cc')); } public function getToAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'to')); } private function loadExcludeMailRecipientPHIDs() { $addresses = array_merge( $this->getToAddresses(), $this->getCCAddresses() ); + return $this->loadPHIDsFromAddresses($addresses); + } + + final public function loadCCPHIDs() { + return $this->loadPHIDsFromAddresses($this->getCCAddresses()); + } + + private function loadPHIDsFromAddresses(array $addresses) { $users = id(new PhabricatorUserEmail()) ->loadAllWhere('address IN (%Ls)', $addresses); $user_phids = mpull($users, 'getUserPHID'); $mailing_lists = id(new PhabricatorMetaMTAMailingList()) ->loadAllWhere('email in (%Ls)', $addresses); $mailing_list_phids = mpull($mailing_lists, 'getPHID'); return array_merge($user_phids, $mailing_list_phids); } /** * Parses "to" addresses, looking for a public create email address * first and if not found parsing the "to" address for reply handler * information: receiver name, user id, and hash. */ private function getPhabricatorToInformation() { // Only one "public" create address so far $create_task = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); // For replies, look for an object address with a format like: // D291+291+b0a41ca848d66dcc@example.com $single_handle_prefix = PhabricatorEnv::getEnvConfig( 'metamta.single-reply-handler-prefix'); $prefixPattern = ($single_handle_prefix) ? preg_quote($single_handle_prefix, '/') . '\+' : ''; $pattern = "/^{$prefixPattern}((?:D|T|C)\d+)\+([\w]+)\+([a-f0-9]{16})@/U"; $phabricator_address = null; $receiver_name = null; $user_id = null; $hash = null; foreach ($this->getToAddresses() as $address) { if ($address == $create_task) { $phabricator_address = $address; // it's okay to stop here because we just need to map a create // address to an application and don't need / won't have more // information in these cases. break; } $matches = null; $ok = preg_match( $pattern, $address, $matches); if ($ok) { $phabricator_address = $address; $receiver_name = $matches[1]; $user_id = $matches[2]; $hash = $matches[3]; break; } } return array( $phabricator_address, $receiver_name, $user_id, $hash ); } public function processReceivedMail() { // If Phabricator sent the mail, always drop it immediately. This prevents // loops where, e.g., the public bug address is also a user email address // and creating a bug sends them an email, which loops. $is_phabricator_mail = idx( $this->headers, 'x-phabricator-sent-this-message'); if ($is_phabricator_mail) { $message = "Ignoring email with 'X-Phabricator-Sent-This-Message' ". "header to avoid loops."; return $this->setMessage($message)->save(); } list($to, $receiver_name, $user_id, $hash) = $this->getPhabricatorToInformation(); if (!$to) { $raw_to = idx($this->headers, 'to'); return $this->setMessage("Unrecognized 'to' format: {$raw_to}")->save(); } $from = idx($this->headers, 'from'); // TODO -- make this a switch statement / better if / when we add more // public create email addresses! $create_task = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); if ($create_task && $to == $create_task) { $receiver = new ManiphestTask(); $user = $this->lookupPublicUser(); if ($user) { $this->setAuthorPHID($user->getPHID()); } else { $default_author = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.default-public-author'); if ($default_author) { $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $default_author); if ($user) { $receiver->setOriginalEmailSource($from); } else { throw new Exception( "Phabricator is misconfigured, the configuration key ". "'metamta.maniphest.default-public-author' is set to user ". "'{$default_author}' but that user does not exist."); } } else { // TODO: We should probably bounce these since from the user's // perspective their email vanishes into a black hole. return $this->setMessage("Invalid public user '{$from}'.")->save(); } } $receiver->setAuthorPHID($user->getPHID()); $receiver->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); $editor = new ManiphestTransactionEditor(); $editor->setActor($user); $handler = $editor->buildReplyHandler($receiver); $handler->setActor($user); $handler->setExcludeMailRecipientPHIDs( $this->loadExcludeMailRecipientPHIDs()); $handler->processEmail($this); $this->setRelatedPHID($receiver->getPHID()); $this->setMessage('OK'); return $this->save(); } if ($user_id == 'public') { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { return $this->setMessage("Public replies not enabled.")->save(); } $user = $this->lookupPublicUser(); if (!$user) { return $this->setMessage("Invalid public user '{$from}'.")->save(); } $use_user_hash = false; } else { $user = id(new PhabricatorUser())->load($user_id); if (!$user) { return $this->setMessage("Invalid private user '{$user_id}'.")->save(); } $use_user_hash = true; } if ($user->getIsDisabled()) { return $this->setMessage("User '{$user_id}' is disabled")->save(); } $this->setAuthorPHID($user->getPHID()); $receiver = self::loadReceiverObject($receiver_name); if (!$receiver) { return $this->setMessage("Invalid object '{$receiver_name}'")->save(); } $this->setRelatedPHID($receiver->getPHID()); if ($use_user_hash) { // This is a private reply-to address, check that the user hash is // correct. $check_phid = $user->getPHID(); } else { // This is a public reply-to address, check that the object hash is // correct. $check_phid = $receiver->getPHID(); } $expect_hash = self::computeMailHash($receiver->getMailKey(), $check_phid); // See note at computeOldMailHash(). $old_hash = self::computeOldMailHash($receiver->getMailKey(), $check_phid); if ($expect_hash != $hash && $old_hash != $hash) { return $this->setMessage("Invalid mail hash!")->save(); } if ($receiver instanceof ManiphestTask) { $editor = new ManiphestTransactionEditor(); $editor->setActor($user); $handler = $editor->buildReplyHandler($receiver); } else if ($receiver instanceof DifferentialRevision) { $handler = DifferentialMail::newReplyHandlerForRevision($receiver); } else if ($receiver instanceof PhabricatorRepositoryCommit) { $handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit( $receiver); } $handler->setActor($user); $handler->setExcludeMailRecipientPHIDs( $this->loadExcludeMailRecipientPHIDs()); $handler->processEmail($this); $this->setMessage('OK'); return $this->save(); } public function getCleanTextBody() { $body = idx($this->bodies, 'text'); $parser = new PhabricatorMetaMTAEmailBodyParser(); return $parser->stripTextBody($body); } public function getRawTextBody() { return idx($this->bodies, 'text'); } public static function loadReceiverObject($receiver_name) { if (!$receiver_name) { return null; } $receiver_type = $receiver_name[0]; $receiver_id = substr($receiver_name, 1); $class_obj = null; switch ($receiver_type) { case 'T': $class_obj = new ManiphestTask(); break; case 'D': $class_obj = new DifferentialRevision(); break; case 'C': $class_obj = new PhabricatorRepositoryCommit(); break; default: return null; } return $class_obj->load($receiver_id); } public static function computeMailHash($mail_key, $phid) { $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); $hash = PhabricatorHash::digest($mail_key.$global_mail_key.$phid); return substr($hash, 0, 16); } public static function computeOldMailHash($mail_key, $phid) { // TODO: Remove this method entirely in a couple of months. We've moved from // plain sha1 to sha1+hmac to make the codebase more auditable for good uses // of hash functions, but still accept the old hashes on email replies to // avoid breaking things. Once we've been sending only hmac hashes for a // while, remove this and start rejecting old hashes. See T547. $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); $hash = sha1($mail_key.$global_mail_key.$phid); return substr($hash, 0, 16); } /** * Strip an email address down to the actual user@domain.tld part if * necessary, since sometimes it will have formatting like * '"Abraham Lincoln" '. */ private function getRawEmailAddress($address) { $matches = null; $ok = preg_match('/<(.*)>/', $address, $matches); if ($ok) { $address = $matches[1]; } return $address; } private function getRawEmailAddresses($addresses) { $raw_addresses = array(); foreach (explode(',', $addresses) as $address) { $raw_addresses[] = $this->getRawEmailAddress($address); } return $raw_addresses; } private function lookupPublicUser() { $from = idx($this->headers, 'from'); $from = $this->getRawEmailAddress($from); $user = PhabricatorUser::loadOneWithEmailAddress($from); // If Phabricator is configured to allow "Reply-To" authentication, try // the "Reply-To" address if we failed to match the "From" address. $config_key = 'metamta.insecure-auth-with-reply-to'; $allow_reply_to = PhabricatorEnv::getEnvConfig($config_key); if (!$user && $allow_reply_to) { $reply_to = idx($this->headers, 'reply-to'); $reply_to = $this->getRawEmailAddress($reply_to); if ($reply_to) { $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); } } return $user; } }