@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

Legalpad V0.2 - add mail integration

Summary:
Supports !unsubscribe and commenting on replies. Subscribers get mailed something reasonable. Fixes T3480.

Sneaks in /LX/ support. In the near future I want to have that /LX/ be a clean "signature" page sans all the edit actions and other fluff... Will resolve this as part of T3481.

Test Plan: used the metamta console to add comments and unsubscribe. added a phlog() inside mail code to verify mail bodies looked okay.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Korvin

Maniphest Tasks: T3480

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

+272 -2
+25
resources/sql/patches/legalpad-mailkey-populate.php
··· 1 + <?php 2 + 3 + echo "Populating Legalpad Documents with mail keys...\n"; 4 + $table = new LegalpadDocument(); 5 + $table->openTransaction(); 6 + 7 + foreach (new LiskMigrationIterator($table) as $document) { 8 + $id = $document->getID(); 9 + 10 + echo "Document {$id}: "; 11 + if (!$document->getMailKey()) { 12 + queryfx( 13 + $document->establishConnection('w'), 14 + 'UPDATE %T SET mailKey = %s WHERE id = %d', 15 + $document->getTableName(), 16 + Filesystem::readRandomCharacters(20), 17 + $id); 18 + echo "Generated Key\n"; 19 + } else { 20 + echo "-\n"; 21 + } 22 + } 23 + 24 + $table->saveTransaction(); 25 + echo "Done.\n";
+2
resources/sql/patches/legalpad-mailkey.sql
··· 1 + ALTER TABLE `{$NAMESPACE}_legalpad`.legalpad_document 2 + ADD mailKey VARCHAR(20) NOT NULL COLLATE utf8_bin;
+6
src/__phutil_library_map__.php
··· 644 644 'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php', 645 645 'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php', 646 646 'LegalpadDocumentViewController' => 'applications/legalpad/controller/LegalpadDocumentViewController.php', 647 + 'LegalpadMockMailReceiver' => 'applications/legalpad/mail/LegalpadMockMailReceiver.php', 648 + 'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php', 647 649 'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php', 648 650 'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php', 649 651 'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php', ··· 1169 1171 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 1170 1172 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', 1171 1173 'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php', 1174 + 'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php', 1172 1175 'PhabricatorLintEngine' => 'infrastructure/lint/PhabricatorLintEngine.php', 1173 1176 'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php', 1174 1177 'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php', ··· 2572 2575 'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine', 2573 2576 'LegalpadDocumentSignature' => 'LegalpadDAO', 2574 2577 'LegalpadDocumentViewController' => 'LegalpadController', 2578 + 'LegalpadMockMailReceiver' => 'PhabricatorObjectMailReceiver', 2579 + 'LegalpadReplyHandler' => 'PhabricatorMailReplyHandler', 2575 2580 'LegalpadTransaction' => 'PhabricatorApplicationTransaction', 2576 2581 'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment', 2577 2582 'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery', ··· 3120 3125 'PhabricatorInlineSummaryView' => 'AphrontView', 3121 3126 'PhabricatorJavelinLinter' => 'ArcanistLinter', 3122 3127 'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache', 3128 + 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions', 3123 3129 'PhabricatorLintEngine' => 'PhutilLintEngine', 3124 3130 'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow', 3125 3131 'PhabricatorLipsumManagementWorkflow' => 'PhutilArgumentWorkflow',
+1
src/applications/legalpad/application/PhabricatorApplicationLegalpad.php
··· 40 40 41 41 public function getRoutes() { 42 42 return array( 43 + '/L(?P<id>\d+)/' => 'LegalpadDocumentViewController', 43 44 '/legalpad/' => array( 44 45 '' => 'LegalpadDocumentListController', 45 46 '(query/(?P<queryKey>[^/]+)/)?' => 'LegalpadDocumentListController',
+27
src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php
··· 1 + <?php 2 + 3 + /** 4 + * @group legalpad 5 + */ 6 + final class PhabricatorLegalpadConfigOptions 7 + extends PhabricatorApplicationConfigOptions { 8 + 9 + public function getName() { 10 + return pht('Legalpad'); 11 + } 12 + 13 + public function getDescription() { 14 + return pht('Configure Legalpad.'); 15 + } 16 + 17 + public function getOptions() { 18 + return array( 19 + $this->newOption( 20 + 'metamta.legalpad.subject-prefix', 21 + 'string', 22 + '[Legalpad]') 23 + ->setDescription(pht('Subject prefix for Legalpad email.')) 24 + ); 25 + } 26 + 27 + }
+1
src/applications/legalpad/controller/LegalpadDocumentCommentController.php
··· 22 22 $document = id(new LegalpadDocumentQuery()) 23 23 ->setViewer($user) 24 24 ->withIDs(array($this->id)) 25 + ->needDocumentBodies(true) 25 26 ->executeOne(); 26 27 27 28 if (!$document) {
+56 -1
src/applications/legalpad/editor/LegalpadDocumentEditor.php
··· 111 111 return parent::mergeTransactions($u, $v); 112 112 } 113 113 114 + /* -( Sending Mail )------------------------------------------------------- */ 115 + 114 116 protected function supportsMail() { 115 - return false; 117 + return true; 118 + } 119 + 120 + protected function buildReplyHandler(PhabricatorLiskDAO $object) { 121 + return id(new LegalpadReplyHandler()) 122 + ->setMailReceiver($object); 123 + } 124 + 125 + protected function buildMailTemplate(PhabricatorLiskDAO $object) { 126 + $id = $object->getID(); 127 + $phid = $object->getPHID(); 128 + $title = $object->getDocumentBody()->getTitle(); 129 + 130 + return id(new PhabricatorMetaMTAMail()) 131 + ->setSubject("L{$id}: {$title}") 132 + ->addHeader('Thread-Topic', "L{$id}: {$phid}"); 133 + } 134 + 135 + protected function getMailTo(PhabricatorLiskDAO $object) { 136 + return array( 137 + $object->getCreatorPHID(), 138 + $this->requireActor()->getPHID(), 139 + ); 140 + } 141 + 142 + protected function shouldImplyCC( 143 + PhabricatorLiskDAO $object, 144 + PhabricatorApplicationTransaction $xaction) { 145 + 146 + switch ($xaction->getTransactionType()) { 147 + case LegalpadTransactionType::TYPE_TEXT: 148 + case LegalpadTransactionType::TYPE_TITLE: 149 + return true; 150 + } 151 + 152 + return parent::shouldImplyCC($object, $xaction); 116 153 } 154 + 155 + protected function buildMailBody( 156 + PhabricatorLiskDAO $object, 157 + array $xactions) { 158 + 159 + $body = parent::buildMailBody($object, $xactions); 160 + 161 + $body->addTextSection( 162 + pht('DOCUMENT DETAIL'), 163 + PhabricatorEnv::getProductionURI('/L'.$object->getID())); 164 + 165 + return $body; 166 + } 167 + 168 + protected function getMailSubjectPrefix() { 169 + return PhabricatorEnv::getEnvConfig('metamta.legalpad.subject-prefix'); 170 + } 171 + 117 172 118 173 protected function supportsFeed() { 119 174 return false;
+38
src/applications/legalpad/mail/LegalpadMockMailReceiver.php
··· 1 + <?php 2 + 3 + final class LegalpadMockMailReceiver extends PhabricatorObjectMailReceiver { 4 + 5 + public function isEnabled() { 6 + $app_class = 'PhabricatorApplicationLegalpad'; 7 + return PhabricatorApplication::isClassInstalled($app_class); 8 + } 9 + 10 + protected function getObjectPattern() { 11 + return 'L[1-9]\d*'; 12 + } 13 + 14 + protected function loadObject($pattern, PhabricatorUser $viewer) { 15 + $id = (int)trim($pattern, 'L'); 16 + 17 + return id(new LegalpadDocumentQuery()) 18 + ->setViewer($viewer) 19 + ->withIDs(array($id)) 20 + ->needDocumentBodies(true) 21 + ->executeOne(); 22 + } 23 + 24 + protected function processReceivedObjectMail( 25 + PhabricatorMetaMTAReceivedMail $mail, 26 + PhabricatorLiskDAO $object, 27 + PhabricatorUser $sender) { 28 + 29 + $handler = id(new LegalpadReplyHandler()) 30 + ->setMailReceiver($object) 31 + ->setActor($sender) 32 + ->setExcludeMailRecipientPHIDs( 33 + $mail->loadExcludeMailRecipientPHIDs()); 34 + 35 + return $handler->processEmail($mail); 36 + } 37 + 38 + }
+99
src/applications/legalpad/mail/LegalpadReplyHandler.php
··· 1 + <?php 2 + 3 + /** 4 + * @group legalpad 5 + */ 6 + final class LegalpadReplyHandler extends PhabricatorMailReplyHandler { 7 + 8 + public function validateMailReceiver($mail_receiver) { 9 + if (!($mail_receiver instanceof LegalpadDocument)) { 10 + throw new Exception("Mail receiver is not a LegalpadDocument!"); 11 + } 12 + } 13 + 14 + public function getPrivateReplyHandlerEmailAddress( 15 + PhabricatorObjectHandle $handle) { 16 + return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'L'); 17 + } 18 + 19 + public function getPublicReplyHandlerEmailAddress() { 20 + return $this->getDefaultPublicReplyHandlerEmailAddress('L'); 21 + } 22 + 23 + public function getReplyHandlerDomain() { 24 + return PhabricatorEnv::getEnvConfig( 25 + 'metamta.reply-handler-domain'); 26 + } 27 + 28 + public function getReplyHandlerInstructions() { 29 + if ($this->supportsReplies()) { 30 + return 'Reply to comment or !unsubscribe.'; 31 + } else { 32 + return null; 33 + } 34 + } 35 + 36 + protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { 37 + $actor = $this->getActor(); 38 + $document = $this->getMailReceiver(); 39 + 40 + $body = $mail->getCleanTextBody(); 41 + $body = trim($body); 42 + $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments()); 43 + 44 + $content_source = PhabricatorContentSource::newForSource( 45 + PhabricatorContentSource::SOURCE_EMAIL, 46 + array( 47 + 'id' => $mail->getID(), 48 + )); 49 + 50 + $lines = explode("\n", trim($body)); 51 + $first_line = head($lines); 52 + 53 + $xactions = array(); 54 + $command = null; 55 + $matches = null; 56 + if (preg_match('/^!(\w+)/', $first_line, $matches)) { 57 + $lines = array_slice($lines, 1); 58 + $body = implode("\n", $lines); 59 + $body = trim($body); 60 + 61 + $command = $matches[1]; 62 + } 63 + 64 + switch ($command) { 65 + case 'unsubscribe': 66 + $xaction = id(new LegalpadTransaction()) 67 + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 68 + ->setNewValue(array('-' => array($actor->getPHID()))); 69 + $xactions[] = $xaction; 70 + break; 71 + } 72 + 73 + $xactions[] = id(new LegalpadTransaction()) 74 + ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 75 + ->attachComment( 76 + id(new LegalpadTransactionComment()) 77 + ->setDocumentID($document->getID()) 78 + ->setLineNumber(0) 79 + ->setLineLength(0) 80 + ->setContent($body)); 81 + 82 + $editor = id(new LegalpadDocumentEditor()) 83 + ->setActor($actor) 84 + ->setContentSource($content_source) 85 + ->setContinueOnNoEffect(true) 86 + ->setIsPreview(false); 87 + 88 + try { 89 + $xactions = $editor->applyTransactions($document, $xactions); 90 + } catch (PhabricatorApplicationTransactionNoEffectException $ex) { 91 + // just do nothing, though unclear why you're sending a blank email 92 + return true; 93 + } 94 + 95 + $head_xaction = head($xactions); 96 + return $head_xaction->getID(); 97 + } 98 + 99 + }
+8
src/applications/legalpad/storage/LegalpadDocument.php
··· 15 15 protected $documentBodyPHID; 16 16 protected $viewPolicy; 17 17 protected $editPolicy; 18 + protected $mailKey; 18 19 19 20 private $documentBody; 20 21 private $contributors; ··· 56 57 public function attachContributors(array $contributors) { 57 58 $this->contributors = $contributors; 58 59 return $this; 60 + } 61 + 62 + public function save() { 63 + if (!$this->getMailKey()) { 64 + $this->setMailKey(Filesystem::readRandomCharacters(20)); 65 + } 66 + return parent::save(); 59 67 } 60 68 61 69 /* -( PhabricatorSubscribableInterface Implementation )-------------------- */
+1 -1
src/applications/phid/handle/PhabricatorObjectHandleData.php
··· 213 213 214 214 case PhabricatorPHIDConstants::PHID_TYPE_LEGD: 215 215 $legds = id(new LegalpadDocumentQuery()) 216 - ->needDocumentBody(true) 216 + ->needDocumentBodies(true) 217 217 ->withPHIDs($phids) 218 218 ->setViewer($this->viewer) 219 219 ->execute();
+8
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1414 1414 'type' => 'sql', 1415 1415 'name' => $this->getPatchPath('20130701.conduitlog.sql'), 1416 1416 ), 1417 + 'legalpad-mailkey.sql' => array( 1418 + 'type' => 'sql', 1419 + 'name' => $this->getPatchPath('legalpad-mailkey.sql'), 1420 + ), 1421 + 'legalpad-mailkey-populate.php' => array( 1422 + 'type' => 'php', 1423 + 'name' => $this->getPatchPath('legalpad-mailkey-populate.php'), 1424 + ), 1417 1425 ); 1418 1426 } 1419 1427 }