Paste - add support for email replies and subscribers
Summary: Email replies and subscribers seem to go hand in hand so deploy both at once. Test Plan: played around with bin/mail. Verified replies posted comments on the paste. Reviewers: epriestley Reviewed By: epriestley CC: aran, Korvin Maniphest Tasks: T3650 Differential Revision: https://secure.phabricator.com/D6682
This commit is contained in:
		
							
								
								
									
										15
									
								
								resources/sql/patches/20130805.pasteedges.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								resources/sql/patches/20130805.pasteedges.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | CREATE TABLE {$NAMESPACE}_pastebin.edge ( | ||||||
|  |   src VARCHAR(64) NOT NULL COLLATE utf8_bin, | ||||||
|  |   type VARCHAR(64) NOT NULL COLLATE utf8_bin, | ||||||
|  |   dst VARCHAR(64) NOT NULL COLLATE utf8_bin, | ||||||
|  |   dateCreated INT UNSIGNED NOT NULL, | ||||||
|  |   seq INT UNSIGNED NOT NULL, | ||||||
|  |   dataID INT UNSIGNED, | ||||||
|  |   PRIMARY KEY (src, type, dst), | ||||||
|  |   KEY (src, type, dateCreated, seq) | ||||||
|  | ) ENGINE=InnoDB, COLLATE utf8_general_ci; | ||||||
|  |  | ||||||
|  | CREATE TABLE {$NAMESPACE}_pastebin.edgedata ( | ||||||
|  |   id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, | ||||||
|  |   data LONGTEXT NOT NULL COLLATE utf8_bin | ||||||
|  | ) ENGINE=InnoDB, COLLATE utf8_general_ci; | ||||||
							
								
								
									
										2
									
								
								resources/sql/patches/20130805.pastemailkey.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								resources/sql/patches/20130805.pastemailkey.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE {$NAMESPACE}_pastebin.pastebin_paste | ||||||
|  |   ADD COLUMN `mailKey` varchar(20) NOT NULL; | ||||||
							
								
								
									
										27
									
								
								resources/sql/patches/20130805.pastemailkeypop.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								resources/sql/patches/20130805.pastemailkeypop.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | echo "Populating pastes with mail keys...\n"; | ||||||
|  |  | ||||||
|  | $table = new PhabricatorPaste(); | ||||||
|  | $table->openTransaction(); | ||||||
|  | $conn_w = $table->establishConnection('w'); | ||||||
|  |  | ||||||
|  | foreach (new LiskMigrationIterator($table) as $paste) { | ||||||
|  |   $id = $paste->getID(); | ||||||
|  |  | ||||||
|  |   echo "P{$id}: "; | ||||||
|  |   if (!$paste->getMailKey()) { | ||||||
|  |     queryfx( | ||||||
|  |       $conn_w, | ||||||
|  |       'UPDATE %T SET mailKey = %s WHERE id = %d', | ||||||
|  |       $paste->getTableName(), | ||||||
|  |       Filesystem::readRandomCharacters(20), | ||||||
|  |       $id); | ||||||
|  |     echo("Generated Key\n"); | ||||||
|  |   } else { | ||||||
|  |     echo "-\n"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $table->saveTransaction(); | ||||||
|  | echo "Done.\n"; | ||||||
| @@ -759,6 +759,8 @@ phutil_register_library_map(array( | |||||||
|     'PackageModifyMail' => 'applications/owners/mail/PackageModifyMail.php', |     'PackageModifyMail' => 'applications/owners/mail/PackageModifyMail.php', | ||||||
|     'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php', |     'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php', | ||||||
|     'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', |     'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', | ||||||
|  |     'PasteMockMailReceiver' => 'applications/paste/mail/PasteMockMailReceiver.php', | ||||||
|  |     'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php', | ||||||
|     'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', |     'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', | ||||||
|     'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php', |     'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php', | ||||||
|     'PhabricatorAccessLog' => 'infrastructure/PhabricatorAccessLog.php', |     'PhabricatorAccessLog' => 'infrastructure/PhabricatorAccessLog.php', | ||||||
| @@ -2773,6 +2775,8 @@ phutil_register_library_map(array( | |||||||
|     'PackageModifyMail' => 'PackageMail', |     'PackageModifyMail' => 'PackageMail', | ||||||
|     'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', |     'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', | ||||||
|     'PasteEmbedView' => 'AphrontView', |     'PasteEmbedView' => 'AphrontView', | ||||||
|  |     'PasteMockMailReceiver' => 'PhabricatorObjectMailReceiver', | ||||||
|  |     'PasteReplyHandler' => 'PhabricatorMailReplyHandler', | ||||||
|     'Phabricator404Controller' => 'PhabricatorController', |     'Phabricator404Controller' => 'PhabricatorController', | ||||||
|     'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', |     'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', | ||||||
|     'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', |     'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', | ||||||
| @@ -3417,8 +3421,9 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorPaste' => |     'PhabricatorPaste' => | ||||||
|     array( |     array( | ||||||
|       0 => 'PhabricatorPasteDAO', |       0 => 'PhabricatorPasteDAO', | ||||||
|       1 => 'PhabricatorTokenReceiverInterface', |       1 => 'PhabricatorSubscribableInterface', | ||||||
|       2 => 'PhabricatorPolicyInterface', |       2 => 'PhabricatorTokenReceiverInterface', | ||||||
|  |       3 => 'PhabricatorPolicyInterface', | ||||||
|     ), |     ), | ||||||
|     'PhabricatorPasteCommentController' => 'PhabricatorPasteController', |     'PhabricatorPasteCommentController' => 'PhabricatorPasteController', | ||||||
|     'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', |     'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', | ||||||
|   | |||||||
| @@ -20,7 +20,12 @@ final class PhabricatorPasteConfigOptions | |||||||
|         'metamta.paste.public-create-email', |         'metamta.paste.public-create-email', | ||||||
|         'string', |         'string', | ||||||
|         null) |         null) | ||||||
|         ->setDescription(pht('Allow creating pastes via email.')) |         ->setDescription(pht('Allow creating pastes via email.')), | ||||||
|  |       $this->newOption( | ||||||
|  |         'metamta.paste.subject-prefix', | ||||||
|  |         'string', | ||||||
|  |         '[Paste]') | ||||||
|  |         ->setDescription(pht('Subject prefix for paste email.')) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -101,7 +101,11 @@ final class PhabricatorPasteEditor | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function supportsMail() { |   protected function supportsMail() { | ||||||
|     return false; |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function getMailSubjectPrefix() { | ||||||
|  |     return PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function getMailTo(PhabricatorLiskDAO $object) { |   protected function getMailTo(PhabricatorLiskDAO $object) { | ||||||
| @@ -111,8 +115,18 @@ final class PhabricatorPasteEditor | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function getMailCC(PhabricatorLiskDAO $object) { |   protected function buildReplyHandler(PhabricatorLiskDAO $object) { | ||||||
|     return array(); |     return id(new PasteReplyHandler()) | ||||||
|  |       ->setMailReceiver($object); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function buildMailTemplate(PhabricatorLiskDAO $object) { | ||||||
|  |     $id = $object->getID(); | ||||||
|  |     $name = $object->getTitle(); | ||||||
|  |  | ||||||
|  |     return id(new PhabricatorMetaMTAMail()) | ||||||
|  |       ->setSubject("P{$id}: {$name}") | ||||||
|  |       ->addHeader('Thread-Topic', "P{$id}"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function supportsFeed() { |   protected function supportsFeed() { | ||||||
|   | |||||||
| @@ -66,6 +66,8 @@ final class PasteCreateMailReceiver | |||||||
|  |  | ||||||
|     $mail->setRelatedPHID($paste->getPHID()); |     $mail->setRelatedPHID($paste->getPHID()); | ||||||
|  |  | ||||||
|  |     $subject_prefix = | ||||||
|  |       PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); | ||||||
|     $subject = pht('You successfully created a paste.'); |     $subject = pht('You successfully created a paste.'); | ||||||
|     $paste_uri = PhabricatorEnv::getProductionURI($paste->getURI()); |     $paste_uri = PhabricatorEnv::getProductionURI($paste->getURI()); | ||||||
|     $body = new PhabricatorMetaMTAMailBody(); |     $body = new PhabricatorMetaMTAMailBody(); | ||||||
| @@ -74,7 +76,8 @@ final class PasteCreateMailReceiver | |||||||
|  |  | ||||||
|     id(new PhabricatorMetaMTAMail()) |     id(new PhabricatorMetaMTAMail()) | ||||||
|       ->addTos(array($sender->getPHID())) |       ->addTos(array($sender->getPHID())) | ||||||
|       ->setSubject('[Paste] '.$subject) |       ->setSubject($subject) | ||||||
|  |       ->setSubjectPrefix($subject_prefix) | ||||||
|       ->setFrom($sender->getPHID()) |       ->setFrom($sender->getPHID()) | ||||||
|       ->setRelatedPHID($paste->getPHID()) |       ->setRelatedPHID($paste->getPHID()) | ||||||
|       ->setBody($body->render()) |       ->setBody($body->render()) | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								src/applications/paste/mail/PasteMockMailReceiver.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/applications/paste/mail/PasteMockMailReceiver.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @group paste | ||||||
|  |  */ | ||||||
|  | final class PasteMockMailReceiver extends PhabricatorObjectMailReceiver { | ||||||
|  |  | ||||||
|  |   public function isEnabled() { | ||||||
|  |     $app_class = 'PhabricatorApplicationPaste'; | ||||||
|  |     return PhabricatorApplication::isClassInstalled($app_class); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function getObjectPattern() { | ||||||
|  |     return 'P[1-9]\d*'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function loadObject($pattern, PhabricatorUser $viewer) { | ||||||
|  |     $id = (int)trim($pattern, 'P'); | ||||||
|  |  | ||||||
|  |     return id(new PhabricatorPasteQuery()) | ||||||
|  |       ->setViewer($viewer) | ||||||
|  |       ->withIDs(array($id)) | ||||||
|  |       ->executeOne(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function processReceivedObjectMail( | ||||||
|  |     PhabricatorMetaMTAReceivedMail $mail, | ||||||
|  |     PhabricatorLiskDAO $object, | ||||||
|  |     PhabricatorUser $sender) { | ||||||
|  |  | ||||||
|  |     $handler = id(new PasteReplyHandler()) | ||||||
|  |       ->setMailReceiver($object); | ||||||
|  |  | ||||||
|  |     $handler->setActor($sender); | ||||||
|  |     $handler->setExcludeMailRecipientPHIDs( | ||||||
|  |       $mail->loadExcludeMailRecipientPHIDs()); | ||||||
|  |     $handler->processEmail($mail); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										92
									
								
								src/applications/paste/mail/PasteReplyHandler.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/applications/paste/mail/PasteReplyHandler.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @group paste | ||||||
|  |  */ | ||||||
|  | final class PasteReplyHandler extends PhabricatorMailReplyHandler { | ||||||
|  |  | ||||||
|  |   public function validateMailReceiver($mail_receiver) { | ||||||
|  |     if (!($mail_receiver instanceof PhabricatorPaste)) { | ||||||
|  |       throw new Exception('Mail receiver is not a PhabricatorPaste.'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getPrivateReplyHandlerEmailAddress( | ||||||
|  |     PhabricatorObjectHandle $handle) { | ||||||
|  |     return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'P'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getPublicReplyHandlerEmailAddress() { | ||||||
|  |     return $this->getDefaultPublicReplyHandlerEmailAddress('P'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getReplyHandlerInstructions() { | ||||||
|  |     if ($this->supportsReplies()) { | ||||||
|  |       return pht('Reply to comment or !unsubscribe.'); | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { | ||||||
|  |     $actor = $this->getActor(); | ||||||
|  |     $paste = $this->getMailReceiver(); | ||||||
|  |  | ||||||
|  |     $body = $mail->getCleanTextBody(); | ||||||
|  |     $body = trim($body); | ||||||
|  |     $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments()); | ||||||
|  |  | ||||||
|  |     $content_source = PhabricatorContentSource::newForSource( | ||||||
|  |       PhabricatorContentSource::SOURCE_EMAIL, | ||||||
|  |       array( | ||||||
|  |         'id' => $mail->getID(), | ||||||
|  |       )); | ||||||
|  |  | ||||||
|  |     $lines = explode("\n", trim($body)); | ||||||
|  |     $first_line = head($lines); | ||||||
|  |  | ||||||
|  |     $xactions = array(); | ||||||
|  |     $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]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     switch ($command) { | ||||||
|  |       case 'unsubscribe': | ||||||
|  |         $xaction = id(new PhabricatorPasteTransaction()) | ||||||
|  |           ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) | ||||||
|  |           ->setNewValue(array('-' => array($actor->getPHID()))); | ||||||
|  |         $xactions[] = $xaction; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $xactions[] = id(new PhabricatorPasteTransaction()) | ||||||
|  |       ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) | ||||||
|  |       ->attachComment( | ||||||
|  |        id(new PhabricatorPasteTransactionComment()) | ||||||
|  |         ->setContent($body)); | ||||||
|  |  | ||||||
|  |     $editor = id(new PhabricatorPasteEditor()) | ||||||
|  |       ->setActor($actor) | ||||||
|  |       ->setContentSource($content_source) | ||||||
|  |       ->setContinueOnNoEffect(true) | ||||||
|  |       ->setIsPreview(false); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       $xactions = $editor->applyTransactions($paste, $xactions); | ||||||
|  |     } catch (PhabricatorApplicationTransactionNoEffectException $ex) { | ||||||
|  |       // just do nothing, though unclear why you're sending a blank email | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $head_xaction = head($xactions); | ||||||
|  |     return $head_xaction->getID(); | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -4,7 +4,10 @@ | |||||||
|  * @group paste |  * @group paste | ||||||
|  */ |  */ | ||||||
| final class PhabricatorPaste extends PhabricatorPasteDAO | final class PhabricatorPaste extends PhabricatorPasteDAO | ||||||
|   implements PhabricatorTokenReceiverInterface, PhabricatorPolicyInterface { |   implements | ||||||
|  |     PhabricatorSubscribableInterface, | ||||||
|  |     PhabricatorTokenReceiverInterface, | ||||||
|  |     PhabricatorPolicyInterface { | ||||||
|  |  | ||||||
|   protected $phid; |   protected $phid; | ||||||
|   protected $title; |   protected $title; | ||||||
| @@ -13,6 +16,7 @@ final class PhabricatorPaste extends PhabricatorPasteDAO | |||||||
|   protected $language; |   protected $language; | ||||||
|   protected $parentPHID; |   protected $parentPHID; | ||||||
|   protected $viewPolicy; |   protected $viewPolicy; | ||||||
|  |   protected $mailKey; | ||||||
|  |  | ||||||
|   private $content; |   private $content; | ||||||
|   private $rawContent; |   private $rawContent; | ||||||
| @@ -32,6 +36,13 @@ final class PhabricatorPaste extends PhabricatorPasteDAO | |||||||
|       PhabricatorPastePHIDTypePaste::TYPECONST); |       PhabricatorPastePHIDTypePaste::TYPECONST); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function save() { | ||||||
|  |     if (!$this->getMailKey()) { | ||||||
|  |       $this->setMailKey(Filesystem::readRandomCharacters(20)); | ||||||
|  |     } | ||||||
|  |     return parent::save(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getCapabilities() { |   public function getCapabilities() { | ||||||
|     return array( |     return array( | ||||||
|       PhabricatorPolicyCapability::CAN_VIEW, |       PhabricatorPolicyCapability::CAN_VIEW, | ||||||
| @@ -82,6 +93,14 @@ final class PhabricatorPaste extends PhabricatorPasteDAO | |||||||
|     return $this; |     return $this; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | /* -(  PhabricatorSubscribableInterface Implementation  )-------------------- */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public function isAutomaticallySubscribed($phid) { | ||||||
|  |     return ($this->authorPHID == $phid); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */ | /* -(  PhabricatorTokenReceiverInterface  )---------------------------------- */ | ||||||
|  |  | ||||||
|   public function getUsersToNotifyOfTokenGiven() { |   public function getUsersToNotifyOfTokenGiven() { | ||||||
|   | |||||||
| @@ -1507,6 +1507,18 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { | |||||||
|         'type' => 'php', |         'type' => 'php', | ||||||
|         'name' => $this->getPatchPath('20130801.pastexactions.php'), |         'name' => $this->getPatchPath('20130801.pastexactions.php'), | ||||||
|       ), |       ), | ||||||
|  |       '20130805.pastemailkey.sql' => array( | ||||||
|  |         'type' => 'sql', | ||||||
|  |         'name' => $this->getPatchPath('20130805.pastemailkey.sql'), | ||||||
|  |       ), | ||||||
|  |       '20130805.pasteedges.sql' => array( | ||||||
|  |         'type' => 'sql', | ||||||
|  |         'name' => $this->getPatchPath('20130805.pasteedges.sql'), | ||||||
|  |       ), | ||||||
|  |       '20130805.pastemailkeypop.php' => array( | ||||||
|  |         'type' => 'php', | ||||||
|  |         'name' => $this->getPatchPath('20130805.pastemailkeypop.php'), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Bob Trahan
					Bob Trahan