Changeset View
Changeset View
Standalone View
Standalone View
src/applications/metamta/storage/PhabricatorMetaMTAMail.php
Show All 15 Lines | final class PhabricatorMetaMTAMail | ||||
protected $relatedPHID; | protected $relatedPHID; | ||||
private $recipientExpansionMap; | private $recipientExpansionMap; | ||||
private $routingMap; | private $routingMap; | ||||
public function __construct() { | public function __construct() { | ||||
$this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE; | $this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE; | ||||
$this->parameters = array('sensitive' => true); | $this->parameters = array( | ||||
'sensitive' => true, | |||||
'mustEncrypt' => false, | |||||
); | |||||
parent::__construct(); | parent::__construct(); | ||||
} | } | ||||
protected function getConfiguration() { | protected function getConfiguration() { | ||||
return array( | return array( | ||||
self::CONFIG_AUX_PHID => true, | self::CONFIG_AUX_PHID => true, | ||||
self::CONFIG_SERIALIZATION => array( | self::CONFIG_SERIALIZATION => array( | ||||
▲ Show 20 Lines • Show All 209 Lines • ▼ Show 20 Lines | public function setSensitiveContent($bool) { | ||||
$this->setParam('sensitive', $bool); | $this->setParam('sensitive', $bool); | ||||
return $this; | return $this; | ||||
} | } | ||||
public function hasSensitiveContent() { | public function hasSensitiveContent() { | ||||
return $this->getParam('sensitive', true); | return $this->getParam('sensitive', true); | ||||
} | } | ||||
public function setMustEncrypt($bool) { | |||||
$this->setParam('mustEncrypt', $bool); | |||||
return $this; | |||||
} | |||||
public function getMustEncrypt() { | |||||
return $this->getParam('mustEncrypt', false); | |||||
} | |||||
public function setHTMLBody($html) { | public function setHTMLBody($html) { | ||||
$this->setParam('html-body', $html); | $this->setParam('html-body', $html); | ||||
return $this; | return $this; | ||||
} | } | ||||
public function getBody() { | public function getBody() { | ||||
return $this->getParam('body'); | return $this->getParam('body'); | ||||
} | } | ||||
▲ Show 20 Lines • Show All 168 Lines • ▼ Show 20 Lines | try { | ||||
if (empty($params['from'])) { | if (empty($params['from'])) { | ||||
$mailer->setFrom($default_from); | $mailer->setFrom($default_from); | ||||
} | } | ||||
$is_first = idx($params, 'is-first-message'); | $is_first = idx($params, 'is-first-message'); | ||||
unset($params['is-first-message']); | unset($params['is-first-message']); | ||||
$is_threaded = (bool)idx($params, 'thread-id'); | $is_threaded = (bool)idx($params, 'thread-id'); | ||||
$must_encrypt = $this->getMustEncrypt(); | |||||
$reply_to_name = idx($params, 'reply-to-name', ''); | $reply_to_name = idx($params, 'reply-to-name', ''); | ||||
unset($params['reply-to-name']); | unset($params['reply-to-name']); | ||||
$add_cc = array(); | $add_cc = array(); | ||||
$add_to = array(); | $add_to = array(); | ||||
// If multiplexing is enabled, some recipients will be in "Cc" | // If multiplexing is enabled, some recipients will be in "Cc" | ||||
▲ Show 20 Lines • Show All 55 Lines • ▼ Show 20 Lines | try { | ||||
case 'cc': | case 'cc': | ||||
$cc_phids = $this->expandRecipients($value); | $cc_phids = $this->expandRecipients($value); | ||||
$cc_actors = array_select_keys($deliverable_actors, $cc_phids); | $cc_actors = array_select_keys($deliverable_actors, $cc_phids); | ||||
$add_cc = array_merge( | $add_cc = array_merge( | ||||
$add_cc, | $add_cc, | ||||
mpull($cc_actors, 'getEmailAddress')); | mpull($cc_actors, 'getEmailAddress')); | ||||
break; | break; | ||||
case 'attachments': | case 'attachments': | ||||
// If the mail content must be encrypted, don't add attachments. | |||||
if ($must_encrypt) { | |||||
break; | |||||
} | |||||
$value = $this->getAttachments(); | $value = $this->getAttachments(); | ||||
foreach ($value as $attachment) { | foreach ($value as $attachment) { | ||||
$mailer->addAttachment( | $mailer->addAttachment( | ||||
$attachment->getData(), | $attachment->getData(), | ||||
$attachment->getFilename(), | $attachment->getFilename(), | ||||
$attachment->getMimeType()); | $attachment->getMimeType()); | ||||
} | } | ||||
break; | break; | ||||
case 'subject': | case 'subject': | ||||
$subject = array(); | $subject = array(); | ||||
if ($is_threaded) { | if ($is_threaded) { | ||||
if ($this->shouldAddRePrefix($preferences)) { | if ($this->shouldAddRePrefix($preferences)) { | ||||
$subject[] = 'Re:'; | $subject[] = 'Re:'; | ||||
} | } | ||||
} | } | ||||
$subject[] = trim(idx($params, 'subject-prefix')); | $subject[] = trim(idx($params, 'subject-prefix')); | ||||
// If mail content must be encrypted, we replace the subject with | |||||
// a generic one. | |||||
if ($must_encrypt) { | |||||
$subject[] = pht('Object Updated'); | |||||
} else { | |||||
$vary_prefix = idx($params, 'vary-subject-prefix'); | $vary_prefix = idx($params, 'vary-subject-prefix'); | ||||
if ($vary_prefix != '') { | if ($vary_prefix != '') { | ||||
if ($this->shouldVarySubject($preferences)) { | if ($this->shouldVarySubject($preferences)) { | ||||
$subject[] = $vary_prefix; | $subject[] = $vary_prefix; | ||||
} | } | ||||
} | } | ||||
$subject[] = $value; | $subject[] = $value; | ||||
} | |||||
$mailer->setSubject(implode(' ', array_filter($subject))); | $mailer->setSubject(implode(' ', array_filter($subject))); | ||||
break; | break; | ||||
case 'thread-id': | case 'thread-id': | ||||
// NOTE: Gmail freaks out about In-Reply-To and References which | // NOTE: Gmail freaks out about In-Reply-To and References which | ||||
// aren't in the form "<string@domain.tld>"; this is also required | // aren't in the form "<string@domain.tld>"; this is also required | ||||
// by RFC 2822, although some clients are more liberal in what they | // by RFC 2822, although some clients are more liberal in what they | ||||
Show All 22 Lines | try { | ||||
break; | break; | ||||
default: | default: | ||||
// Other parameters are handled elsewhere or are not relevant to | // Other parameters are handled elsewhere or are not relevant to | ||||
// constructing the message. | // constructing the message. | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
$body = idx($params, 'body', ''); | $raw_body = idx($params, 'body', ''); | ||||
$body = $raw_body; | |||||
if ($must_encrypt) { | |||||
$parts = array(); | |||||
$parts[] = pht( | |||||
'The content for this message can only be transmitted over a '. | |||||
'secure channel. To view the message content, follow this '. | |||||
'link:'); | |||||
$parts[] = PhabricatorEnv::getProductionURI($this->getURI()); | |||||
$body = implode("\n\n", $parts); | |||||
} else { | |||||
$body = $raw_body; | |||||
} | |||||
$max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); | $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); | ||||
if (strlen($body) > $max) { | if (strlen($body) > $max) { | ||||
$body = id(new PhutilUTF8StringTruncator()) | $body = id(new PhutilUTF8StringTruncator()) | ||||
->setMaximumBytes($max) | ->setMaximumBytes($max) | ||||
->truncateString($body); | ->truncateString($body); | ||||
$body .= "\n"; | $body .= "\n"; | ||||
$body .= pht('(This email was truncated at %d bytes.)', $max); | $body .= pht('(This email was truncated at %d bytes.)', $max); | ||||
} | } | ||||
$mailer->setBody($body); | $mailer->setBody($body); | ||||
$html_emails = $this->shouldSendHTML($preferences); | // If we sent a different message body than we were asked to, record | ||||
if ($html_emails && isset($params['html-body'])) { | // what we actually sent to make debugging and diagnostics easier. | ||||
if ($body !== $raw_body) { | |||||
$this->setParam('body.sent', $body); | |||||
} | |||||
if ($must_encrypt) { | |||||
$send_html = false; | |||||
} else { | |||||
$send_html = $this->shouldSendHTML($preferences); | |||||
} | |||||
if ($send_html && isset($params['html-body'])) { | |||||
$mailer->setHTMLBody($params['html-body']); | $mailer->setHTMLBody($params['html-body']); | ||||
} | } | ||||
// Pass the headers to the mailer, then save the state so we can show | // Pass the headers to the mailer, then save the state so we can show | ||||
// them in the web UI. | // them in the web UI. If the mail must be encrypted, we remove headers | ||||
foreach ($headers as $header) { | // which are not on a strict whitelist to avoid disclosing information. | ||||
$filtered_headers = $this->filterHeaders($headers, $must_encrypt); | |||||
foreach ($filtered_headers as $header) { | |||||
list($header_key, $header_value) = $header; | list($header_key, $header_value) = $header; | ||||
$mailer->addHeader($header_key, $header_value); | $mailer->addHeader($header_key, $header_value); | ||||
} | } | ||||
$this->setParam('headers.sent', $headers); | $this->setParam('headers.unfiltered', $headers); | ||||
$this->setParam('headers.sent', $filtered_headers); | |||||
// Save the final deliverability outcomes and reasoning so we can | // Save the final deliverability outcomes and reasoning so we can | ||||
// explain why things happened the way they did. | // explain why things happened the way they did. | ||||
$actor_list = array(); | $actor_list = array(); | ||||
foreach ($actors as $actor) { | foreach ($actors as $actor) { | ||||
$actor_list[$actor->getPHID()] = array( | $actor_list[$actor->getPHID()] = array( | ||||
'deliverable' => $actor->isDeliverable(), | 'deliverable' => $actor->isDeliverable(), | ||||
'reasons' => $actor->getDeliverabilityReasons(), | 'reasons' => $actor->getDeliverabilityReasons(), | ||||
▲ Show 20 Lines • Show All 396 Lines • ▼ Show 20 Lines | public function generateHeaders() { | ||||
$headers = array(); | $headers = array(); | ||||
$headers[] = array('X-Phabricator-Sent-This-Message', 'Yes'); | $headers[] = array('X-Phabricator-Sent-This-Message', 'Yes'); | ||||
$headers[] = array('X-Mail-Transport-Agent', 'MetaMTA'); | $headers[] = array('X-Mail-Transport-Agent', 'MetaMTA'); | ||||
// Some clients respect this to suppress OOF and other auto-responses. | // Some clients respect this to suppress OOF and other auto-responses. | ||||
$headers[] = array('X-Auto-Response-Suppress', 'All'); | $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'); | $mailtags = $this->getParam('mailtags'); | ||||
if ($mailtags) { | if ($mailtags) { | ||||
$tag_header = array(); | $tag_header = array(); | ||||
foreach ($mailtags as $mailtag) { | foreach ($mailtags as $mailtag) { | ||||
$tag_header[] = '<'.$mailtag.'>'; | $tag_header[] = '<'.$mailtag.'>'; | ||||
} | } | ||||
$tag_header = implode(', ', $tag_header); | $tag_header = implode(', ', $tag_header); | ||||
$headers[] = array('X-Phabricator-Mail-Tags', $tag_header); | $headers[] = array('X-Phabricator-Mail-Tags', $tag_header); | ||||
} | } | ||||
$value = $this->getParam('headers', array()); | $value = $this->getParam('headers', array()); | ||||
foreach ($value as $pair) { | foreach ($value as $pair) { | ||||
list($header_key, $header_value) = $pair; | list($header_key, $header_value) = $pair; | ||||
// NOTE: If we have \n in a header, SES rejects the email. | // NOTE: If we have \n in a header, SES rejects the email. | ||||
$header_value = str_replace("\n", ' ', $header_value); | $header_value = str_replace("\n", ' ', $header_value); | ||||
$headers[] = array($header_key, $header_value); | $headers[] = array($header_key, $header_value); | ||||
} | } | ||||
$is_bulk = $this->getParam('is-bulk'); | $is_bulk = $this->getParam('is-bulk'); | ||||
if ($is_bulk) { | if ($is_bulk) { | ||||
$headers[] = array('Precedence', 'bulk'); | $headers[] = array('Precedence', 'bulk'); | ||||
} | } | ||||
if ($this->getMustEncrypt()) { | |||||
$headers[] = array('X-Phabricator-Must-Encrypt', 'Yes'); | |||||
} | |||||
return $headers; | return $headers; | ||||
} | } | ||||
public function getDeliveredHeaders() { | public function getDeliveredHeaders() { | ||||
return $this->getParam('headers.sent'); | return $this->getParam('headers.sent'); | ||||
} | } | ||||
public function getUnfilteredHeaders() { | |||||
$unfiltered = $this->getParam('headers.unfiltered'); | |||||
if ($unfiltered === null) { | |||||
// Older versions of Phabricator did not filter headers, and thus did | |||||
// not record unfiltered headers. If we don't have unfiltered header | |||||
// data just return the delivered headers for compatibility. | |||||
return $this->getDeliveredHeaders(); | |||||
} | |||||
return $unfiltered; | |||||
} | |||||
public function getDeliveredActors() { | public function getDeliveredActors() { | ||||
return $this->getParam('actors.sent'); | return $this->getParam('actors.sent'); | ||||
} | } | ||||
public function getDeliveredRoutingRules() { | public function getDeliveredRoutingRules() { | ||||
return $this->getParam('routing.sent'); | return $this->getParam('routing.sent'); | ||||
} | } | ||||
public function getDeliveredRoutingMap() { | public function getDeliveredRoutingMap() { | ||||
return $this->getParam('routingmap.sent'); | return $this->getParam('routingmap.sent'); | ||||
} | } | ||||
public function getDeliveredBody() { | |||||
return $this->getParam('body.sent'); | |||||
} | |||||
private function filterHeaders(array $headers, $must_encrypt) { | |||||
if (!$must_encrypt) { | |||||
return $headers; | |||||
} | |||||
$whitelist = array( | |||||
'In-Reply-To', | |||||
'Message-ID', | |||||
'Precedence', | |||||
'References', | |||||
'Thread-Index', | |||||
'X-Mail-Transport-Agent', | |||||
'X-Auto-Response-Suppress', | |||||
'X-Phabricator-Sent-This-Message', | |||||
'X-Phabricator-Must-Encrypt', | |||||
); | |||||
// NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". | |||||
// This header contains a significant amount of meaningful information | |||||
// about the object. | |||||
$whitelist_map = array(); | |||||
foreach ($whitelist as $term) { | |||||
$whitelist_map[phutil_utf8_strtolower($term)] = true; | |||||
} | |||||
foreach ($headers as $key => $header) { | |||||
list($name, $value) = $header; | |||||
$name = phutil_utf8_strtolower($name); | |||||
if (!isset($whitelist_map[$name])) { | |||||
unset($headers[$key]); | |||||
} | |||||
} | |||||
return $headers; | |||||
} | |||||
public function getURI() { | |||||
return '/mail/detail/'.$this->getID().'/'; | |||||
} | |||||
/* -( Routing )------------------------------------------------------------ */ | /* -( Routing )------------------------------------------------------------ */ | ||||
public function addRoutingRule($routing_rule, $phids, $reason_phid) { | public function addRoutingRule($routing_rule, $phids, $reason_phid) { | ||||
$routing = $this->getParam('routing', array()); | $routing = $this->getParam('routing', array()); | ||||
$routing[] = array( | $routing[] = array( | ||||
'routingRule' => $routing_rule, | 'routingRule' => $routing_rule, | ||||
▲ Show 20 Lines • Show All 127 Lines • Show Last 20 Lines |