@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add reply handler for differential revision

Summary:
add email reply handler so that the user can reply to a
differential email to act on the revision. It generates the reply-to
email address, creates email body text with supported commands list, and
handle the action request on the differential revision.

Right now the reply-to handing is disabled in the config file. But a
site using Phabricator can enable it and implement a class
inheriting from DifferentialReplyHandler to enable customized email
handing.

Later we will need to add code to DifferentialMail.php to support
sending separate email to each email recipient to achieve better
security (see D226). The reply-to will be something like
D<revision_id>+<user_id>+<hash>@domain.com. We will create separate task
for it.

Test Plan:
tried comment on a revision from web UI and the email was
sent out as before without any change. When a subclass of
DifferentialReplyHandler is implemented and enabled, email's reply-to is
set and email text is added. Reply to the email with valid command did
create action to the revision.

Reviewed By: epriestley
Reviewers: tuomaspelkonen, epriestley, slawekbiel, dpepper
CC: aran, epriestley, jungejason
Differential Revision: 224

authored by

jungejason and committed by
jungejason
162f34b8 4a761741

+316 -23
+3
conf/default.conf.php
··· 313 313 314 314 'differential.revision-custom-detail-renderer' => null, 315 315 316 + 'phabricator.enable-reply-handling' => false, 317 + 'differential.replyhandler' => 'DifferentialReplyHandler', 318 + 316 319 317 320 // -- Maniphest ------------------------------------------------------------- // 318 321
+3
src/__phutil_library_map__.php
··· 137 137 'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty', 138 138 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents', 139 139 'DifferentialDiffViewController' => 'applications/differential/controller/diffview', 140 + 'DifferentialExceptionMail' => 'applications/differential/mail/exception', 140 141 'DifferentialHunk' => 'applications/differential/storage/hunk', 141 142 'DifferentialInlineComment' => 'applications/differential/storage/inlinecomment', 142 143 'DifferentialInlineCommentEditController' => 'applications/differential/controller/inlinecommentedit', ··· 146 147 'DifferentialMail' => 'applications/differential/mail/base', 147 148 'DifferentialMarkupEngineFactory' => 'applications/differential/parser/markup', 148 149 'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff', 150 + 'DifferentialReplyHandler' => 'applications/differential/replyhandler', 149 151 'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest', 150 152 'DifferentialRevision' => 'applications/differential/storage/revision', 151 153 'DifferentialRevisionCommentListView' => 'applications/differential/view/revisioncommentlist', ··· 601 603 'DifferentialDiffProperty' => 'DifferentialDAO', 602 604 'DifferentialDiffTableOfContentsView' => 'AphrontView', 603 605 'DifferentialDiffViewController' => 'DifferentialController', 606 + 'DifferentialExceptionMail' => 'DifferentialMail', 604 607 'DifferentialHunk' => 'DifferentialDAO', 605 608 'DifferentialInlineComment' => 'DifferentialDAO', 606 609 'DifferentialInlineCommentEditController' => 'DifferentialController',
+61 -23
src/applications/differential/mail/base/DifferentialMail.php
··· 33 33 protected $isFirstMailToRecipients; 34 34 protected $heraldTranscriptURI; 35 35 protected $heraldRulesHeader; 36 + protected $replyHandler; 36 37 37 38 abstract protected function renderSubject(); 38 39 abstract protected function renderBody(); ··· 70 71 $body = $this->buildBody(); 71 72 72 73 $mail = new PhabricatorMetaMTAMail(); 73 - $handle = $this->getActorHandle(); 74 - $reply = $this->getReplyHandlerEmailAddress(); 75 - if ($handle) { 76 - $mail->setFrom($handle->getPHID()); 77 - if ($reply) { 78 - $mail->setReplyTo($this->getReplyHandlerEmailAddress()); 74 + $actor_handle = $this->getActorHandle(); 75 + $reply_handler = $this->getReplyHandler(); 76 + 77 + if ($actor_handle) { 78 + $mail->setFrom($actor_handle->getPHID()); 79 + } 80 + 81 + if ($reply_handler) { 82 + if ($actor_handle) { 83 + $actor = id(new PhabricatorUser())->loadOneWhere( 84 + 'phid = %s', 85 + $actor_handle->getPHID()); 86 + $reply_handler->setActor($actor); 79 87 } 80 - } else { 81 - if ($reply) { 82 - $mail->setFrom($this->getReplyHandlerEmailAddress()); 88 + 89 + $reply_to = $reply_handler->getReplyHandlerEmailAddress(); 90 + if ($reply_to) { 91 + $mail->setReplyTo($reply_to); 83 92 } 84 93 } 85 94 ··· 114 123 115 124 protected function buildBody() { 116 125 117 - $actions = array(); 118 126 $body = $this->renderBody(); 119 - /* 120 - $body .= <<<EOTEXT 121 127 122 - ACTIONS 123 - Reply to comment, or !accept, !reject, !abandon, !resign, or !showdiff. 124 - 125 - EOTEXT; 126 - */ 128 + $handler_body_text = $this->getReplyHandlerBodyText(); 129 + if ($handler_body_text) { 130 + $body .= $handler_body_text; 131 + } 127 132 128 133 if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) { 129 134 $manage_uri = PhabricatorEnv::getProductionURI( ··· 146 151 return $body; 147 152 } 148 153 149 - protected function getReplyHandlerEmailAddress() { 150 - return null; 151 - // TODO 152 - $phid = $this->getRevision()->getPHID(); 153 - $server = 'todo.example.com'; 154 - return "differential+{$phid}@{$server}"; 154 + protected function getReplyHandlerBodyText() { 155 + $reply_handler = $this->getReplyHandler(); 156 + 157 + if (!$reply_handler) { 158 + return null; 159 + } 160 + 161 + return $reply_handler->getBodyText(); 162 + } 163 + 164 + protected function getReplyHandler() { 165 + if ($this->replyHandler) { 166 + return $this->replyHandler; 167 + } 168 + 169 + $reply_handler = self::loadReplyHandler(); 170 + if (!$reply_handler) { 171 + return null; 172 + } 173 + 174 + $reply_handler->setRevision($this->getRevision()); 175 + $this->replyHandler = $reply_handler; 176 + return $this->replyHandler; 177 + } 178 + 179 + public static function loadReplyHandler() { 180 + if (!PhabricatorEnv::getEnvConfig('phabricator.enable-reply-handling')) { 181 + return null; 182 + } 183 + 184 + $reply_handler = PhabricatorEnv::getEnvConfig('differential.replyhandler'); 185 + 186 + if (!$reply_handler) { 187 + return null; 188 + } 189 + 190 + PhutilSymbolLoader::loadClass($reply_handler); 191 + $reply_handler = newv($reply_handler, array()); 192 + return $reply_handler; 155 193 } 156 194 157 195 protected function formatText($text) {
+4
src/applications/differential/mail/base/__init__.php
··· 7 7 8 8 9 9 phutil_require_module('phabricator', 'applications/metamta/storage/mail'); 10 + phutil_require_module('phabricator', 'applications/people/storage/user'); 10 11 phutil_require_module('phabricator', 'infrastructure/env'); 12 + 13 + phutil_require_module('phutil', 'symbols'); 14 + phutil_require_module('phutil', 'utils'); 11 15 12 16 13 17 phutil_require_source('DifferentialMail.php');
+62
src/applications/differential/mail/exception/DifferentialExceptionMail.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2011 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + class DifferentialExceptionMail extends DifferentialMail { 20 + 21 + public function __construct( 22 + DifferentialRevision $revision, 23 + Exception $exception, 24 + $original_body) { 25 + 26 + $this->revision = $revision; 27 + $this->exception = $exception; 28 + $this->originalBody = $original_body; 29 + } 30 + 31 + protected function renderBody() { 32 + // Never called since buildBody() is overridden. 33 + } 34 + 35 + protected function renderSubject() { 36 + return "Exception: unable to process your mail request."; 37 + } 38 + 39 + protected function buildBody() { 40 + $exception = $this->exception; 41 + $original_body = $this->originalBody; 42 + 43 + $message = $exception->getMessage(); 44 + $trace = $exception->getTraceAsString(); 45 + 46 + return <<<EOBODY 47 + Your request failed because an exception was encoutered while processing it: 48 + 49 + EXCEPTION: {$message} 50 + 51 + {$trace} 52 + 53 + -- Original Body -------------------------- 54 + 55 + {$original_body} 56 + 57 + EOBODY; 58 + } 59 + 60 + } 61 + 62 +
+12
src/applications/differential/mail/exception/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/differential/mail/base'); 10 + 11 + 12 + phutil_require_source('DifferentialExceptionMail.php');
+156
src/applications/differential/replyhandler/DifferentialReplyHandler.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2011 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + class DifferentialReplyHandler { 20 + protected $revision; 21 + protected $actor; 22 + 23 + /* 24 + * Generate text like the following from the supported commands. 25 + * " 26 + * 27 + * ACTIONS 28 + * Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim. 29 + * 30 + * " 31 + */ 32 + public function getBodyText() { 33 + $supported_commands = $this->getSupportedCommands(); 34 + $text = ''; 35 + if (empty($supported_commands)) { 36 + return $text; 37 + } 38 + 39 + $comment_command_printed = false; 40 + $text .= "\nACTIONS\n"; 41 + if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) { 42 + $text .= 'Reply to comment'; 43 + $comment_command_printed = true; 44 + 45 + $supported_commands = array_diff( 46 + $supported_commands, array(DifferentialAction::ACTION_COMMENT)); 47 + } 48 + 49 + if (!empty($supported_commands)) { 50 + if ($comment_command_printed) { 51 + $text .= ', or '; 52 + } 53 + 54 + $modified_commands = array(); 55 + foreach ($supported_commands as $command) { 56 + $modified_commands[] = '!'.$command; 57 + } 58 + 59 + $text .= implode(', ', $modified_commands); 60 + } 61 + 62 + $text .= ".\n\n"; 63 + 64 + return $text; 65 + } 66 + 67 + public function getSupportedCommands() { 68 + return array( 69 + DifferentialAction::ACTION_COMMENT, 70 + DifferentialAction::ACTION_ACCEPT, 71 + DifferentialAction::ACTION_REJECT, 72 + DifferentialAction::ACTION_ABANDON, 73 + DifferentialAction::ACTION_RECLAIM, 74 + DifferentialAction::ACTION_RESIGN, 75 + ); 76 + } 77 + 78 + public function getReplyHandlerEmailAddress() { 79 + if (!self::isEnabled()) { 80 + return null; 81 + } 82 + 83 + $revision = $this->getRevision(); 84 + if (!$revision) { 85 + return null; 86 + } 87 + 88 + return '...'; // TODO: build the D1234+92+aldsbn@domain.com as per D226 89 + } 90 + 91 + public function handleAction($body) { 92 + // all commands start with a bang and separated from the body by a newline 93 + // to make sure that actual feedback text couldn't trigger an action. 94 + // unrecognized commands will be parsed as part of the comment. 95 + $command = DifferentialAction::ACTION_COMMENT; 96 + $supported_commands = $this->getSupportedCommands(); 97 + $regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/"; 98 + $matches = array(); 99 + if (preg_match($regex, $body, $matches)) { 100 + $command = $matches[1]; 101 + $body = trim(str_replace('!' . $command, '', $body)); 102 + } 103 + 104 + $actor = $this->getActor(); 105 + if (!$actor) { 106 + throw new Exception('No actor is set for the reply action.'); 107 + } 108 + 109 + try { 110 + $editor = new DifferentialCommentEditor( 111 + $this->getRevision(), 112 + $actor->getPHID(), 113 + $command); 114 + 115 + $editor->setMessage($body); 116 + $editor->setAddCC(($command != DifferentialAction::ACTION_RESIGN)); 117 + $comment = $editor->save(); 118 + 119 + return $comment->getID(); 120 + 121 + } catch (Exception $ex) { 122 + $exception_mail = new DifferentialExceptionMail( 123 + $this->getRevision(), 124 + $ex, 125 + $body); 126 + 127 + $exception_mail->setToPHIDs(array($this->getActor()->getPHID())); 128 + $exception_mail->send(); 129 + 130 + throw $ex; 131 + } 132 + } 133 + 134 + public function setActor(PhabricatorUser $actor) { 135 + $this->actor = $actor; 136 + return $this; 137 + } 138 + 139 + public function getActor() { 140 + return $this->actor; 141 + } 142 + 143 + public function setRevision(DifferentialRevision $revision) { 144 + $this->revision = $revision; 145 + return $this; 146 + } 147 + 148 + public function getRevision() { 149 + return $this->revision; 150 + } 151 + 152 + public static function isEnabled() { 153 + return PhabricatorEnv::getEnvConfig('phabricator.enable-reply-handling'); 154 + } 155 + 156 + }
+15
src/applications/differential/replyhandler/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/differential/constants/action'); 10 + phutil_require_module('phabricator', 'applications/differential/editor/comment'); 11 + phutil_require_module('phabricator', 'applications/differential/mail/exception'); 12 + phutil_require_module('phabricator', 'infrastructure/env'); 13 + 14 + 15 + phutil_require_source('DifferentialReplyHandler.php');