diff --git a/conf/default.conf.php b/conf/default.conf.php
index 7472c1ecb0..00adb39311 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -201,6 +201,16 @@ return array(
// single public email address, so objects can not be replied to blindly.
'metamta.public-replies' => false,
+ // You can configure an email address like "bugs@phabricator.example.com"
+ // which will automatically create Maniphest tasks when users send email
+ // to it. This relies on the "From" address to authenticate users, so it is
+ // is not completely secure. To set this up, enter a complete email
+ // address like "bugs@phabricator.example.com" and then configure mail to
+ // that address so it routed to Phabricator (if you've already configured
+ // reply handlers, you're probably already done). See "Configuring Inbound
+ // Email" in the documentation for more information.
+ 'metamta.maniphest.public-create-email' => null,
+
// -- Auth ------------------------------------------------------------------ //
diff --git a/src/applications/maniphest/controller/taskedit/ManiphestTaskEditController.php b/src/applications/maniphest/controller/taskedit/ManiphestTaskEditController.php
index 9f5f278935..e529d4f60b 100644
--- a/src/applications/maniphest/controller/taskedit/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/taskedit/ManiphestTaskEditController.php
@@ -285,11 +285,20 @@ class ManiphestTaskEditController extends ManiphestController {
}
}
+ $email_create = PhabricatorEnv::getEnvConfig(
+ 'metamta.maniphest.public-create-email');
+ $email_hint = null;
+ if (!$task->getID() && $email_create) {
+ $email_hint = 'You can also create tasks by sending an email to: '.
+ ''.phutil_escape_html($email_create).'';
+ }
+
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Description')
->setName('description')
+ ->setCaption($email_hint)
->setValue($task->getDescription()))
->appendChild(
id(new AphrontFormSubmitControl())
diff --git a/src/applications/maniphest/controller/taskedit/__init__.php b/src/applications/maniphest/controller/taskedit/__init__.php
index d524917ca9..a12c754262 100644
--- a/src/applications/maniphest/controller/taskedit/__init__.php
+++ b/src/applications/maniphest/controller/taskedit/__init__.php
@@ -19,6 +19,7 @@ phutil_require_module('phabricator', 'applications/maniphest/storage/transaction
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/form/base');
diff --git a/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php b/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
index 0e0297c197..374da6c953 100644
--- a/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
@@ -52,31 +52,85 @@ class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
+ // NOTE: We'll drop in here on both the "reply to a task" and "create a
+ // new task" workflows! Make sure you test both if you make changes!
+
$task = $this->getMailReceiver();
+
+ $is_new_task = !$task->getID();
+
$user = $this->getActor();
$body = $mail->getCleanTextBody();
$body = trim($body);
- $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);
+ $template = new ManiphestTransaction();
+ $template->setAuthorPHID($user->getPHID());
- $command = $matches[1];
+ if ($is_new_task) {
+ // If this is a new task, create a "User created this task." transaction
+ // and then set the title and description.
+ $xaction = clone $template;
+ $xaction->setTransactionType(ManiphestTransactionType::TYPE_STATUS);
+ $xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN);
+ $xactions[] = $xaction;
+
+ $task->setAuthorPHID($user->getPHID());
+ $task->setTitle(nonempty($mail->getSubject(), 'Untitled Task'));
+ $task->setDescription($body);
+
+ } else {
+ $lines = explode("\n", trim($body));
+ $first_line = head($lines);
+
+ $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];
+ }
+
+ $ttype = ManiphestTransactionType::TYPE_NONE;
+ $new_value = null;
+ switch ($command) {
+ case 'close':
+ $ttype = ManiphestTransactionType::TYPE_STATUS;
+ $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
+ break;
+ case 'claim':
+ $ttype = ManiphestTransactionType::TYPE_OWNER;
+ $new_value = $user->getPHID();
+ break;
+ case 'unsubscribe':
+ $ttype = ManiphestTransactionType::TYPE_CCS;
+ $ccs = $task->getCCPHIDs();
+ foreach ($ccs as $k => $phid) {
+ if ($phid == $user->getPHID()) {
+ unset($ccs[$k]);
+ }
+ }
+ $new_value = array_values($ccs);
+ break;
+ }
+
+ $xaction = clone $template;
+ $xaction->setTransactionType($ttype);
+ $xaction->setNewValue($new_value);
+ $xaction->setComments($body);
+
+ $xactions[] = $xaction;
}
- $xactions = array();
+ // TODO: We should look at CCs on the mail and add them as CCs.
$files = $mail->getAttachments();
if ($files) {
- $file_xaction = new ManiphestTransaction();
- $file_xaction->setAuthorPHID($user->getPHID());
+ $file_xaction = clone $template;
$file_xaction->setTransactionType(ManiphestTransactionType::TYPE_ATTACH);
$phid_type = PhabricatorPHIDConstants::PHID_TYPE_FILE;
@@ -89,37 +143,6 @@ class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
$xactions[] = $file_xaction;
}
- $ttype = ManiphestTransactionType::TYPE_NONE;
- $new_value = null;
- switch ($command) {
- case 'close':
- $ttype = ManiphestTransactionType::TYPE_STATUS;
- $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
- break;
- case 'claim':
- $ttype = ManiphestTransactionType::TYPE_OWNER;
- $new_value = $user->getPHID();
- break;
- case 'unsubscribe':
- $ttype = ManiphestTransactionType::TYPE_CCS;
- $ccs = $task->getCCPHIDs();
- foreach ($ccs as $k => $phid) {
- if ($phid == $user->getPHID()) {
- unset($ccs[$k]);
- }
- }
- $new_value = array_values($ccs);
- break;
- }
-
- $xaction = new ManiphestTransaction();
- $xaction->setAuthorPHID($user->getPHID());
- $xaction->setTransactionType($ttype);
- $xaction->setNewValue($new_value);
- $xaction->setComments($body);
-
- $xactions[] = $xaction;
-
$editor = new ManiphestTransactionEditor();
$editor->setParentMessageID($mail->getMessageID());
$editor->applyTransactions($task, $xactions);
diff --git a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
index 1ddc36c6e5..bbeca66523 100644
--- a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
@@ -50,15 +50,50 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
return idx($this->headers, 'message-id');
}
+ public function getSubject() {
+ return idx($this->headers, 'subject');
+ }
+
public function processReceivedMail() {
$to = idx($this->headers, 'to');
+ $to = $this->getRawEmailAddress($to);
- // Accept a match either at the beginning of the address or after an open
- // angle bracket, as in:
- // "some display name"
+ $from = idx($this->headers, 'from');
+
+ $create_task = PhabricatorEnv::getEnvConfig(
+ 'metamta.maniphest.public-create-email');
+
+ if ($create_task && $to == $create_task) {
+ $user = $this->lookupPublicUser();
+ if (!$user) {
+ // TODO: We should probably bounce these since from the user's
+ // perspective their email vanishes into a black hole.
+ return $this->setMessage("Invalid public user '{$from}'.")->save();
+ }
+
+ $this->setAuthorPHID($user->getPHID());
+
+ $receiver = new ManiphestTask();
+ $receiver->setAuthorPHID($user->getPHID());
+ $receiver->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
+
+ $editor = new ManiphestTransactionEditor();
+ $handler = $editor->buildReplyHandler($receiver);
+
+ $handler->setActor($user);
+ $handler->receiveEmail($this);
+
+ $this->setRelatedPHID($receiver->getPHID());
+ $this->setMessage('OK');
+
+ return $this->save();
+ }
+
+ // We've already stripped this, so look for an object address which has
+ // a format like: D291+291+b0a41ca848d66dcc@example.com
$matches = null;
$ok = preg_match(
- '/(?:^|<)((?:D|T)\d+)\+([\w]+)\+([a-f0-9]{16})@/U',
+ '/^((?:D|T)\d+)\+([\w]+)\+([a-f0-9]{16})@/U',
$to,
$matches);
@@ -75,18 +110,8 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
return $this->setMessage("Public replies not enabled.")->save();
}
- // Strip the email address out of the 'from' if necessary, since it might
- // have some formatting like '"Abraham Lincoln" '.
- $from = idx($this->headers, 'from');
- $matches = null;
- $ok = preg_match('/<(.*)>/', $from, $matches);
- if ($ok) {
- $from = $matches[1];
- }
+ $user = $this->lookupPublicUser();
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'email = %s',
- $from);
if (!$user) {
return $this->setMessage("Invalid public user '{$from}'.")->save();
}
@@ -181,5 +206,27 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
return substr($hash, 0, 16);
}
+ /**
+ * Strip an email address down to the actual user@domain.tld part if
+ * necessary, since sometimes it will have formatting like
+ * '"Abraham Lincoln" '.
+ */
+ private function getRawEmailAddress($address) {
+ $matches = null;
+ $ok = preg_match('/<(.*)>/', $address, $matches);
+ if ($ok) {
+ $address = $matches[1];
+ }
+ return $address;
+ }
+
+ private function lookupPublicUser() {
+ $from = idx($this->headers, 'from');
+ $from = $this->getRawEmailAddress($from);
+
+ return id(new PhabricatorUser())->loadOneWhere(
+ 'email = %s',
+ $from);
+ }
}
diff --git a/src/applications/metamta/storage/receivedmail/__init__.php b/src/applications/metamta/storage/receivedmail/__init__.php
index afd38aa144..6254939d3f 100644
--- a/src/applications/metamta/storage/receivedmail/__init__.php
+++ b/src/applications/metamta/storage/receivedmail/__init__.php
@@ -7,7 +7,9 @@
phutil_require_module('phabricator', 'applications/differential/mail/base');
+phutil_require_module('phabricator', 'applications/maniphest/constants/priority');
phutil_require_module('phabricator', 'applications/maniphest/editor/transaction');
+phutil_require_module('phabricator', 'applications/maniphest/storage/task');
phutil_require_module('phabricator', 'applications/metamta/parser');
phutil_require_module('phabricator', 'applications/metamta/storage/base');
phutil_require_module('phabricator', 'applications/people/storage/user');
diff --git a/src/docs/configuration/configuring_inbound_email.diviner b/src/docs/configuration/configuring_inbound_email.diviner
index 981b6a9512..e19f8885c9 100644
--- a/src/docs/configuration/configuring_inbound_email.diviner
+++ b/src/docs/configuration/configuring_inbound_email.diviner
@@ -2,7 +2,8 @@
@group config
This document contains instructions for configuring inbound email, so users
-may update Differential and Maniphest by replying to messages.
+may update Differential and Maniphest by replying to messages and create
+Maniphest tasks via email.
= Preamble =
@@ -42,6 +43,11 @@ configured correctly, according to the instructions below -- will parse incoming
email and allow users to interact with Maniphest tasks and Differential
revisions over email.
+You can also set up a task creation email address, like ##bugs@example.com##,
+which will create a Maniphest task out of any email which is set to it. To do
+this, set ##metamta.maniphest.public-create-email## in your configuration. This
+has some mild security implications, see below.
+
= Security =
The email reply channel is "somewhat" authenticated. Each reply-to address is
@@ -74,6 +80,9 @@ practically, is a reasonable setting for many installs. The reply-to address
will still contain a hash unique to the object it represents, so users who have
not received an email about an object can not blindly interact with it.
+If you enable ##metamta.maniphest.public-create-email##, that address also uses
+the weaker "From" authentication mechanism.
+
NOTE: Phabricator does not currently attempt to verify "From" addresses because
this is technically complex, seems unreasonably difficult in the general case,
and no installs have had a need for it yet. If you have a specific case where a