1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 00:32:42 +01:00

Support email multiplexing for private Reply-To addresses

Summary:
Provide a base PhabricatorMailReplyHandler class which handles the plumbing for
multiplexing email if necessary and supporting public and private reply handler
addressses. DifferentialReplyHandler now extends it, and a new
ManiphestReplyHandler also does.

The general approach here is that we have three supported cases:

  - no reply handler, default config, same as what we're doing now
  - public reply handler, requires overriding classes but just sets "reply-to"
to some address the install generates and still sends only one email
  - private reply handler, provides a default generation mechanism or you can
override it and splits mail apart so we send one to each recipient

Test Plan:
Sent email from Maniphest and Differential with and without
reply-handler-domains set.

Reviewed By: aran
Reviewers: jungejason, tuomaspelkonen, aran
CC: aran, epriestley
Differential Revision: 254
This commit is contained in:
epriestley 2011-05-09 16:31:26 -07:00
parent 9f12ffbaba
commit 71efb46ba7
14 changed files with 365 additions and 114 deletions

View file

@ -180,6 +180,38 @@ return array(
'amazon-ses.access-key' => null,
'amazon-ses.secret-key' => null,
// You can configure a reply handler domain so that email sent from Maniphest
// will have a special "Reply To" address like "T123+82+af19f@example.com"
// that allows recipients to reply by email and interact with tasks. For
// instructions on configurating reply handlers, see the article
// "Configuring Inbound Email" in the Phabricator documentation. By default,
// this is set to 'null' and Phabricator will use a generic 'noreply@' address
// or the address of the acting user instead of a special reply handler
// address (see 'metamta.default-address'). If you set a domain here,
// Phabricator will begin generating private reply handler addresses. See
// also 'metamta.maniphest.reply-handler' to further configure behavior.
// This key should be set to the domain part after the @, like "example.com".
'metamta.maniphest.reply-handler-domain' => null,
// You can follow the instructions in "Configuring Inbound Email" in the
// Phabricator documentation and set 'metamta.maniphest.reply-handler-domain'
// to support updating Maniphest tasks by email. If you want more advanced
// customization than this provides, you can override the reply handler
// class with an implementation of your own. This will allow you to do things
// like have a single public reply handler or change how private reply
// handlers are generated and validated.
// This key should be set to a loadable subclass of
// PhabricatorMailReplyHandler (and possibly of ManiphestReplyHandler).
'metamta.maniphest.reply-handler' => 'ManiphestReplyHandler',
// See 'metamta.maniphest.reply-handler-domain'. This does the same thing,
// but allows email replies via Differential.
'metamta.differential.reply-handler-domain' => null,
// See 'metamta.maniphest.reply-handler'. This does the same thing, but
// affects Differential.
'metamta.differential.reply-handler' => 'DifferentialReplyHandler',
// -- Auth ------------------------------------------------------------------ //
@ -317,9 +349,6 @@ return array(
'differential.revision-custom-detail-renderer' => null,
'phabricator.enable-reply-handling' => false,
'differential.replyhandler' => 'DifferentialReplyHandler',
// -- Maniphest ------------------------------------------------------------- //

View file

@ -248,6 +248,7 @@ phutil_register_library_map(array(
'LiskIsolationTestDAOException' => 'storage/lisk/dao/__tests__',
'ManiphestController' => 'applications/maniphest/controller/base',
'ManiphestDAO' => 'applications/maniphest/storage/base',
'ManiphestReplyHandler' => 'applications/maniphest/replyhandler',
'ManiphestTask' => 'applications/maniphest/storage/task',
'ManiphestTaskDetailController' => 'applications/maniphest/controller/taskdetail',
'ManiphestTaskEditController' => 'applications/maniphest/controller/taskedit',
@ -331,6 +332,7 @@ phutil_register_library_map(array(
'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/amazonses',
'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite',
'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/test',
'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/base',
'PhabricatorMetaMTAController' => 'applications/metamta/controller/base',
'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base',
'PhabricatorMetaMTADaemon' => 'applications/metamta/daemon/mta',
@ -615,6 +617,7 @@ phutil_register_library_map(array(
'DifferentialInlineCommentPreviewController' => 'DifferentialController',
'DifferentialInlineCommentView' => 'AphrontView',
'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail',
'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler',
'DifferentialReviewRequestMail' => 'DifferentialMail',
'DifferentialRevision' => 'DifferentialDAO',
'DifferentialRevisionCommentListView' => 'AphrontView',
@ -677,6 +680,7 @@ phutil_register_library_map(array(
'LiskIsolationTestDAO' => 'LiskDAO',
'ManiphestController' => 'PhabricatorController',
'ManiphestDAO' => 'PhabricatorLiskDAO',
'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler',
'ManiphestTask' => 'ManiphestDAO',
'ManiphestTaskDetailController' => 'ManiphestController',
'ManiphestTaskEditController' => 'ManiphestController',

View file

@ -70,48 +70,50 @@ abstract class DifferentialMail {
$subject = $this->buildSubject();
$body = $this->buildBody();
$mail = new PhabricatorMetaMTAMail();
$template = new PhabricatorMetaMTAMail();
$actor_handle = $this->getActorHandle();
$reply_handler = $this->getReplyHandler();
if ($actor_handle) {
$mail->setFrom($actor_handle->getPHID());
$template->setFrom($actor_handle->getPHID());
}
if ($reply_handler) {
if ($actor_handle) {
$actor = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$actor_handle->getPHID());
$reply_handler->setActor($actor);
}
$reply_to = $reply_handler->getReplyHandlerEmailAddress();
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
}
$mail
->addTos($to_phids)
->addCCs($cc_phids)
$template
->setSubject($subject)
->setBody($body)
->setIsHTML($this->shouldMarkMailAsHTML())
->addHeader('Thread-Topic', $this->getRevision()->getTitle());
$mail->setThreadID(
$template->setThreadID(
$this->getThreadID(),
$this->isFirstMailAboutRevision());
if ($this->heraldRulesHeader) {
$mail->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
$template->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
}
$mail->setRelatedPHID($this->getRevision()->getPHID());
$template->setRelatedPHID($this->getRevision()->getPHID());
$phids = array();
foreach ($to_phids as $phid) {
$phids[$phid] = true;
}
foreach ($cc_phids as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $to_phids),
array_select_keys($handles, $cc_phids));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
protected function buildSubject() {
return self::SUBJECT_PREFIX.' '.$this->renderSubject();
@ -125,9 +127,12 @@ abstract class DifferentialMail {
$body = $this->renderBody();
$handler_body_text = $this->getReplyHandlerBodyText();
if ($handler_body_text) {
$body .= $handler_body_text;
$reply_handler = $this->getReplyHandler();
$reply_instructions = $reply_handler->getReplyHandlerInstructions();
if ($reply_instructions) {
$body .=
"\nREPLY HANDLER ACTIONS\n".
" {$reply_instructions}\n";
}
if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
@ -151,47 +156,22 @@ EOTEXT;
return $body;
}
protected function getReplyHandlerBodyText() {
$reply_handler = $this->getReplyHandler();
if (!$reply_handler) {
return null;
}
return $reply_handler->getBodyText();
}
protected function getReplyHandler() {
if ($this->replyHandler) {
return $this->replyHandler;
}
$reply_handler = self::loadReplyHandler();
if (!$reply_handler) {
return null;
}
$handler_class = PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler');
$reply_handler = newv($handler_class, array());
$reply_handler->setMailReceiver($this->getRevision());
$reply_handler->setRevision($this->getRevision());
$this->replyHandler = $reply_handler;
return $this->replyHandler;
}
public static function loadReplyHandler() {
if (!PhabricatorEnv::getEnvConfig('phabricator.enable-reply-handling')) {
return null;
}
$reply_handler = PhabricatorEnv::getEnvConfig('differential.replyhandler');
if (!$reply_handler) {
return null;
}
PhutilSymbolLoader::loadClass($reply_handler);
$reply_handler = newv($reply_handler, array());
return $reply_handler;
}
protected function formatText($text) {
$text = explode("\n", $text);
foreach ($text as &$line) {

View file

@ -7,10 +7,9 @@
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phutil', 'symbols');
phutil_require_module('phutil', 'utils');

View file

@ -16,9 +16,23 @@
* limitations under the License.
*/
class DifferentialReplyHandler {
protected $revision;
protected $actor;
class DifferentialReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof DifferentialRevision)) {
throw new Exception("Receiver is not a DifferentialRevision!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'D');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler-domain');
}
/*
* Generate text like the following from the supported commands.
@ -29,7 +43,11 @@ class DifferentialReplyHandler {
*
* "
*/
public function getBodyText() {
public function getReplyHandlerInstructions() {
if (!$this->supportsReplies()) {
return null;
}
$supported_commands = $this->getSupportedCommands();
$text = '';
if (empty($supported_commands)) {
@ -37,7 +55,6 @@ class DifferentialReplyHandler {
}
$comment_command_printed = false;
$text .= "\nACTIONS\n";
if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) {
$text .= 'Reply to comment';
$comment_command_printed = true;
@ -59,7 +76,7 @@ class DifferentialReplyHandler {
$text .= implode(', ', $modified_commands);
}
$text .= ".\n\n";
$text .= ".";
return $text;
}
@ -75,19 +92,6 @@ class DifferentialReplyHandler {
);
}
public function getReplyHandlerEmailAddress() {
if (!self::isEnabled()) {
return null;
}
$revision = $this->getRevision();
if (!$revision) {
return null;
}
return '...'; // TODO: build the D1234+92+aldsbn@domain.com as per D226
}
public function handleAction($body) {
// all commands start with a bang and separated from the body by a newline
// to make sure that actual feedback text couldn't trigger an action.
@ -131,26 +135,4 @@ class DifferentialReplyHandler {
}
}
public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
public function getActor() {
return $this->actor;
}
public function setRevision(DifferentialRevision $revision) {
$this->revision = $revision;
return $this;
}
public function getRevision() {
return $this->revision;
}
public static function isEnabled() {
return PhabricatorEnv::getEnvConfig('phabricator.enable-reply-handling');
}
}

View file

@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'applications/differential/constants/action');
phutil_require_module('phabricator', 'applications/differential/editor/comment');
phutil_require_module('phabricator', 'applications/differential/mail/exception');
phutil_require_module('phabricator', 'applications/metamta/replyhandler/base');
phutil_require_module('phabricator', 'infrastructure/env');

View file

@ -140,6 +140,12 @@ class ManiphestTransactionEditor {
$phids[$phid] = true;
}
}
foreach ($email_to as $phid) {
$phids[$phid] = true;
}
foreach ($email_cc as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))
@ -162,6 +168,8 @@ class ManiphestTransactionEditor {
$task_uri = PhabricatorEnv::getURI('/T'.$task->getID());
$reply_handler = $this->buildReplyHandler($task);
if ($is_create) {
$body .=
"\n\n".
@ -174,19 +182,43 @@ class ManiphestTransactionEditor {
"TASK DETAIL\n".
" ".$task_uri."\n";
$reply_instructions = $reply_handler->getReplyHandlerInstructions();
if ($reply_instructions) {
$body .=
"\n".
"REPLY HANDLER ACTIONS\n".
" ".$reply_instructions."\n";
}
$thread_id = '<maniphest-task-'.$task->getPHID().'>';
$task_id = $task->getID();
$title = $task->getTitle();
id(new PhabricatorMetaMTAMail())
$template = id(new PhabricatorMetaMTAMail())
->setSubject(self::SUBJECT_PREFIX." [{$action}] T{$task_id}: {$title}")
->setFrom($transaction->getAuthorPHID())
->addTos($email_to)
->addCCs($email_cc)
->addHeader('Thread-Topic', 'Maniphest Task '.$task->getID())
->setThreadID($thread_id, $is_create)
->setRelatedPHID($task->getPHID())
->setBody($body)
->saveAndSend();
->setBody($body);
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
private function buildReplyHandler(ManiphestTask $task) {
$handler_class = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler');
$handler_object = newv($handler_class, array());
$handler_object->setMailReceiver($task);
return $handler_object;
}
}

View file

@ -0,0 +1,47 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ManiphestTask)) {
throw new Exception("Mail receiver is not a ManiphestTask!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return "Reply to comment or attach files, or !close, !claim, ".
"!unsubscribe, or !assign <user|upforgrabs>. ".
"TODO: None of this works yet!";
} else {
return null;
}
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/metamta/replyhandler/base');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_source('ManiphestReplyHandler.php');

View file

@ -0,0 +1,139 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorMailReplyHandler {
private $mailReceiver;
private $actor;
final public function setMailReceiver($mail_receiver) {
$this->validateMailReceiver($mail_receiver);
$this->mailReceiver = $mail_receiver;
return $this;
}
final public function getMailReceiver() {
return $this->mailReceiver;
}
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final public function getActor() {
return $this->actor;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle);
abstract public function getReplyHandlerDomain();
abstract public function getReplyHandlerInstructions();
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
(bool)$this->getPublicReplyHandlerEmailAddress();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
final public function multiplexMail(
PhabricatorMetaMTAMail $mail_template,
array $to_handles,
array $cc_handles) {
$result = array();
// If private replies are not supported, simply send one email to all
// recipients and CCs. This covers cases where we have no reply handler,
// or we have a public reply handler.
if (!$this->supportsPrivateReplies()) {
$mail = clone $mail_template;
$mail->addTos(mpull($to_handles, 'getPHID'));
$mail->addCCs(mpull($cc_handles, 'getPHID'));
$reply_to = $this->getPublicReplyHandlerEmailAddress();
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
return $result;
}
// Merge all the recipients together. TODO: We could keep the CCs as real
// CCs and send to a "noreply@domain.com" type address, but keep it simple
// for now.
$recipients = mpull($to_handles, null, 'getPHID') +
mpull($cc_handles, null, 'getPHID');
// This grouping is just so we can use the public reply-to for any
// recipients without a private reply-to, e.g. mailing lists.
$groups = array();
foreach ($recipients as $recipient) {
$private = $this->getPrivateReplyHandlerEmailAddress($recipient);
$groups[$private][] = $recipient;
}
foreach ($groups as $reply_to => $group) {
$mail = clone $mail_template;
$mail->addTos(mpull($group, 'getPHID'));
if (!$reply_to) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
}
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
}
return $result;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle,
$prefix) {
if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) {
// You must be a real user to get a private reply handler address.
return null;
}
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $handle->getAlternateID();
$hash = PhabricatorMetaMTAReceivedMail::computeMailHash(
$receiver->getMailKey(),
$handle->getPHID());
$domain = $this->getReplyHandlerDomain();
return "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/metamta/storage/receivedmail');
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorMailReplyHandler.php');

View file

@ -67,7 +67,9 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
$this->setRelatedPHID($receiver->getPHID());
$expect_hash = self::computeMailHash($receiver, $user);
$expect_hash = self::computeMailHash(
$receiver->getMailKey(),
$user->getPHID());
if ($expect_hash != $hash) {
return $this->setMessage("Invalid mail hash!")->save();
}
@ -138,13 +140,10 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
return $class_obj->load($receiver_id);
}
public static function computeMailHash(
$mail_receiver,
PhabricatorUser $user) {
public static function computeMailHash($mail_key, $phid) {
$global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
$local_mail_key = $mail_receiver->getMailKey();
$hash = sha1($local_mail_key.$global_mail_key.$user->getPHID());
$hash = sha1($mail_key.$global_mail_key.$phid);
return substr($hash, 0, 16);
}

View file

@ -26,6 +26,7 @@ class PhabricatorObjectHandle {
private $fullName;
private $imageURI;
private $timestamp;
private $alternateID;
public function setURI($uri) {
$this->uri = $uri;
@ -102,6 +103,15 @@ class PhabricatorObjectHandle {
return $this->timestamp;
}
public function setAlternateID($alternate_id) {
$this->alternateID = $alternate_id;
return $this;
}
public function getAlternateID() {
return $this->alternateID;
}
public function renderLink() {
switch ($this->getType()) {

View file

@ -76,6 +76,7 @@ class PhabricatorObjectHandleData {
$handle->setEmail($user->getEmail());
$handle->setFullName(
$user->getUsername().' ('.$user->getRealName().')');
$handle->setAlternateID($user->getID());
$img_phid = $user->getProfileImagePHID();
if ($img_phid) {