diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1958,6 +1958,7 @@ 'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php', 'PhabricatorApplicationEditor' => 'applications/meta/editor/PhabricatorApplicationEditor.php', 'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php', + 'PhabricatorApplicationObjectMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php', 'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php', 'PhabricatorApplicationPolicyChangeTransaction' => 'applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php', 'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php', @@ -2828,6 +2829,7 @@ 'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php', 'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php', 'PhabricatorEmailSelfActionsSetting' => 'applications/settings/setting/PhabricatorEmailSelfActionsSetting.php', + 'PhabricatorEmailStampsSetting' => 'applications/settings/setting/PhabricatorEmailStampsSetting.php', 'PhabricatorEmailTagsSetting' => 'applications/settings/setting/PhabricatorEmailTagsSetting.php', 'PhabricatorEmailVarySubjectsSetting' => 'applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php', 'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php', @@ -3174,6 +3176,7 @@ 'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php', 'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php', 'PhabricatorMailEmailSubjectHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailSubjectHeraldField.php', + 'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', @@ -3202,6 +3205,7 @@ 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', + 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php', @@ -4204,6 +4208,7 @@ 'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php', 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', 'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php', + 'PhabricatorStringMailStamp' => 'applications/metamta/stamp/PhabricatorStringMailStamp.php', 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', 'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', @@ -7269,6 +7274,7 @@ 'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView', 'PhabricatorApplicationEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController', + 'PhabricatorApplicationObjectMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationPolicyChangeTransaction' => 'PhabricatorApplicationTransactionType', 'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem', @@ -8276,6 +8282,7 @@ 'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailSelfActionsSetting' => 'PhabricatorSelectSetting', + 'PhabricatorEmailStampsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailTagsSetting' => 'PhabricatorInternalSetting', 'PhabricatorEmailVarySubjectsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailVerificationController' => 'PhabricatorAuthController', @@ -8662,6 +8669,7 @@ 'PhabricatorMailEmailHeraldField' => 'HeraldField', 'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorMailEmailSubjectHeraldField' => 'PhabricatorMailEmailHeraldField', + 'PhabricatorMailEngineExtension' => 'Phobject', 'PhabricatorMailImplementationAdapter' => 'Phobject', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', @@ -8690,6 +8698,7 @@ 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMainMenuBarExtension' => 'Phobject', @@ -9907,6 +9916,7 @@ 'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType', 'PhabricatorStringListEditField' => 'PhabricatorEditField', 'PhabricatorStringListExportField' => 'PhabricatorListExportField', + 'PhabricatorStringMailStamp' => 'PhabricatorMailStamp', 'PhabricatorStringSetting' => 'PhabricatorSetting', 'PhabricatorSubmitEditField' => 'PhabricatorEditField', 'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/metamta/engine/PhabricatorMailEngineExtension.php b/src/applications/metamta/engine/PhabricatorMailEngineExtension.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailEngineExtension.php @@ -0,0 +1,47 @@ +getPhobjectClassConstant('EXTENSIONKEY'); + } + + final public function setViewer($viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setEditor( + PhabricatorApplicationTransactionEditor $editor) { + $this->editor = $editor; + return $this; + } + + final public function getEditor() { + return $this->editor; + } + + abstract public function supportsObject($object); + abstract public function newMailStampTemplates($object); + abstract public function newMailStamps($object, array $xactions); + + final public static function getAllExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExtensionKey') + ->execute(); + } + + final protected function getMailStamp($key) { + return $this->getEditor()->getMailStamp($key); + } + +} diff --git a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php --- a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php @@ -58,23 +58,48 @@ public function willSendMail(PhabricatorMetaMTAMail $mail) { $viewer = $this->getViewer(); + $show_stamps = $mail->shouldRenderMailStampsInBody($viewer); + + $body = $mail->getBody(); + $html_body = $mail->getHTMLBody(); + $has_html = (strlen($html_body) > 0); + + if ($show_stamps) { + $stamps = $mail->getMailStamps(); + + $body .= "\n"; + $body .= pht('STAMPS'); + $body .= "\n"; + $body .= implode(', ', $stamps); + $body .= "\n"; + + if ($has_html) { + $html = array(); + $html[] = phutil_tag('strong', array(), pht('STAMPS')); + $html[] = phutil_tag('br'); + $html[] = phutil_implode_html(', ', $stamps); + $html[] = phutil_tag('br'); + $html = phutil_tag('div', array(), $html); + $html_body .= hsprintf('%s', $html); + } + } + $mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs); $mail->addPHIDHeaders('X-Phabricator-Cc', $this->rawCCPHIDs); $to_handles = $viewer->loadHandles($this->rawToPHIDs); $cc_handles = $viewer->loadHandles($this->rawCCPHIDs); - $body = $mail->getBody(); $body .= "\n"; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); - $mail->setBody($body); - $html_body = $mail->getHTMLBody(); - if (strlen($html_body)) { + if ($has_html) { $html_body .= hsprintf( '%s', $this->getRecipientsSummaryHTML($to_handles, $cc_handles)); } + + $mail->setBody($body); $mail->setHTMLBody($html_body); $reply_to = $this->getReplyTo(); diff --git a/src/applications/metamta/stamp/PhabricatorMailStamp.php b/src/applications/metamta/stamp/PhabricatorMailStamp.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/stamp/PhabricatorMailStamp.php @@ -0,0 +1,88 @@ +getPhobjectClassConstant('STAMPTYPE'); + } + + final public function setKey($key) { + $this->key = $key; + return $this; + } + + final public function getKey() { + return $this->key; + } + + final protected function setRawValue($value) { + $this->value = $value; + return $this; + } + + final protected function getRawValue() { + return $this->value; + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setLabel($label) { + $this->label = $label; + return $this; + } + + final public function getLabel() { + return $this->label; + } + + public function setValue($value) { + return $this->setRawValue($value); + } + + final public function toDictionary() { + return array( + 'type' => $this->getStampType(), + 'key' => $this->getKey(), + 'value' => $this->getValueForDictionary(), + ); + } + + final public static function getAllStamps() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getStampType') + ->execute(); + } + + protected function getValueForDictionary() { + return $this->getRawValue(); + } + + public function setValueFromDictionary($value) { + return $this->setRawValue($value); + } + + public function getValueForRendering() { + return $this->getRawValue(); + } + + abstract public function renderStamps($value); + + final protected function renderStamp($key, $value = null) { + return $key.'('.$value.')'; + } + +} diff --git a/src/applications/metamta/stamp/PhabricatorStringMailStamp.php b/src/applications/metamta/stamp/PhabricatorStringMailStamp.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/stamp/PhabricatorStringMailStamp.php @@ -0,0 +1,16 @@ +renderStamp($this->getKey(), $value); + } + +} diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -299,6 +299,22 @@ return $this->getParam('mustEncryptReasons', array()); } + public function setMailStamps(array $stamps) { + return $this->setParam('stamps', $stamps); + } + + public function getMailStamps() { + return $this->getParam('stamps', array()); + } + + public function setMailStampMetadata($metadata) { + return $this->setParam('stampMetadata', $metadata); + } + + public function getMailStampMetadata() { + return $this->getParam('stampMetadata', array()); + } + public function setHTMLBody($html) { $this->setParam('html-body', $html); return $this; @@ -637,6 +653,11 @@ } } + $stamps = $this->getMailStamps(); + if ($stamps) { + $headers[] = array('X-Phabricator-Stamps', implode(', ', $stamps)); + } + $raw_body = idx($params, 'body', ''); $body = $raw_body; if ($must_encrypt) { @@ -1304,6 +1325,14 @@ return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL); } + public function shouldRenderMailStampsInBody($viewer) { + $preferences = $this->loadPreferences($viewer->getPHID()); + $value = $preferences->getSettingValue( + PhabricatorEmailStampsSetting::SETTINGKEY); + + return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/settings/setting/PhabricatorEmailStampsSetting.php b/src/applications/settings/setting/PhabricatorEmailStampsSetting.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorEmailStampsSetting.php @@ -0,0 +1,47 @@ + pht('Mail Headers'), + self::VALUE_BODY_STAMPS => pht('Mail Headers and Body'), + ); + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -72,6 +72,8 @@ private $modularTypes; private $silent; private $mustEncrypt; + private $stampTemplates = array(); + private $mailStamps = array(); private $transactionQueue = array(); @@ -1181,6 +1183,12 @@ $this->mailShouldSend = true; $this->mailToPHIDs = $this->getMailTo($object); $this->mailCCPHIDs = $this->getMailCC($object); + + $mail_xactions = $this->getTransactionsForMail($object, $xactions); + $stamps = $this->newMailStamps($object, $xactions); + foreach ($stamps as $stamp) { + $this->mailStamps[] = $stamp->toDictionary(); + } } if ($this->shouldPublishFeedStory($object, $xactions)) { @@ -2611,6 +2619,7 @@ $mail_tags = $this->getMailTags($object, $mail_xactions); $action = $this->getMailAction($object, $mail_xactions); + $stamps = $this->generateMailStamps($object, $this->mailStamps); if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { $this->addEmailPreferenceSectionToMailBody( @@ -2649,6 +2658,18 @@ $mail->setParentMessageID($this->getParentMessageID()); } + // If we have stamps, attach the raw dictionary version (not the actual + // objects) to the mail so that debugging tools can see what we used to + // render the final list. + if ($this->mailStamps) { + $mail->setMailStampMetadata($this->mailStamps); + } + + // If we have rendered stamps, attach them to the mail. + if ($stamps) { + $mail->setMailStamps($stamps); + } + return $target->willSendMail($mail); } @@ -3569,6 +3590,7 @@ 'feedShouldPublish', 'mailShouldSend', 'mustEncrypt', + 'mailStamps', ); } @@ -3961,4 +3983,131 @@ return $editor; } + +/* -( Stamps )------------------------------------------------------------- */ + + + public function newMailStampTemplates($object) { + $actor = $this->getActor(); + + $templates = array(); + + $extensions = $this->newMailExtensions($object); + foreach ($extensions as $extension) { + $stamps = $extension->newMailStampTemplates($object); + foreach ($stamps as $stamp) { + $key = $stamp->getKey(); + if (isset($templates[$key])) { + throw new Exception( + pht( + 'Mail extension ("%s") defines a stamp template with the '. + 'same key ("%s") as another template. Each stamp template '. + 'must have a unique key.', + get_class($extension), + $key)); + } + + $stamp->setViewer($actor); + + $templates[$key] = $stamp; + } + } + + return $templates; + } + + final public function getMailStamp($key) { + if (!isset($this->stampTemplates)) { + throw new PhutilInvalidStateException('newMailStampTemplates'); + } + + if (!isset($this->stampTemplates[$key])) { + throw new Exception( + pht( + 'Editor ("%s") has no mail stamp template with provided key ("%s").', + get_class($this), + $key)); + } + + return $this->stampTemplates[$key]; + } + + private function newMailStamps($object, array $xactions) { + $actor = $this->getActor(); + + $this->stampTemplates = $this->newMailStampTemplates($object); + + $extensions = $this->newMailExtensions($object); + $stamps = array(); + foreach ($extensions as $extension) { + $extension->newMailStamps($object, $xactions); + } + + return $this->stampTemplates; + } + + private function newMailExtensions($object) { + $actor = $this->getActor(); + + $all_extensions = PhabricatorMailEngineExtension::getAllExtensions(); + + $extensions = array(); + foreach ($all_extensions as $key => $template) { + $extension = id(clone $template) + ->setViewer($actor) + ->setEditor($this); + + if ($extension->supportsObject($object)) { + $extensions[$key] = $extension; + } + } + + return $extensions; + } + + private function generateMailStamps($object, $data) { + if (!$data || !is_array($data)) { + return null; + } + + $templates = $this->newMailStampTemplates($object); + foreach ($data as $spec) { + if (!is_array($spec)) { + continue; + } + + $key = idx($spec, 'key'); + if (!isset($templates[$key])) { + continue; + } + + $type = idx($spec, 'type'); + if ($templates[$key]->getStampType() !== $type) { + continue; + } + + $value = idx($spec, 'value'); + $templates[$key]->setValueFromDictionary($value); + } + + $results = array(); + foreach ($templates as $template) { + $value = $template->getValueForRendering(); + + $rendered = $template->renderStamps($value); + if ($rendered === null) { + continue; + } + + $rendered = (array)$rendered; + foreach ($rendered as $stamp) { + $results[] = $stamp; + } + } + + sort($results); + + return $results; + } + } diff --git a/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php @@ -0,0 +1,92 @@ +setKey('application') + ->setLabel(pht('Application')), + ); + + if ($this->hasMonogram($object)) { + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('monogram') + ->setLabel(pht('Object Monogram')); + } + + if ($this->hasPHID($object)) { + // This is a PHID, but we always want to render it as a raw string, so + // use a string mail stamp. + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('phid') + ->setLabel(pht('Object PHID')); + + $templates[] = id(new PhabricatorStringMailStamp()) + ->setKey('object-type') + ->setLabel(pht('Object Type')); + } + + return $templates; + } + + public function newMailStamps($object, array $xactions) { + $editor = $this->getEditor(); + $viewer = $this->getViewer(); + + $application = null; + $class = $editor->getEditorApplicationClass(); + if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + $application = newv($class, array()); + } + + if ($application) { + $application_name = $application->getName(); + $this->getMailStamp('application') + ->setValue($application_name); + } + + if ($this->hasMonogram($object)) { + $monogram = $object->getMonogram(); + $this->getMailStamp('monogram') + ->setValue($monogram); + } + + if ($this->hasPHID($object)) { + $object_phid = $object->getPHID(); + + $this->getMailStamp('phid') + ->setValue($object_phid); + + $phid_type = phid_get_type($object_phid); + if ($phid_type != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { + $this->getMailStamp('object-type') + ->setValue($phid_type); + } + } + } + + private function hasPHID($object) { + if (!($object instanceof LiskDAO)) { + return false; + } + + if (!$object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) { + return false; + } + + return true; + } + + private function hasMonogram($object) { + return method_exists($object, 'getMonogram'); + } + +}