mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-19 12:00:55 +01:00
Enable SendGrid Parse API as an inbound email handler
Summary: Sendmail is seriously difficult to configure; SendGrid is extremely easy. It's also pretty expensive ($80/mo) but there are a bunch of startups that already have plans so it's effectively free for them. Test Plan: Configured SendGrid and sent reply email through it. Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: aran, epriestley Differential Revision: 376
This commit is contained in:
parent
d3fed84b9c
commit
8ae765f6d7
7 changed files with 211 additions and 6 deletions
|
@ -359,6 +359,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorMetaMTAReceivedListController' => 'applications/metamta/controller/receivedlist',
|
'PhabricatorMetaMTAReceivedListController' => 'applications/metamta/controller/receivedlist',
|
||||||
'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/receivedmail',
|
'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/receivedmail',
|
||||||
'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
|
'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
|
||||||
|
'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/sendgridreceive',
|
||||||
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
|
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
|
||||||
'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default',
|
'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default',
|
||||||
'PhabricatorOAuthDiagnosticsController' => 'applications/auth/controller/oauthdiagnostics',
|
'PhabricatorOAuthDiagnosticsController' => 'applications/auth/controller/oauthdiagnostics',
|
||||||
|
@ -798,6 +799,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorMetaMTAReceivedListController' => 'PhabricatorMetaMTAController',
|
'PhabricatorMetaMTAReceivedListController' => 'PhabricatorMetaMTAController',
|
||||||
'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
|
'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
|
||||||
'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
|
'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
|
||||||
|
'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
|
||||||
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
|
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
|
||||||
'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController',
|
'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController',
|
||||||
'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController',
|
'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController',
|
||||||
|
|
|
@ -126,6 +126,7 @@ class AphrontDefaultApplicationConfiguration
|
||||||
=> 'PhabricatorMetaMTAMailingListEditController',
|
=> 'PhabricatorMetaMTAMailingListEditController',
|
||||||
'receive/$' => 'PhabricatorMetaMTAReceiveController',
|
'receive/$' => 'PhabricatorMetaMTAReceiveController',
|
||||||
'received/$' => 'PhabricatorMetaMTAReceivedListController',
|
'received/$' => 'PhabricatorMetaMTAReceivedListController',
|
||||||
|
'sendgrid/$' => 'PhabricatorMetaMTASendGridReceiveController',
|
||||||
),
|
),
|
||||||
|
|
||||||
'/login/' => array(
|
'/login/' => array(
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?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 PhabricatorMetaMTASendGridReceiveController
|
||||||
|
extends PhabricatorMetaMTAController {
|
||||||
|
|
||||||
|
public function shouldRequireLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
|
||||||
|
$request = $this->getRequest();
|
||||||
|
|
||||||
|
$raw_headers = $request->getStr('headers');
|
||||||
|
$raw_headers = explode("\n", rtrim($raw_headers));
|
||||||
|
$raw_dict = array();
|
||||||
|
foreach (array_filter($raw_headers) as $header) {
|
||||||
|
list($name, $value) = explode(':', $header, 2);
|
||||||
|
$raw_dict[$name] = ltrim($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = array(
|
||||||
|
'to' => $request->getStr('to'),
|
||||||
|
'from' => $request->getStr('from'),
|
||||||
|
'subject' => $request->getStr('subject'),
|
||||||
|
) + $raw_dict;
|
||||||
|
|
||||||
|
$received = new PhabricatorMetaMTAReceivedMail();
|
||||||
|
$received->setHeaders($headers);
|
||||||
|
$received->setBodies(array(
|
||||||
|
'text' => $request->getStr('text'),
|
||||||
|
'html' => $request->getStr('from'),
|
||||||
|
));
|
||||||
|
|
||||||
|
$file_phids = array();
|
||||||
|
foreach ($_FILES as $file_raw) {
|
||||||
|
try {
|
||||||
|
$file = PhabricatorFile::newFromPHPUpload($file_raw);
|
||||||
|
$file_phids[] = $file->getPHID();
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
phlog($ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$received->setAttachments($file_phids);
|
||||||
|
$received->save();
|
||||||
|
|
||||||
|
$received->processReceivedMail();
|
||||||
|
|
||||||
|
$response = new AphrontWebpageResponse();
|
||||||
|
$response->setContent("Got it! Thanks, SendGrid!\n");
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/webpage');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/file');
|
||||||
|
phutil_require_module('phabricator', 'applications/metamta/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/metamta/storage/receivedmail');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'error');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorMetaMTASendGridReceiveController.php');
|
|
@ -36,6 +36,16 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
|
||||||
) + parent::getConfiguration();
|
) + parent::getConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setHeaders(array $headers) {
|
||||||
|
// Normalize headers to lowercase.
|
||||||
|
$normalized = array();
|
||||||
|
foreach ($headers as $name => $value) {
|
||||||
|
$normalized[strtolower($name)] = $value;
|
||||||
|
}
|
||||||
|
$this->headers = $normalized;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function processReceivedMail() {
|
public function processReceivedMail() {
|
||||||
$to = idx($this->headers, 'to');
|
$to = idx($this->headers, 'to');
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,114 @@ may update Differential and Maniphest by replying to messages.
|
||||||
|
|
||||||
= Preamble =
|
= Preamble =
|
||||||
|
|
||||||
This is extremely difficult to configure correctly. This is doubly true if
|
This can be extremely difficult to configure correctly. This is doubly true if
|
||||||
you use sendmail.
|
you use sendmail.
|
||||||
|
|
||||||
|
There are basically a few approaches available:
|
||||||
|
|
||||||
|
- Use SendGrid (<http://sendgrid.com/>), which is very easy but is not free.
|
||||||
|
- Run your own MTA, which can be quite harrowing to configure but is free.
|
||||||
|
- Tell the Phabricator devteam about another service you'd like support for,
|
||||||
|
this stuff is seriously terrible to configure on your own.
|
||||||
|
|
||||||
|
= Configuring Phabricator =
|
||||||
|
|
||||||
|
By default, Phabricator uses a "noreply@example.com" email address as the 'From'
|
||||||
|
(configurable with ##metamta.default-address##) and sets 'Reply-To' to the
|
||||||
|
user generating the email (e.g., by making a comment), if the mail was generated
|
||||||
|
by a user action. This means that users can reply (or reply-all) to email to
|
||||||
|
discuss changes, but the conversation won't be recorded in Phabricator and users
|
||||||
|
will not be able to take actions like claiming tasks or requesting changes to
|
||||||
|
revisions.
|
||||||
|
|
||||||
|
To change this behavior so that users can interact with objects in Phabricator
|
||||||
|
over email, set these configuration keys:
|
||||||
|
|
||||||
|
- ##metamta.differential.reply-handler-domain##: enables email replies for
|
||||||
|
Differential.
|
||||||
|
- ##metamta.maniphest.reply-handler-domain##: enables email replies for
|
||||||
|
Maniphest.
|
||||||
|
|
||||||
|
Set these keys to some domain which you configure according to the instructions
|
||||||
|
below, e.g. "##phabricator.example.com##". You can set these both to the same
|
||||||
|
domain, and will generally want to. Once you set these keys, emails will use a
|
||||||
|
'Reply-To' like "##T123+273+af310f9220ad@example.com##", which -- when
|
||||||
|
configured correctly, according to the instructions below -- will parse incoming
|
||||||
|
email and allow users to interact with Maniphest tasks and Differential
|
||||||
|
revisions over email.
|
||||||
|
|
||||||
|
= Security =
|
||||||
|
|
||||||
|
The email reply channel is "somewhat" authenticated. Each reply-to address is
|
||||||
|
unique to the recipient and includes a hash of user information and a unique
|
||||||
|
object ID, so it can only be used to update that object and only be used to act
|
||||||
|
on behalf of the recipient.
|
||||||
|
|
||||||
|
However, if an address is leaked (which is fairly easy -- for instance,
|
||||||
|
forwarding an email will leak a live reply address, or a user might take a
|
||||||
|
screenshot), //anyone// who can send mail to your reply-to domain may interact
|
||||||
|
with the object the email relates to as the user who leaked the mail. Because
|
||||||
|
the authentication around email has this weakness, some actions (like accepting
|
||||||
|
revisions) are not permitted over email.
|
||||||
|
|
||||||
|
This implementation is an attempt to balance utility and security, but makes
|
||||||
|
some sacrifices on both sides to achieve it because of the difficulty of
|
||||||
|
authenticating senders in the general case (e.g., where you are an open source
|
||||||
|
project and need to interact with users whose email accounts you have no control
|
||||||
|
over).
|
||||||
|
|
||||||
|
If you leak a bunch of reply-to addresses by accident, you can change
|
||||||
|
##phabricator.mail-key## in your configuration to invalidate all the old hashes.
|
||||||
|
|
||||||
|
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
|
||||||
|
reasonable mechanism exists to provide sender verification (e.g., DKIM
|
||||||
|
signatures are sufficient to authenticate the sender under your configuration,
|
||||||
|
or you are willing to require all users to sign their email), file a feature
|
||||||
|
request.
|
||||||
|
|
||||||
|
= Testing =
|
||||||
|
|
||||||
|
You can view a log of received mail by going to MetaMTA -> Received in the
|
||||||
|
Phabricator web interface. This can help you determine if mail is being
|
||||||
|
delivered to Phabricator or not.
|
||||||
|
|
||||||
|
You can also use the "Test Receiver" button, but note that this just simulates
|
||||||
|
receiving mail and doesn't send any information over the network. It is
|
||||||
|
primarily aimed at developing email handlers: it will still work properly
|
||||||
|
if your inbound email configuration is incorrect or even disabled.
|
||||||
|
|
||||||
|
= SendGrid =
|
||||||
|
|
||||||
|
To use SendGrid, you need a SendGrid account with access to the "Parse API" for
|
||||||
|
inbound email. Provided you have such an account, configure it like this:
|
||||||
|
|
||||||
|
- Configure an MX record according to SendGrid's instructions, i.e. add
|
||||||
|
##phabricator.example.com MX 10 mx.sendgrid.net.## or similar.
|
||||||
|
- Go to the "Parse Incoming Emails" page on SendGrid
|
||||||
|
(<http://sendgrid.com/developer/reply>) and add the domain as the
|
||||||
|
"Hostname".
|
||||||
|
- Add the URL ##https://phabricator.example.com/mail/sendgrid/## as the "Url",
|
||||||
|
using your domain (and HTTP instead of HTTPS if you are not configured with
|
||||||
|
SSL).
|
||||||
|
- If you get an error that the hostname "can't be located or verified", it
|
||||||
|
means your MX record is either incorrectly configured or hasn't propagated
|
||||||
|
yet.
|
||||||
|
- Set ##metamta.maniphest.reply-handler-domain## and/or
|
||||||
|
##metamta.differential.reply-handler-domain## to
|
||||||
|
"##phabricator.example.com##" (whatever you configured the MX record for),
|
||||||
|
depending on whether you want to support email replies for Maniphest,
|
||||||
|
Differential, or both.
|
||||||
|
|
||||||
|
That's it! If everything is working properly you should be able to send email
|
||||||
|
to ##anything@phabricator.example.com## and it should appear in the "Received"
|
||||||
|
tab of MetaMTA within a few seconds.
|
||||||
|
|
||||||
= Installing Mailparse =
|
= Installing Mailparse =
|
||||||
|
|
||||||
You need to install the PECL mailparse extension. In theory, you can do that
|
If you're going to run your own MTA, you need to install the PECL mailparse
|
||||||
with:
|
extension. In theory, you can do that with:
|
||||||
|
|
||||||
$ sudo pecl install mailparse
|
$ sudo pecl install mailparse
|
||||||
|
|
||||||
|
@ -32,10 +133,13 @@ If you get a linker error like this:
|
||||||
mailparse.so. This is not the default if you have individual files in
|
mailparse.so. This is not the default if you have individual files in
|
||||||
##php.d/##.
|
##php.d/##.
|
||||||
|
|
||||||
= Configuring Sendmail =
|
= MTA: Configuring Sendmail =
|
||||||
|
|
||||||
|
Before you can configure Sendmail, you need to install Mailparse. See the
|
||||||
|
section "Installing Mailparse" above.
|
||||||
|
|
||||||
Sendmail is very difficult to configure. First, you need to configure it for
|
Sendmail is very difficult to configure. First, you need to configure it for
|
||||||
your domain so that mail can be delievered correctly. In broad strokes, this
|
your domain so that mail can be delivered correctly. In broad strokes, this
|
||||||
probably means something like this:
|
probably means something like this:
|
||||||
|
|
||||||
- add an MX record;
|
- add an MX record;
|
||||||
|
@ -61,5 +165,5 @@ Finally, edit ##/etc/mail/virtusertable## and add an entry like this:
|
||||||
|
|
||||||
That will forward all mail to @yourdomain.com to the Phabricator processing
|
That will forward all mail to @yourdomain.com to the Phabricator processing
|
||||||
script. Run ##sudo /etc/mail/make## or similar and then restart sendmail with
|
script. Run ##sudo /etc/mail/make## or similar and then restart sendmail with
|
||||||
##sudo /etc/init.d/sendmail restart##
|
##sudo /etc/init.d/sendmail restart##.
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
phutil_require_module('phutil', 'markup/engine/remarkup/markuprule/base');
|
phutil_require_module('phutil', 'markup/engine/remarkup/markuprule/base');
|
||||||
phutil_require_module('phutil', 'parser/uri');
|
phutil_require_module('phutil', 'parser/uri');
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
phutil_require_source('PhabricatorRemarkupRuleYoutube.php');
|
phutil_require_source('PhabricatorRemarkupRuleYoutube.php');
|
||||||
|
|
Loading…
Reference in a new issue