@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.

Modernize email command parsing

Summary:
Ref T7199. This prepares for an exciting new world of more powerful "!action" commands. In particular:

- We parse multiple commands per mail.
- We parse command arguments (these are currently not used).
- We parse commands at the beginning or end of mail.

Additionally:

- Do a quick modernization pass on all handlers.
- Break legacy compatibility with really hacky Facebook stuff (see T1992). They've theoretically been on notice for a year and a half, and their setup relies on calling very old reply handler APIs directly.
- Some of these handlers had some copy/paste fluff.
- The Releeph handler is unreachable, but fix it //in theory//.

Test Plan:
- Sent mail to a file; used "!unsubscribe".
- Sent mail to a legalpad document; used "!unsubscribe".
- Sent mail to a task; used various "!close", "!claim", "!assign", etc.
- Sent mail to a paste.
- Sent mail to a revision; used various "!reject", "!claim", etc.
- Tried to send mail to a pull request but it's not actually reachable.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7199

Differential Revision: https://secure.phabricator.com/D12230

+231 -167
+55 -72
src/applications/differential/mail/DifferentialReplyHandler.php
··· 1 1 <?php 2 2 3 - /** 4 - * NOTE: Do not extend this! 5 - * 6 - * @concrete-extensible 7 - */ 8 - class DifferentialReplyHandler extends PhabricatorMailReplyHandler { 9 - 10 - private $receivedMail; 3 + final class DifferentialReplyHandler extends PhabricatorMailReplyHandler { 11 4 12 5 public function validateMailReceiver($mail_receiver) { 13 6 if (!($mail_receiver instanceof DifferentialRevision)) { ··· 48 41 } 49 42 50 43 protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { 51 - $this->receivedMail = $mail; 52 - $this->handleAction($mail->getCleanTextBody(), $mail->getAttachments()); 53 - } 44 + $revision = $this->getMailReceiver(); 45 + $viewer = $this->getActor(); 54 46 55 - public function handleAction($body, array $attachments) { 56 - // all commands start with a bang and separated from the body by a newline 57 - // to make sure that actual feedback text couldn't trigger an action. 58 - // unrecognized commands will be parsed as part of the comment. 59 - $command = DifferentialAction::ACTION_COMMENT; 60 - $supported_commands = $this->getSupportedCommands(); 61 - $regex = "/\A\n*!(".implode('|', $supported_commands).")\n*/"; 62 - $matches = array(); 63 - if (preg_match($regex, $body, $matches)) { 64 - $command = $matches[1]; 65 - $body = trim(str_replace('!'.$command, '', $body)); 66 - } 47 + $body_data = $mail->parseBody(); 48 + $body = $body_data['body']; 49 + $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments()); 67 50 68 - $actor = $this->getActor(); 69 - if (!$actor) { 70 - throw new Exception('No actor is set for the reply action.'); 71 - } 51 + $xactions = array(); 52 + $content_source = PhabricatorContentSource::newForSource( 53 + PhabricatorContentSource::SOURCE_EMAIL, 54 + array( 55 + 'id' => $mail->getID(), 56 + )); 72 57 73 - switch ($command) { 74 - case 'unsubscribe': 75 - id(new PhabricatorSubscriptionsEditor()) 76 - ->setActor($actor) 77 - ->setObject($this->getMailReceiver()) 78 - ->unsubscribe(array($actor->getPHID())) 79 - ->save(); 80 - // TODO: Send the user a confirmation email? 81 - return null; 82 - } 58 + $template = id(new DifferentialTransaction()); 83 59 84 - $body = $this->enhanceBodyWithAttachments($body, $attachments); 85 - 86 - $xactions = array(); 87 - 88 - if ($command && ($command != DifferentialAction::ACTION_COMMENT)) { 89 - $xactions[] = id(new DifferentialTransaction()) 90 - ->setTransactionType(DifferentialTransaction::TYPE_ACTION) 91 - ->setNewValue($command); 60 + $commands = $body_data['commands']; 61 + foreach ($commands as $command_argv) { 62 + $command = head($command_argv); 63 + switch ($command) { 64 + case 'unsubscribe': 65 + $xactions[] = id(clone $template) 66 + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 67 + ->setNewValue( 68 + array( 69 + '-' => array($viewer->getPHID()), 70 + )); 71 + break; 72 + case DifferentialAction::ACTION_ACCEPT: 73 + $accept_key = 'differential.enable-email-accept'; 74 + $can_accept = PhabricatorEnv::getEnvConfig($accept_key); 75 + if (!$can_accept) { 76 + throw new Exception( 77 + pht( 78 + 'You can not !accept revisions over email because '. 79 + 'Differential is configured to disallow this.')); 80 + } 81 + // Fall through... 82 + case DifferentialAction::ACTION_REJECT: 83 + case DifferentialAction::ACTION_ABANDON: 84 + case DifferentialAction::ACTION_RECLAIM: 85 + case DifferentialAction::ACTION_RESIGN: 86 + case DifferentialAction::ACTION_RETHINK: 87 + case DifferentialAction::ACTION_CLAIM: 88 + case DifferentialAction::ACTION_REOPEN: 89 + $xactions[] = id(clone $template) 90 + ->setTransactionType(DifferentialTransaction::TYPE_ACTION) 91 + ->setNewValue($command); 92 + break; 93 + } 92 94 } 93 95 94 - if (strlen($body)) { 95 - $xactions[] = id(new DifferentialTransaction()) 96 - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 97 - ->attachComment( 98 - id(new DifferentialTransactionComment()) 99 - ->setContent($body)); 100 - } 96 + $xactions[] = id(clone $template) 97 + ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 98 + ->attachComment( 99 + id(new DifferentialTransactionComment()) 100 + ->setContent($body)); 101 101 102 102 $editor = id(new DifferentialTransactionEditor()) 103 - ->setActor($actor) 103 + ->setActor($viewer) 104 + ->setContentSource($content_source) 104 105 ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) 105 106 ->setContinueOnMissingFields(true) 106 107 ->setContinueOnNoEffect(true); 107 108 108 - // NOTE: We have to be careful about this because Facebook's 109 - // implementation jumps straight into handleAction() and will not have 110 - // a PhabricatorMetaMTAReceivedMail object. 111 - if ($this->receivedMail) { 112 - $content_source = PhabricatorContentSource::newForSource( 113 - PhabricatorContentSource::SOURCE_EMAIL, 114 - array( 115 - 'id' => $this->receivedMail->getID(), 116 - )); 117 - $editor->setContentSource($content_source); 118 - $editor->setParentMessageID($this->receivedMail->getMessageID()); 119 - } else { 120 - $content_source = PhabricatorContentSource::newForSource( 121 - PhabricatorContentSource::SOURCE_LEGACY, 122 - array()); 123 - $editor->setContentSource($content_source); 124 - } 125 - 126 - $editor->applyTransactions($this->getMailReceiver(), $xactions); 109 + $editor->applyTransactions($revision, $xactions); 127 110 } 128 111 129 112 }
+13 -20
src/applications/files/mail/FileReplyHandler.php
··· 32 32 )); 33 33 34 34 $xactions = array(); 35 - $command = $body_data['body']; 36 - 37 - switch ($command) { 38 - case 'unsubscribe': 39 - $xaction = id(new PhabricatorFileTransaction()) 40 - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 41 - ->setNewValue(array('-' => array($actor->getPHID()))); 42 - $xactions[] = $xaction; 43 - break; 35 + $commands = $body_data['commands']; 36 + foreach ($commands as $command) { 37 + switch (head($command)) { 38 + case 'unsubscribe': 39 + $xaction = id(new PhabricatorFileTransaction()) 40 + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 41 + ->setNewValue(array('-' => array($actor->getPHID()))); 42 + $xactions[] = $xaction; 43 + break; 44 + } 44 45 } 45 46 46 47 $xactions[] = id(new PhabricatorFileTransaction()) 47 48 ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 48 49 ->attachComment( 49 - id(new PhabricatorFileTransactionComment()) 50 - ->setContent($body)); 50 + id(new PhabricatorFileTransactionComment()) 51 + ->setContent($body)); 51 52 52 53 $editor = id(new PhabricatorFileEditor()) 53 54 ->setActor($actor) ··· 55 56 ->setContinueOnNoEffect(true) 56 57 ->setIsPreview(false); 57 58 58 - try { 59 - $xactions = $editor->applyTransactions($file, $xactions); 60 - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { 61 - // just do nothing, though unclear why you're sending a blank email 62 - return true; 63 - } 64 - 65 - $head_xaction = head($xactions); 66 - return $head_xaction->getID(); 59 + $editor->applyTransactions($file, $xactions); 67 60 } 68 61 69 62 }
+15 -22
src/applications/legalpad/mail/LegalpadReplyHandler.php
··· 31 31 'id' => $mail->getID(), 32 32 )); 33 33 34 - 35 34 $xactions = array(); 36 - $command = $body_data['command']; 37 35 38 - switch ($command) { 39 - case 'unsubscribe': 40 - $xaction = id(new LegalpadTransaction()) 41 - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 42 - ->setNewValue(array('-' => array($actor->getPHID()))); 43 - $xactions[] = $xaction; 44 - break; 36 + $commands = $body_data['commands']; 37 + foreach ($commands as $command) { 38 + switch (head($command)) { 39 + case 'unsubscribe': 40 + $xaction = id(new LegalpadTransaction()) 41 + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 42 + ->setNewValue(array('-' => array($actor->getPHID()))); 43 + $xactions[] = $xaction; 44 + break; 45 + } 45 46 } 46 47 47 48 $xactions[] = id(new LegalpadTransaction()) 48 49 ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 49 50 ->attachComment( 50 51 id(new LegalpadTransactionComment()) 51 - ->setDocumentID($document->getID()) 52 - ->setLineNumber(0) 53 - ->setLineLength(0) 54 - ->setContent($body)); 52 + ->setDocumentID($document->getID()) 53 + ->setLineNumber(0) 54 + ->setLineLength(0) 55 + ->setContent($body)); 55 56 56 57 $editor = id(new LegalpadDocumentEditor()) 57 58 ->setActor($actor) ··· 59 60 ->setContinueOnNoEffect(true) 60 61 ->setIsPreview(false); 61 62 62 - try { 63 - $xactions = $editor->applyTransactions($document, $xactions); 64 - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { 65 - // just do nothing, though unclear why you're sending a blank email 66 - return true; 67 - } 68 - 69 - $head_xaction = head($xactions); 70 - return $head_xaction->getID(); 63 + $editor->applyTransactions($document, $xactions); 71 64 } 72 65 73 66 }
+10 -2
src/applications/maniphest/mail/ManiphestReplyHandler.php
··· 63 63 64 64 } else { 65 65 66 - $command = $body_data['command']; 67 - $command_value = $body_data['command_value']; 66 + $commands = $body_data['commands']; 67 + 68 + // TODO: Support multiple commands. 69 + if ($commands) { 70 + $command_argv = head($commands); 71 + } else { 72 + $command_argv = array(); 73 + } 74 + $command = idx($command_argv, 0); 75 + $command_value = idx($command_argv, 1); 68 76 69 77 $ttype = PhabricatorTransactions::TYPE_COMMENT; 70 78 $new_value = null;
+49 -19
src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
··· 16 16 * please, take this task I took; its hard 17 17 * 18 18 * This function parses such an email body and returns a dictionary 19 - * containing a clean body text (e.g. "taking this task"), a $command 20 - * (e.g. !claim, !assign) and a $command_value (e.g. "epriestley" in the 21 - * !assign example.) 19 + * containing a clean body text (e.g. "taking this task"), and a list of 20 + * commands. For example, this body above might parse as: 21 + * 22 + * array( 23 + * 'body' => 'please, take this task I took; its hard', 24 + * 'commands' => array( 25 + * array('assign', 'epriestley'), 26 + * ), 27 + * ) 22 28 * 23 - * @return dict 29 + * @param string Raw mail text body. 30 + * @return dict Parsed body. 24 31 */ 25 32 public function parseBody($body) { 26 33 $body = $this->stripTextBody($body); 27 - $lines = explode("\n", trim($body)); 28 - $first_line = head($lines); 29 34 30 - $command = null; 31 - $command_value = null; 32 - $matches = null; 33 - if (preg_match('/^!(\w+)\s*(\S+)?/', $first_line, $matches)) { 34 - $lines = array_slice($lines, 1); 35 - $body = implode("\n", $lines); 36 - $body = trim($body); 35 + $commands = array(); 37 36 38 - $command = $matches[1]; 39 - $command_value = idx($matches, 2); 40 - } 37 + $lines = phutil_split_lines($body, $retain_endings = true); 38 + 39 + // We'll match commands at the beginning and end of the mail, but not 40 + // in the middle of the mail body. 41 + list($top_commands, $lines) = $this->stripCommands($lines); 42 + list($end_commands, $lines) = $this->stripCommands(array_reverse($lines)); 43 + $lines = array_reverse($lines); 44 + $commands = array_merge($top_commands, array_reverse($end_commands)); 45 + 46 + $lines = rtrim(implode('', $lines)); 41 47 42 48 return array( 43 - 'body' => $body, 44 - 'command' => $command, 45 - 'command_value' => $command_value, 49 + 'body' => $lines, 50 + 'commands' => $commands, 46 51 ); 52 + } 53 + 54 + private function stripCommands(array $lines) { 55 + $saw_command = false; 56 + $commands = array(); 57 + foreach ($lines as $key => $line) { 58 + if (!strlen(trim($line)) && $saw_command) { 59 + unset($lines[$key]); 60 + continue; 61 + } 62 + 63 + $matches = null; 64 + if (!preg_match('/^\s*!(\w+.*$)/', $line, $matches)) { 65 + break; 66 + } 67 + 68 + $arg_str = $matches[1]; 69 + $argv = preg_split('/[,\s]+/', trim($arg_str)); 70 + $commands[] = $argv; 71 + unset($lines[$key]); 72 + 73 + $saw_command = true; 74 + } 75 + 76 + return array($commands, $lines); 47 77 } 48 78 49 79 public function stripTextBody($body) {
+68 -4
src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
··· 18 18 $parser = new PhabricatorMetaMTAEmailBodyParser(); 19 19 $body_data = $parser->parseBody($body); 20 20 $this->assertEqual('OKAY', $body_data['body']); 21 - $this->assertEqual('whatevs', $body_data['command']); 22 - $this->assertEqual('dude', $body_data['command_value']); 21 + $this->assertEqual( 22 + array( 23 + array('whatevs', 'dude'), 24 + ), 25 + $body_data['commands']); 23 26 } 27 + 24 28 $bodies = $this->getEmailBodiesWithPartialCommands(); 25 29 foreach ($bodies as $body) { 26 30 $parser = new PhabricatorMetaMTAEmailBodyParser(); 27 31 $body_data = $parser->parseBody($body); 28 32 $this->assertEqual('OKAY', $body_data['body']); 29 - $this->assertEqual('whatevs', $body_data['command']); 30 - $this->assertEqual(null, $body_data['command_value']); 33 + $this->assertEqual( 34 + array( 35 + array('whatevs'), 36 + ), 37 + $body_data['commands']); 38 + } 39 + 40 + $bodies = $this->getEmailBodiesWithMultipleCommands(); 41 + foreach ($bodies as $body) { 42 + $parser = new PhabricatorMetaMTAEmailBodyParser(); 43 + $body_data = $parser->parseBody($body); 44 + $this->assertEqual("preface\n\nOKAY", $body_data['body']); 45 + $this->assertEqual( 46 + array( 47 + array('top1'), 48 + array('top2'), 49 + ), 50 + $body_data['commands']); 51 + } 52 + 53 + $bodies = $this->getEmailBodiesWithSplitCommands(); 54 + foreach ($bodies as $body) { 55 + $parser = new PhabricatorMetaMTAEmailBodyParser(); 56 + $body_data = $parser->parseBody($body); 57 + $this->assertEqual('OKAY', $body_data['body']); 58 + $this->assertEqual( 59 + array( 60 + array('cmd1'), 61 + array('cmd2'), 62 + ), 63 + $body_data['commands']); 64 + } 65 + 66 + $bodies = $this->getEmailBodiesWithMiddleCommands(); 67 + foreach ($bodies as $body) { 68 + $parser = new PhabricatorMetaMTAEmailBodyParser(); 69 + $body_data = $parser->parseBody($body); 70 + $this->assertEqual("HEAD\n!cmd2\nTAIL", $body_data['body']); 31 71 } 32 72 } 33 73 ··· 63 103 return $with_commands; 64 104 } 65 105 106 + private function getEmailBodiesWithMultipleCommands() { 107 + $bodies = $this->getEmailBodies(); 108 + $with_commands = array(); 109 + foreach ($bodies as $body) { 110 + $with_commands[] = "!top1\n\n!top2\n\npreface\n\n".$body; 111 + } 112 + return $with_commands; 113 + } 114 + 115 + private function getEmailBodiesWithSplitCommands() { 116 + $with_split = array(); 117 + $with_split[] = "!cmd1\n!cmd2\nOKAY"; 118 + $with_split[] = "!cmd1\nOKAY\n!cmd2"; 119 + $with_split[] = "OKAY\n!cmd1\n!cmd2"; 120 + return $with_split; 121 + } 122 + 123 + private function getEmailBodiesWithMiddleCommands() { 124 + $with_middle = array(); 125 + $with_middle[] = "!cmd1\nHEAD\n!cmd2\nTAIL\n!cmd3"; 126 + $with_middle[] = "!cmd1\nHEAD\n!cmd2\nTAIL"; 127 + $with_middle[] = "HEAD\n!cmd2\nTAIL\n!cmd3"; 128 + return $with_middle; 129 + } 66 130 67 131 private function getEmailBodies() { 68 132 $trailing_space = ' ';
+11 -17
src/applications/paste/mail/PasteReplyHandler.php
··· 35 35 $first_line = head($lines); 36 36 37 37 $xactions = array(); 38 - $command = $body_data['command']; 39 38 40 - switch ($command) { 41 - case 'unsubscribe': 42 - $xaction = id(new PhabricatorPasteTransaction()) 43 - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 44 - ->setNewValue(array('-' => array($actor->getPHID()))); 45 - $xactions[] = $xaction; 46 - break; 39 + $commands = $body_data['commands']; 40 + foreach ($commands as $command) { 41 + switch (head($command)) { 42 + case 'unsubscribe': 43 + $xaction = id(new PhabricatorPasteTransaction()) 44 + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 45 + ->setNewValue(array('-' => array($actor->getPHID()))); 46 + $xactions[] = $xaction; 47 + break; 48 + } 47 49 } 48 50 49 51 $xactions[] = id(new PhabricatorPasteTransaction()) ··· 58 60 ->setContinueOnNoEffect(true) 59 61 ->setIsPreview(false); 60 62 61 - try { 62 - $xactions = $editor->applyTransactions($paste, $xactions); 63 - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { 64 - // just do nothing, though unclear why you're sending a blank email 65 - return true; 66 - } 67 - 68 - $head_xaction = head($xactions); 69 - return $head_xaction->getID(); 63 + $editor->applyTransactions($paste, $xactions); 70 64 } 71 65 72 66 }
+2 -2
src/applications/releeph/mail/ReleephRequestMailReceiver.php
··· 8 8 } 9 9 10 10 protected function getObjectPattern() { 11 - return 'RQ[1-9]\d*'; 11 + return 'Y[1-9]\d*'; 12 12 } 13 13 14 14 protected function loadObject($pattern, PhabricatorUser $viewer) { 15 - $id = (int)substr($pattern, 2); 15 + $id = (int)substr($pattern, 1); 16 16 17 17 return id(new ReleephRequestQuery()) 18 18 ->setViewer($viewer)
+8 -9
src/applications/releeph/mail/ReleephRequestReplyHandler.php
··· 10 10 11 11 public function getPrivateReplyHandlerEmailAddress( 12 12 PhabricatorObjectHandle $handle) { 13 - return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'RERQ'); 13 + return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'Y'); 14 14 } 15 15 16 16 public function getPublicReplyHandlerEmailAddress() { 17 - return $this->getDefaultPublicReplyHandlerEmailAddress('RERQ'); 17 + return $this->getDefaultPublicReplyHandlerEmailAddress('Y'); 18 18 } 19 19 20 20 protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { ··· 26 26 array( 27 27 'id' => $mail->getID(), 28 28 )); 29 - 30 - $editor = id(new ReleephRequestTransactionalEditor()) 31 - ->setActor($user) 32 - ->setContentSource($content_source) 33 - ->setParentMessageID($mail->getMessageID()); 34 29 35 30 $body = $mail->getCleanTextBody(); 36 31 ··· 39 34 ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 40 35 ->attachComment($body); 41 36 42 - $editor->applyTransactions($rq, $xactions); 37 + $editor = id(new ReleephRequestTransactionalEditor()) 38 + ->setActor($user) 39 + ->setContentSource($content_source) 40 + ->setContinueOnNoEffect(true) 41 + ->setParentMessageID($mail->getMessageID()); 43 42 44 - return $rq; 43 + $editor->applyTransactions($rq, $xactions); 45 44 } 46 45 47 46 }