diff --git a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php --- a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php +++ b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php @@ -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 @@ } 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 @@ return false; } - public function isLaunchable() { - return false; - } - public function getTypeaheadURI() { - return null; + return '/mail/'; } public function getRoutes() { diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php @@ -4,7 +4,7 @@ extends PhabricatorMetaMTAController { public function handleRequest(AphrontRequest $request) { - $viewer = $request->getUser(); + $viewer = $this->getViewer(); $mail = id(new PhabricatorMetaMTAMailQuery()) ->setViewer($viewer) @@ -19,17 +19,51 @@ } 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 @@ )); } - 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 @@ 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; } diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -76,23 +76,20 @@ $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 @@ $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 @@ } } - $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; + } } } diff --git a/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php --- a/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php @@ -28,8 +28,11 @@ ->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 @@ } $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 @@ '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 @@ 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; diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php --- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php @@ -5,6 +5,7 @@ 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 @@ 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( diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -436,6 +436,8 @@ } try { + $headers = $this->generateHeaders(); + $params = $this->parameters; $actors = $this->loadAllActors(); @@ -535,16 +537,6 @@ $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 @@ $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 @@ $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 @@ $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 @@ $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 @@ 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 @@ 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 )----------------------------------------- */