1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 14:00:56 +01:00

Flesh out web UI for mail a bit to prepare for Herald outbound rules

Summary:
Ref T9141. Ref T5791. Ref T7013. Major changes here is:

  - Currently, we don't store the headers we actually sent, or the reasons we actually did or did not deliver a mail.
    - Start storing these (as `headers.sent` and `actors.sent`).
    - Show them in the web UI.
    - Show them in `bin/mail show-outbound` (previously, we sort of re-computed them in a hacky way).
    - Take them into account in `bin/mail volume`.

Then some minor changes:

  - Show mail bodies.
  - Show more mail information.
  - Start renaming "MetaMTA" to "Mail", at least in the web UI.

Test Plan:
{F707501}
{F707502}
{F707503}
{F707504}
{F707505}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T5791, T7013, T9141

Differential Revision: https://secure.phabricator.com/D13878
This commit is contained in:
epriestley 2015-08-12 12:27:31 -07:00
parent 9a7beadd22
commit 8c06d89070
6 changed files with 390 additions and 120 deletions

View file

@ -3,7 +3,7 @@
final class PhabricatorMetaMTAApplication extends PhabricatorApplication {
public function getName() {
return pht('MetaMTA');
return pht('Mail');
}
public function getBaseURI() {
@ -15,11 +15,11 @@ final class PhabricatorMetaMTAApplication extends PhabricatorApplication {
}
public function getShortDescription() {
return pht('Delivers Mail');
return pht('Send and Receive Mail');
}
public function getFlavorText() {
return pht('Yo dawg, we heard you like MTAs.');
return pht('Every program attempts to expand until it can read mail.');
}
public function getApplicationGroup() {
@ -30,12 +30,8 @@ final class PhabricatorMetaMTAApplication extends PhabricatorApplication {
return false;
}
public function isLaunchable() {
return false;
}
public function getTypeaheadURI() {
return null;
return '/mail/';
}
public function getRoutes() {

View file

@ -4,7 +4,7 @@ final class PhabricatorMetaMTAMailViewController
extends PhabricatorMetaMTAController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$viewer = $this->getViewer();
$mail = id(new PhabricatorMetaMTAMailQuery())
->setViewer($viewer)
@ -19,17 +19,51 @@ final class PhabricatorMetaMTAMailViewController
} else {
$title = $mail->getSubject();
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($this->getRequest()->getUser())
->setUser($viewer)
->setPolicyObject($mail);
switch ($mail->getStatus()) {
case PhabricatorMetaMTAMail::STATUS_QUEUE:
$icon = 'fa-clock-o';
$color = 'blue';
$name = pht('Queued');
break;
case PhabricatorMetaMTAMail::STATUS_SENT:
$icon = 'fa-envelope';
$color = 'green';
$name = pht('Sent');
break;
case PhabricatorMetaMTAMail::STATUS_FAIL:
$icon = 'fa-envelope';
$color = 'red';
$name = pht('Delivery Failed');
break;
case PhabricatorMetaMTAMail::STATUS_VOID:
$icon = 'fa-envelope';
$color = 'black';
$name = pht('Voided');
break;
default:
$icon = 'fa-question-circle';
$color = 'yellow';
$name = pht('Unknown');
break;
}
$header->setStatus($icon, $color, $name);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(
'Mail '.$mail->getID());
->addTextCrumb(pht('Mail %d', $mail->getID()));
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($this->buildPropertyView($mail));
->addPropertyList($this->buildMessageProperties($mail), pht('Message'))
->addPropertyList($this->buildHeaderProperties($mail), pht('Headers'))
->addPropertyList($this->buildDeliveryProperties($mail), pht('Delivery'))
->addPropertyList($this->buildMetadataProperties($mail), pht('Metadata'));
return $this->buildApplicationPage(
array(
@ -42,42 +76,13 @@ final class PhabricatorMetaMTAMailViewController
));
}
private function buildPropertyView(PhabricatorMetaMTAMail $mail) {
private function buildMessageProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($mail);
$properties->addProperty(
pht('ID'),
$mail->getID());
$properties->addProperty(
pht('Status'),
$mail->getStatus());
if ($mail->getMessage()) {
$properties->addProperty(
pht('Status Details'),
$mail->getMessage());
}
if ($mail->getRelatedPHID()) {
$properties->addProperty(
pht('Related Object'),
$viewer->renderHandle($mail->getRelatedPHID()));
}
if ($mail->getActorPHID()) {
$actor_str = $viewer->renderHandle($mail->getActorPHID());
} else {
$actor_str = pht('Generated by Phabricator');
}
$properties->addProperty(
pht('Actor'),
$actor_str);
if ($mail->getFrom()) {
$from_str = $viewer->renderHandle($mail->getFrom());
} else {
@ -105,6 +110,167 @@ final class PhabricatorMetaMTAMailViewController
pht('Cc'),
$cc_list);
$properties->addSectionHeader(
pht('Message'),
PHUIPropertyListView::ICON_SUMMARY);
if ($mail->hasSensitiveContent()) {
$body = phutil_tag(
'em',
array(),
pht(
'The content of this mail is sensitive and it can not be '.
'viewed from the web UI.'));
} else {
$body = phutil_tag(
'div',
array(
'style' => 'white-space: pre-wrap',
),
$mail->getBody());
}
$properties->addTextContent($body);
return $properties;
}
private function buildHeaderProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setStacked(true);
$headers = $mail->getDeliveredHeaders();
if ($headers === null) {
$headers = $mail->generateHeaders();
}
// Sort headers by name.
$headers = isort($headers, 0);
foreach ($headers as $header) {
list($key, $value) = $header;
$properties->addProperty($key, $value);
}
return $properties;
}
private function buildDeliveryProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$actors = $mail->getDeliveredActors();
$reasons = null;
if (!$actors) {
// TODO: We can get rid of this special-cased message after these changes
// have been live for a while, but provide a more tailored message for
// now so things are a little less confusing for users.
if ($mail->getStatus() == PhabricatorMetaMTAMail::STATUS_SENT) {
$delivery = phutil_tag(
'em',
array(),
pht(
'This is an older message that predates recording delivery '.
'information, so none is available.'));
} else {
$delivery = phutil_tag(
'em',
array(),
pht(
'This message has not been delivered yet, so delivery information '.
'is not available.'));
}
} else {
$actor = idx($actors, $viewer->getPHID());
if (!$actor) {
$delivery = phutil_tag(
'em',
array(),
pht('This message was not delivered to you.'));
} else {
$deliverable = $actor['deliverable'];
if ($deliverable) {
$delivery = pht('Delivered');
} else {
$delivery = pht('Voided');
}
$reasons = id(new PHUIStatusListView());
$reason_codes = $actor['reasons'];
if (!$reason_codes) {
$reason_codes = array(
PhabricatorMetaMTAActor::REASON_NONE,
);
}
$icon_yes = 'fa-check green';
$icon_no = 'fa-times red';
foreach ($reason_codes as $reason) {
$target = phutil_tag(
'strong',
array(),
PhabricatorMetaMTAActor::getReasonName($reason));
if (PhabricatorMetaMTAActor::isDeliveryReason($reason)) {
$icon = $icon_yes;
} else {
$icon = $icon_no;
}
$item = id(new PHUIStatusItemView())
->setIcon($icon)
->setTarget($target)
->setNote(PhabricatorMetaMTAActor::getReasonDescription($reason));
$reasons->addItem($item);
}
}
}
$properties->addProperty(pht('Delivery'), $delivery);
if ($reasons) {
$properties->addProperty(pht('Reasons'), $reasons);
}
return $properties;
}
private function buildMetadataProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$details = $mail->getMessage();
if (!strlen($details)) {
$details = phutil_tag('em', array(), pht('None'));
}
$properties->addProperty(pht('Status Details'), $details);
$actor_phid = $mail->getActorPHID();
if ($actor_phid) {
$actor_str = $viewer->renderHandle($actor_phid);
} else {
$actor_str = pht('Generated by Phabricator');
}
$properties->addProperty(pht('Actor'), $actor_str);
$related_phid = $mail->getRelatedPHID();
if ($related_phid) {
$related = $viewer->renderHandle($mail->getRelatedPHID());
} else {
$related = phutil_tag('em', array(), pht('None'));
}
$properties->addProperty(pht('Related Object'), $related);
return $properties;
}

View file

@ -76,23 +76,20 @@ final class PhabricatorMailManagementShowOutboundWorkflow
$info[] = pht('Related PHID: %s', $message->getRelatedPHID());
$info[] = pht('Message: %s', $message->getMessage());
$ignore = array(
'body' => true,
'html-body' => true,
'headers' => true,
'attachments' => true,
'headers.sent' => true,
'authors.sent' => true,
);
$info[] = null;
$info[] = pht('PARAMETERS');
$parameters = $message->getParameters();
foreach ($parameters as $key => $value) {
if ($key == 'body') {
continue;
}
if ($key == 'html-body') {
continue;
}
if ($key == 'headers') {
continue;
}
if ($key == 'attachments') {
if (isset($ignore[$key])) {
continue;
}
@ -105,7 +102,13 @@ final class PhabricatorMailManagementShowOutboundWorkflow
$info[] = null;
$info[] = pht('HEADERS');
foreach (idx($parameters, 'headers', array()) as $header) {
$headers = $message->getDeliveredHeaders();
if (!$headers) {
$headers = $message->generateHeaders();
}
foreach ($headers as $header) {
list($name, $value) = $header;
$info[] = "{$name}: {$value}";
}
@ -119,21 +122,33 @@ final class PhabricatorMailManagementShowOutboundWorkflow
}
}
$actors = $message->loadAllActors();
$actors = array_select_keys(
$actors,
array_merge($message->getToPHIDs(), $message->getCcPHIDs()));
$info[] = null;
$info[] = pht('RECIPIENTS');
foreach ($actors as $actor) {
if ($actor->isDeliverable()) {
$info[] = ' '.coalesce($actor->getName(), $actor->getPHID());
} else {
$info[] = '! '.coalesce($actor->getName(), $actor->getPHID());
}
foreach ($actor->getDeliverabilityReasons() as $reason) {
$desc = PhabricatorMetaMTAActor::getReasonDescription($reason);
$info[] = ' - '.$desc;
$all_actors = $message->loadAllActors();
$actors = $message->getDeliveredActors();
if ($actors) {
$info[] = null;
$info[] = pht('RECIPIENTS');
foreach ($actors as $actor_phid => $actor_info) {
$actor = idx($all_actors, $actor_phid);
if ($actor) {
$actor_name = coalesce($actor->getName(), $actor_phid);
} else {
$actor_name = $actor_phid;
}
$deliverable = $actor_info['deliverable'];
if ($deliverable) {
$info[] = ' '.$actor_name;
} else {
$info[] = '! '.$actor_name;
}
$reasons = $actor_info['reasons'];
foreach ($reasons as $reason) {
$name = PhabricatorMetaMTAActor::getReasonName($reason);
$desc = PhabricatorMetaMTAActor::getReasonDescription($reason);
$info[] = ' - '.$name.': '.$desc;
}
}
}

View file

@ -28,8 +28,11 @@ final class PhabricatorMailManagementVolumeWorkflow
->execute();
$unfiltered = array();
$delivered = array();
foreach ($mails as $mail) {
// Count messages we attempted to deliver. This includes messages which
// were voided by preferences or other rules.
$unfiltered_actors = mpull($mail->loadAllActors(), 'getPHID');
foreach ($unfiltered_actors as $phid) {
if (empty($unfiltered[$phid])) {
@ -37,9 +40,26 @@ final class PhabricatorMailManagementVolumeWorkflow
}
$unfiltered[$phid]++;
}
// Now, count mail we actually delivered.
$result = $mail->getDeliveredActors();
if ($result) {
foreach ($result as $actor_phid => $actor_info) {
if (!$actor_info['deliverable']) {
continue;
}
if (empty($delivered[$actor_phid])) {
$delivered[$actor_phid] = 0;
}
$delivered[$actor_phid]++;
}
}
}
// Sort users by delivered mail, then unfiltered mail.
arsort($delivered);
arsort($unfiltered);
$delivered = $delivered + array_fill_keys(array_keys($unfiltered), 0);
$table = id(new PhutilConsoleTable())
->setBorders(true)
@ -52,16 +72,23 @@ final class PhabricatorMailManagementVolumeWorkflow
'unfiltered',
array(
'title' => pht('Unfiltered'),
))
->addColumn(
'delivered',
array(
'title' => pht('Delivered'),
));
$handles = $viewer->loadHandles(array_keys($unfiltered));
$names = mpull(iterator_to_array($handles), 'getName', 'getPHID');
foreach ($unfiltered as $phid => $count) {
foreach ($delivered as $phid => $delivered_count) {
$unfiltered_count = idx($unfiltered, $phid, 0);
$table->addRow(
array(
'user' => idx($names, $phid),
'unfiltered' => $count,
'unfiltered' => $unfiltered_count,
'delivered' => $delivered_count,
));
}
@ -70,7 +97,9 @@ final class PhabricatorMailManagementVolumeWorkflow
echo "\n";
echo pht('Mail sent in the last 30 days.')."\n";
echo pht(
'"Unfiltered" is raw volume before preferences were applied.')."\n";
'"Unfiltered" is raw volume before rules applied.')."\n";
echo pht(
'"Delivered" shows email actually sent.')."\n";
echo "\n";
return 0;

View file

@ -5,6 +5,7 @@ final class PhabricatorMetaMTAActor extends Phobject {
const STATUS_DELIVERABLE = 'deliverable';
const STATUS_UNDELIVERABLE = 'undeliverable';
const REASON_NONE = 'none';
const REASON_UNLOADABLE = 'unloadable';
const REASON_UNMAILABLE = 'unmailable';
const REASON_NO_ADDRESS = 'noaddress';
@ -71,8 +72,42 @@ final class PhabricatorMetaMTAActor extends Phobject {
return $this->reasons;
}
public static function isDeliveryReason($reason) {
switch ($reason) {
case self::REASON_NONE:
case self::REASON_FORCE:
case self::REASON_FORCE_HERALD:
return true;
default:
// All other reasons cause the message to not be delivered.
return false;
}
}
public static function getReasonName($reason) {
$names = array(
self::REASON_NONE => pht('None'),
self::REASON_DISABLED => pht('Disabled Recipient'),
self::REASON_BOT => pht('Bot Recipient'),
self::REASON_NO_ADDRESS => pht('No Address'),
self::REASON_EXTERNAL_TYPE => pht('External Recipient'),
self::REASON_UNMAILABLE => pht('Not Mailable'),
self::REASON_RESPONSE => pht('Similar Reply'),
self::REASON_SELF => pht('Self Mail'),
self::REASON_MAIL_DISABLED => pht('Mail Disabled'),
self::REASON_MAILTAGS => pht('Mail Tags'),
self::REASON_UNLOADABLE => pht('Bad Recipient'),
self::REASON_FORCE => pht('Forced Mail'),
self::REASON_FORCE_HERALD => pht('Forced by Herald'),
);
return idx($names, $reason, pht('Unknown ("%s")', $reason));
}
public static function getReasonDescription($reason) {
$descriptions = array(
self::REASON_NONE => pht(
'No special rules affected this mail.'),
self::REASON_DISABLED => pht(
'This user is disabled; disabled users do not receive mail.'),
self::REASON_BOT => pht(

View file

@ -436,6 +436,8 @@ final class PhabricatorMetaMTAMail
}
try {
$headers = $this->generateHeaders();
$params = $this->parameters;
$actors = $this->loadAllActors();
@ -535,16 +537,6 @@ final class PhabricatorMetaMTAMail
$add_cc,
mpull($cc_actors, 'getEmailAddress'));
break;
case 'headers':
foreach ($value as $pair) {
list($header_key, $header_value) = $pair;
// NOTE: If we have \n in a header, SES rejects the email.
$header_value = str_replace("\n", ' ', $header_value);
$mailer->addHeader($header_key, $header_value);
}
break;
case 'attachments':
$value = $this->getAttachments();
foreach ($value as $attachment) {
@ -593,11 +585,6 @@ final class PhabricatorMetaMTAMail
$mailer->setSubject(implode(' ', array_filter($subject)));
break;
case 'is-bulk':
if ($value) {
$mailer->addHeader('Precedence', 'bulk');
}
break;
case 'thread-id':
// NOTE: Gmail freaks out about In-Reply-To and References which
@ -608,7 +595,7 @@ final class PhabricatorMetaMTAMail
$value = '<'.$value.'@'.$domain.'>';
if ($is_first && $mailer->supportsMessageIDHeader()) {
$mailer->addHeader('Message-ID', $value);
$headers[] = array('Message-ID', $value);
} else {
$in_reply_to = $value;
$references = array($value);
@ -620,21 +607,16 @@ final class PhabricatorMetaMTAMail
$references[] = $parent_id;
}
$references = implode(' ', $references);
$mailer->addHeader('In-Reply-To', $in_reply_to);
$mailer->addHeader('References', $references);
$headers[] = array('In-Reply-To', $in_reply_to);
$headers[] = array('References', $references);
}
$thread_index = $this->generateThreadIndex($value, $is_first);
$mailer->addHeader('Thread-Index', $thread_index);
break;
case 'mailtags':
// Handled below.
break;
case 'subject-prefix':
case 'vary-subject-prefix':
// Handled above.
$headers[] = array('Thread-Index', $thread_index);
break;
default:
// Just discard.
// Other parameters are handled elsewhere or are not relevant to
// constructing the message.
break;
}
}
@ -660,6 +642,25 @@ final class PhabricatorMetaMTAMail
$mailer->setHTMLBody($params['html-body']);
}
// Pass the headers to the mailer, then save the state so we can show
// them in the web UI.
foreach ($headers as $header) {
list($header_key, $header_value) = $header;
$mailer->addHeader($header_key, $header_value);
}
$this->setParam('headers.sent', $headers);
// Save the final deliverability outcomes and reasoning so we can
// explain why things happened the way they did.
$actor_list = array();
foreach ($actors as $actor) {
$actor_list[$actor->getPHID()] = array(
'deliverable' => $actor->isDeliverable(),
'reasons' => $actor->getDeliverabilityReasons(),
);
}
$this->setParam('actors.sent', $actor_list);
if (!$add_to && !$add_cc) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
@ -692,24 +693,6 @@ final class PhabricatorMetaMTAMail
return $this->save();
}
$mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes');
$mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
// Some clients respect this to suppress OOF and other auto-responses.
$mailer->addHeader('X-Auto-Response-Suppress', 'All');
// If the message has mailtags, filter out any recipients who don't want
// to receive this type of mail.
$mailtags = $this->getParam('mailtags');
if ($mailtags) {
$tag_header = array();
foreach ($mailtags as $mailtag) {
$tag_header[] = '<'.$mailtag.'>';
}
$tag_header = implode(', ', $tag_header);
$mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header);
}
// Some mailers require a valid "To:" in order to deliver mail. If we
// don't have any "To:", try to fill it in with a placeholder "To:".
// If that also fails, move the "Cc:" line to "To:".
@ -1052,6 +1035,52 @@ final class PhabricatorMetaMTAMail
return $ret;
}
public function generateHeaders() {
$headers = array();
$headers[] = array('X-Phabricator-Sent-This-Message', 'Yes');
$headers[] = array('X-Mail-Transport-Agent', 'MetaMTA');
// Some clients respect this to suppress OOF and other auto-responses.
$headers[] = array('X-Auto-Response-Suppress', 'All');
// If the message has mailtags, filter out any recipients who don't want
// to receive this type of mail.
$mailtags = $this->getParam('mailtags');
if ($mailtags) {
$tag_header = array();
foreach ($mailtags as $mailtag) {
$tag_header[] = '<'.$mailtag.'>';
}
$tag_header = implode(', ', $tag_header);
$headers[] = array('X-Phabricator-Mail-Tags', $tag_header);
}
$value = $this->getParam('headers', array());
foreach ($value as $pair) {
list($header_key, $header_value) = $pair;
// NOTE: If we have \n in a header, SES rejects the email.
$header_value = str_replace("\n", ' ', $header_value);
$headers[] = array($header_key, $header_value);
}
$is_bulk = $this->getParam('is-bulk');
if ($is_bulk) {
$headers[] = array('Precedence', 'bulk');
}
return $headers;
}
public function getDeliveredHeaders() {
return $this->getParam('headers.sent');
}
public function getDeliveredActors() {
return $this->getParam('actors.sent');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */