diff --git a/src/applications/audit/PhabricatorAuditReplyHandler.php b/src/applications/audit/PhabricatorAuditReplyHandler.php index 203b6c5b7c..32393c7e80 100644 --- a/src/applications/audit/PhabricatorAuditReplyHandler.php +++ b/src/applications/audit/PhabricatorAuditReplyHandler.php @@ -1,68 +1,68 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'C'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('C'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.diffusion.reply-handler-domain'); } public function getReplyHandlerInstructions() { if ($this->supportsReplies()) { return "Reply to comment."; } else { return null; } } - public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { + protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $commit = $this->getMailReceiver(); $actor = $this->getActor(); // TODO: Support !raise, !accept, etc. // TODO: Content sources. $comment = id(new PhabricatorAuditComment()) ->setAction(PhabricatorAuditActionConstants::COMMENT) ->setContent($mail->getCleanTextBody()); $editor = new PhabricatorAuditCommentEditor($commit); $editor->setUser($actor); $editor->addComment($comment); } } diff --git a/src/applications/differential/DifferentialReplyHandler.php b/src/applications/differential/DifferentialReplyHandler.php index 3104c04b26..fc935b7691 100644 --- a/src/applications/differential/DifferentialReplyHandler.php +++ b/src/applications/differential/DifferentialReplyHandler.php @@ -1,186 +1,186 @@ 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; } - public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { + protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $this->receivedMail = $mail; $this->handleAction($mail->getCleanTextBody()); } public function handleAction($body) { // 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; } try { $editor = new DifferentialCommentEditor( $this->getMailReceiver(), $actor->getPHID(), $command); // 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 52935b44f7..d1959dd23e 100644 --- a/src/applications/maniphest/ManiphestReplyHandler.php +++ b/src/applications/maniphest/ManiphestReplyHandler.php @@ -1,184 +1,184 @@ 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; } } - public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { + 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); $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); } 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; } $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->setParentMessageID($mail->getMessageID()); $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 c15057e929..3c74108c3e 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -1,223 +1,298 @@ 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; } abstract public function validateMailReceiver($mail_receiver); abstract public function getPrivateReplyHandlerEmailAddress( PhabricatorObjectHandle $handle); abstract public function getReplyHandlerDomain(); abstract public function getReplyHandlerInstructions(); - abstract public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail); + 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)) { + 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); } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 4a05119952..59736d0538 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -1,312 +1,369 @@ 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')); + } + + /** + * 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(); } - $to = idx($this->headers, 'to'); - $to = $this->getRawEmailAddress($to); + 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(); $handler = $editor->buildReplyHandler($receiver); $handler->setActor($user); - $handler->receiveEmail($this); + $handler->processEmail($this); $this->setRelatedPHID($receiver->getPHID()); $this->setMessage('OK'); return $this->save(); } - // We've already stripped this, so look for an object address which has - // a format like: D291+291+b0a41ca848d66dcc@example.com - $matches = null; - $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"; - - $ok = preg_match( - $pattern, - $to, - $matches); - - if (!$ok) { - return $this->setMessage("Unrecognized 'to' format: {$to}")->save(); - } - - $receiver_name = $matches[1]; - $user_id = $matches[2]; - $hash = $matches[3]; - 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(); $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->receiveEmail($this); + $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; } } diff --git a/src/applications/owners/OwnersPackageReplyHandler.php b/src/applications/owners/OwnersPackageReplyHandler.php index 4c21181cc0..6878f5ab18 100644 --- a/src/applications/owners/OwnersPackageReplyHandler.php +++ b/src/applications/owners/OwnersPackageReplyHandler.php @@ -1,48 +1,48 @@