1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-04 20:52:43 +01:00
phorge-phorge/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
Bob Trahan d0127f95e5 Maniphest - add support for !assign command
Summary:
also try to centralize some of the command parsing logic. note that differential is still an exception here. it uses a whitelist-style regex. i think long-term we should have this for every app but changing it seemed too big for this diff.

Fixes T3937.

Test Plan:
echo '!assign btrahan' | ./bin/mail receive-test --as xerxes --to T22 ; echo '!claim' | ./bin/mail receive-test --as xerxes --to T22

unit tests passed, though my new one is silly

Reviewers: epriestley

Reviewed By: epriestley

CC: Korvin, epriestley, aran

Maniphest Tasks: T3937

Differential Revision: https://secure.phabricator.com/D7307
2013-10-14 12:29:41 -07:00

335 lines
9.7 KiB
PHP

<?php
abstract class PhabricatorMailReplyHandler {
private $mailReceiver;
private $actor;
private $excludePHIDs = array();
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;
}
final public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
final public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle);
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.reply-handler-domain');
}
abstract public function getReplyHandlerInstructions();
abstract protected function receiveEmail(
PhabricatorMetaMTAReceivedMail $mail);
public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
$error = $this->sanityCheckEmail($mail);
if ($error) {
if ($this->shouldSendErrorEmail($mail)) {
$this->sendErrorEmail($error, $mail);
}
return null;
}
return $this->receiveEmail($mail);
}
private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) {
$body = $mail->getCleanTextBody();
$attachments = $mail->getAttachments();
if (empty($body) && empty($attachments)) {
return 'Empty email body. Email should begin with an !action and / or '.
'text to comment. Inline replies and signatures are ignored.';
}
return null;
}
/**
* Only send an error email if the user is talking to just Phabricator. We
* can assume if there is only one To address it is a Phabricator address
* since this code is running and everything.
*/
private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) {
return (count($mail->getToAddresses()) == 1) &&
(count($mail->getCCAddresses()) == 0);
}
private function sendErrorEmail($error,
PhabricatorMetaMTAReceivedMail $mail) {
$template = new PhabricatorMetaMTAMail();
$template->setSubject('Exception: unable to process your mail request');
$template->setBody($this->buildErrorMailBody($error, $mail));
$template->setRelatedPHID($mail->getRelatedPHID());
$phid = $this->getActor()->getPHID();
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
$tos = array($phid => $handle);
$mails = $this->multiplexMail($template, $tos, array());
foreach ($mails as $email) {
$email->saveAndSend();
}
return true;
}
private function buildErrorMailBody($error,
PhabricatorMetaMTAReceivedMail $mail) {
$original_body = $mail->getRawTextBody();
$main_body = <<<EOBODY
Your request failed because an error was encoutered while processing it:
ERROR: {$error}
-- Original Body -------------------------------------------------------------
{$original_body}
EOBODY;
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($main_body);
$body->addReplySection($this->getReplyHandlerInstructions());
return $body->render();
}
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain() &&
!$this->supportsPublicReplies();
}
public function supportsPublicReplies() {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
return false;
}
if (!$this->getReplyHandlerDomain()) {
return false;
}
return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
$this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
final public function getRecipientsSummary(
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$body = '';
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
if ($to_handles) {
$body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
}
if ($cc_handles) {
$body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
}
}
return $body;
}
final public function multiplexMail(
PhabricatorMetaMTAMail $mail_template,
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$result = array();
// If MetaMTA is configured to always multiplex, skip the single-email
// case.
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
// 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'));
if ($this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
return $result;
}
}
$tos = mpull($to_handles, null, 'getPHID');
$ccs = mpull($cc_handles, null, 'getPHID');
// 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 = $tos + $ccs;
// When multiplexing mail, explicitly include To/Cc information in the
// message body and headers.
$mail_template = clone $mail_template;
$mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
$mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));
$body = $mail_template->getBody();
$body .= "\n";
$body .= $this->getRecipientsSummary($to_handles, $cc_handles);
foreach ($recipients as $phid => $recipient) {
$mail = clone $mail_template;
if (isset($to_handles[$phid])) {
$mail->addTos(array($phid));
} else if (isset($cc_handles[$phid])) {
$mail->addCCs(array($phid));
} else {
// not good - they should be a to or a cc
continue;
}
$mail->setBody($body);
$reply_to = null;
if (!$reply_to && $this->supportsPrivateReplies()) {
$reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
}
if (!$reply_to && $this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
}
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
}
return $result;
}
protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$domain = $this->getReplyHandlerDomain();
// We compute a hash using the object's own PHID to prevent an attacker
// from blindly interacting with objects that they haven't ever received
// mail about by just sending to D1@, D2@, etc...
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$receiver->getPHID());
$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
protected function getSingleReplyHandlerPrefix($address) {
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
return ($single_handle_prefix)
? $single_handle_prefix . '+' . $address
: $address;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle,
$prefix) {
if ($handle->getType() != PhabricatorPeoplePHIDTypeUser::TYPECONST) {
// You must be a real user to get a private reply handler address.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($handle->getPHID()))
->executeOne();
if (!$user) {
// This may happen if a user was subscribed to something, and was then
// deleted.
return null;
}
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $user->getID();
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$handle->getPHID());
$domain = $this->getReplyHandlerDomain();
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
final protected function enhanceBodyWithAttachments(
$body,
array $attachments,
$format = '- {F%d, layout=link}') {
if (!$attachments) {
return $body;
}
// TODO: (T603) What's the policy here?
$files = id(new PhabricatorFile())
->loadAllWhere('phid in (%Ls)', $attachments);
// if we have some text then double return before adding our file list
if ($body) {
$body .= "\n\n";
}
foreach ($files as $file) {
$file_str = sprintf($format, $file->getID());
$body .= $file_str."\n";
}
return rtrim($body);
}
}