diff --git a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php index 31f61ad5a3..f0840ba7bf 100644 --- a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php @@ -1,130 +1,141 @@ config = $config; - } + private $supportsMessageID; + private $failPermanently; + private $failTemporarily; - public function setFrom($email, $name = '') { - $this->guts['from'] = $email; - $this->guts['from-name'] = $name; + public function setSupportsMessageID($support) { + $this->supportsMessageID = $support; return $this; } - public function addReplyTo($email, $name = '') { - if (empty($this->guts['reply-to'])) { - $this->guts['reply-to'] = array(); - } - $this->guts['reply-to'][] = array( - 'email' => $email, - 'name' => $name, - ); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->guts['tos'][] = $email; - } + public function setFailPermanently($fail) { + $this->failPermanently = true; return $this; } - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->guts['ccs'][] = $email; - } + public function setFailTemporarily($fail) { + $this->failTemporarily = true; return $this; } - public function addAttachment($data, $filename, $mimetype) { - $this->guts['attachments'][] = array( - 'data' => $data, - 'filename' => $filename, - 'mimetype' => $mimetype, + public function getSupportedMessageTypes() { + return array( + PhabricatorMailEmailMessage::MESSAGETYPE, ); - return $this; } - public function addHeader($header_name, $header_value) { - $this->guts['headers'][] = array($header_name, $header_value); - return $this; - } - - public function setBody($body) { - $this->guts['body'] = $body; - return $this; + protected function validateOptions(array $options) { + PhutilTypeSpec::checkMap($options, array()); } - public function setHTMLBody($html_body) { - $this->guts['html-body'] = $html_body; - return $this; + public function newDefaultOptions() { + return array(); } - public function setSubject($subject) { - $this->guts['subject'] = $subject; - return $this; + public function supportsMessageIDHeader() { + return $this->supportsMessageID; } - public function supportsMessageIDHeader() { - return idx($this->config, 'supportsMessageIDHeader', true); + public function getGuts() { + return $this->guts; } - public function send() { - if (!empty($this->guts['fail-permanently'])) { + public function sendMessage(PhabricatorMailExternalMessage $message) { + if ($this->failPermanently) { throw new PhabricatorMetaMTAPermanentFailureException( pht('Unit Test (Permanent)')); } - if (!empty($this->guts['fail-temporarily'])) { + if ($this->failTemporarily) { throw new Exception( pht('Unit Test (Temporary)')); } - $this->guts['did-send'] = true; - return true; - } + $guts = array(); - public function getGuts() { - return $this->guts; - } + $from = $message->getFromAddress(); + $guts['from'] = (string)$from; - public function setFailPermanently($fail) { - $this->guts['fail-permanently'] = $fail; - return $this; - } + $reply_to = $message->getReplyToAddress(); + if ($reply_to) { + $guts['reply-to'] = (string)$reply_to; + } - public function setFailTemporarily($fail) { - $this->guts['fail-temporarily'] = $fail; - return $this; + $to_addresses = $message->getToAddresses(); + $to = array(); + foreach ($to_addresses as $address) { + $to[] = (string)$address; + } + $guts['tos'] = $to; + + $cc_addresses = $message->getCCAddresses(); + $cc = array(); + foreach ($cc_addresses as $address) { + $cc[] = (string)$address; + } + $guts['ccs'] = $cc; + + $subject = $message->getSubject(); + if (strlen($subject)) { + $guts['subject'] = $subject; + } + + $headers = $message->getHeaders(); + $header_list = array(); + foreach ($headers as $header) { + $header_list[] = array( + $header->getName(), + $header->getValue(), + ); + } + $guts['headers'] = $header_list; + + $text_body = $message->getTextBody(); + if (strlen($text_body)) { + $guts['body'] = $text_body; + } + + $html_body = $message->getHTMLBody(); + if (strlen($html_body)) { + $guts['html-body'] = $html_body; + } + + $attachments = $message->getAttachments(); + $file_list = array(); + foreach ($attachments as $attachment) { + $file_list[] = array( + 'data' => $attachment->getData(), + 'filename' => $attachment->getFilename(), + 'mimetype' => $attachment->getMimeType(), + ); + } + $guts['attachments'] = $file_list; + + $guts['did-send'] = true; + + $this->guts = $guts; } + public function getBody() { return idx($this->guts, 'body'); } public function getHTMLBody() { return idx($this->guts, 'html-body'); } + } diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php index fb38601717..391acb2285 100644 --- a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php +++ b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php @@ -1,70 +1,70 @@ overrideEnvConfig('metamta.single-reply-handler-prefix', 'prefix'); $base = 'alincoln@example.com'; $same = array( 'alincoln@example.com', '"Abrahamn Lincoln" ', 'ALincoln@example.com', 'prefix+alincoln@example.com', ); foreach ($same as $address) { $this->assertTrue( PhabricatorMailUtil::matchAddresses( new PhutilEmailAddress($base), new PhutilEmailAddress($address)), pht('Address %s', $address)); } $diff = array( 'a.lincoln@example.com', 'aluncoln@example.com', 'prefixalincoln@example.com', 'badprefix+alincoln@example.com', 'bad+prefix+alincoln@example.com', 'prefix+alincoln+sufffix@example.com', ); foreach ($diff as $address) { $this->assertFalse( PhabricatorMailUtil::matchAddresses( new PhutilEmailAddress($base), new PhutilEmailAddress($address)), pht('Address: %s', $address)); } } public function testReservedAddresses() { - $default_address = id(new PhabricatorMetaMTAMail()) + $default_address = id(new PhabricatorMailEmailEngine()) ->newDefaultEmailAddress(); - $void_address = id(new PhabricatorMetaMTAMail()) + $void_address = id(new PhabricatorMailEmailEngine()) ->newVoidEmailAddress(); $map = array( 'alincoln@example.com' => false, 'sysadmin@example.com' => true, 'hostmaster@example.com' => true, '"Walter Ebmaster" ' => true, (string)$default_address => true, (string)$void_address => true, ); foreach ($map as $raw_address => $expect) { $address = new PhutilEmailAddress($raw_address); $this->assertEqual( $expect, PhabricatorMailUtil::isReservedAddress($address), pht('Reserved: %s', $raw_address)); } } } diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php index 904dd366cb..7462aaf558 100644 --- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php +++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php @@ -1,414 +1,422 @@ true, ); } public function testMailSendFailures() { $user = $this->generateNewTestUser(); $phid = $user->getPHID(); // Normally, the send should succeed. $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, $mail->getStatus()); // When the mailer fails temporarily, the mail should remain queued. $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); $mailer = new PhabricatorMailTestAdapter(); $mailer->setFailTemporarily(true); try { $mail->sendWithMailers(array($mailer)); } catch (Exception $ex) { // Ignore. } $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_QUEUE, $mail->getStatus()); // When the mailer fails permanently, the mail should be failed. $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); $mailer = new PhabricatorMailTestAdapter(); $mailer->setFailPermanently(true); try { $mail->sendWithMailers(array($mailer)); } catch (Exception $ex) { // Ignore. } $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_FAIL, $mail->getStatus()); } public function testRecipients() { $user = $this->generateNewTestUser(); $phid = $user->getPHID(); $mailer = new PhabricatorMailTestAdapter(); $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), pht('"To" is a recipient.')); // Test that the "No Self Mail" and "No Mail" preferences work correctly. $mail->setFrom($phid); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), pht('"From" does not exclude recipients by default.')); $user = $this->writeSetting( $user, PhabricatorEmailSelfActionsSetting::SETTINGKEY, true); $this->assertFalse( in_array($phid, $mail->buildRecipientList()), pht('"From" excludes recipients with no-self-mail set.')); $user = $this->writeSetting( $user, PhabricatorEmailSelfActionsSetting::SETTINGKEY, null); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), pht('"From" does not exclude recipients by default.')); $user = $this->writeSetting( $user, PhabricatorEmailNotificationsSetting::SETTINGKEY, true); $this->assertFalse( in_array($phid, $mail->buildRecipientList()), pht('"From" excludes recipients with no-mail set.')); $mail->setForceDelivery(true); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), pht('"From" includes no-mail recipients when forced.')); $mail->setForceDelivery(false); $user = $this->writeSetting( $user, PhabricatorEmailNotificationsSetting::SETTINGKEY, null); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), pht('"From" does not exclude recipients by default.')); // Test that explicit exclusion works correctly. $mail->setExcludeMailRecipientPHIDs(array($phid)); $this->assertFalse( in_array($phid, $mail->buildRecipientList()), pht('Explicit exclude excludes recipients.')); $mail->setExcludeMailRecipientPHIDs(array()); // Test that mail tag preferences exclude recipients. $user = $this->writeSetting( $user, PhabricatorEmailTagsSetting::SETTINGKEY, array( 'test-tag' => false, )); $mail->setMailTags(array('test-tag')); $this->assertFalse( in_array($phid, $mail->buildRecipientList()), pht('Tag preference excludes recipients.')); $user = $this->writeSetting( $user, PhabricatorEmailTagsSetting::SETTINGKEY, null); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), 'Recipients restored after tag preference removed.'); $email = id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND isPrimary = 1', $phid); $email->setIsVerified(0)->save(); $this->assertFalse( in_array($phid, $mail->buildRecipientList()), pht('Mail not sent to unverified address.')); $email->setIsVerified(1)->save(); $this->assertTrue( in_array($phid, $mail->buildRecipientList()), pht('Mail sent to verified address.')); } public function testThreadIDHeaders() { $this->runThreadIDHeadersWithConfiguration(true, true); $this->runThreadIDHeadersWithConfiguration(true, false); $this->runThreadIDHeadersWithConfiguration(false, true); $this->runThreadIDHeadersWithConfiguration(false, false); } private function runThreadIDHeadersWithConfiguration( $supports_message_id, $is_first_mail) { + $user = $this->generateNewTestUser(); + $phid = $user->getPHID(); + $mailer = new PhabricatorMailTestAdapter(); - $mailer->prepareForSend( - array( - 'supportsMessageIDHeader' => $supports_message_id, - )); + $mailer->setSupportsMessageID($supports_message_id); - $thread_id = ''; + $thread_id = 'somethread-12345'; - $mail = new PhabricatorMetaMTAMail(); - $mail->setThreadID($thread_id, $is_first_mail); - $mail->sendWithMailers(array($mailer)); + $mail = id(new PhabricatorMetaMTAMail()) + ->setThreadID($thread_id, $is_first_mail) + ->addTos(array($phid)) + ->sendWithMailers(array($mailer)); $guts = $mailer->getGuts(); - $dict = ipull($guts['headers'], 1, 0); + + $headers = idx($guts, 'headers', array()); + + $dict = array(); + foreach ($headers as $header) { + list($name, $value) = $header; + $dict[$name] = $value; + } if ($is_first_mail && $supports_message_id) { $expect_message_id = true; $expect_in_reply_to = false; $expect_references = false; } else { $expect_message_id = false; $expect_in_reply_to = true; $expect_references = true; } $case = ''; $this->assertTrue( isset($dict['Thread-Index']), pht('Expect Thread-Index header for case %s.', $case)); $this->assertEqual( $expect_message_id, isset($dict['Message-ID']), pht( 'Expectation about existence of Message-ID header for case %s.', $case)); $this->assertEqual( $expect_in_reply_to, isset($dict['In-Reply-To']), pht( 'Expectation about existence of In-Reply-To header for case %s.', $case)); $this->assertEqual( $expect_references, isset($dict['References']), pht( 'Expectation about existence of References header for case %s.', $case)); } private function writeSetting(PhabricatorUser $user, $key, $value) { $preferences = PhabricatorUserPreferences::loadUserPreferences($user); $editor = id(new PhabricatorUserPreferencesEditor()) ->setActor($user) ->setContentSource($this->newContentSource()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = array(); $xactions[] = $preferences->newTransaction($key, $value); $editor->applyTransactions($preferences, $xactions); return id(new PhabricatorPeopleQuery()) ->setViewer($user) ->withIDs(array($user->getID())) ->executeOne(); } public function testMailerFailover() { $user = $this->generateNewTestUser(); $phid = $user->getPHID(); $status_sent = PhabricatorMailOutboundStatus::STATUS_SENT; $status_queue = PhabricatorMailOutboundStatus::STATUS_QUEUE; $status_fail = PhabricatorMailOutboundStatus::STATUS_FAIL; $mailer1 = id(new PhabricatorMailTestAdapter()) ->setKey('mailer1'); $mailer2 = id(new PhabricatorMailTestAdapter()) ->setKey('mailer2'); $mailers = array( $mailer1, $mailer2, ); // Send mail with both mailers active. The first mailer should be used. $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)) ->sendWithMailers($mailers); $this->assertEqual($status_sent, $mail->getStatus()); $this->assertEqual('mailer1', $mail->getMailerKey()); // If the first mailer fails, the mail should be sent with the second // mailer. Since we transmitted the mail, this doesn't raise an exception. $mailer1->setFailTemporarily(true); $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)) ->sendWithMailers($mailers); $this->assertEqual($status_sent, $mail->getStatus()); $this->assertEqual('mailer2', $mail->getMailerKey()); // If both mailers fail, the mail should remain in queue. $mailer2->setFailTemporarily(true); $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)); $caught = null; try { $mail->sendWithMailers($mailers); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); $this->assertEqual($status_queue, $mail->getStatus()); $this->assertEqual(null, $mail->getMailerKey()); $mailer1->setFailTemporarily(false); $mailer2->setFailTemporarily(false); // If the first mailer fails permanently, the mail should fail even though // the second mailer isn't configured to fail. $mailer1->setFailPermanently(true); $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)); $caught = null; try { $mail->sendWithMailers($mailers); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); $this->assertEqual($status_fail, $mail->getStatus()); $this->assertEqual(null, $mail->getMailerKey()); } public function testMailSizeLimits() { $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('metamta.email-body-limit', 1024 * 512); $user = $this->generateNewTestUser(); $phid = $user->getPHID(); $string_1kb = str_repeat('x', 1024); $html_1kb = str_repeat('y', 1024); $string_1mb = str_repeat('x', 1024 * 1024); $html_1mb = str_repeat('y', 1024 * 1024); // First, send a mail with a small text body and a small HTML body to make // sure the basics work properly. $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)) ->setBody($string_1kb) ->setHTMLBody($html_1kb); $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, $mail->getStatus()); $text_body = $mailer->getBody(); $html_body = $mailer->getHTMLBody(); $this->assertEqual($string_1kb, $text_body); $this->assertEqual($html_1kb, $html_body); // Now, send a mail with a large text body and a large HTML body. We expect // the text body to be truncated and the HTML body to be dropped. $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)) ->setBody($string_1mb) ->setHTMLBody($html_1mb); $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, $mail->getStatus()); $text_body = $mailer->getBody(); $html_body = $mailer->getHTMLBody(); // We expect the body was truncated, because it exceeded the body limit. $this->assertTrue( (strlen($text_body) < strlen($string_1mb)), pht('Text Body Truncated')); // We expect the HTML body was dropped completely after the text body was // truncated. $this->assertTrue( !strlen($html_body), pht('HTML Body Removed')); // Next send a mail with a small text body and a large HTML body. We expect // the text body to be intact and the HTML body to be dropped. $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($phid)) ->setBody($string_1kb) ->setHTMLBody($html_1mb); $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, $mail->getStatus()); $text_body = $mailer->getBody(); $html_body = $mailer->getHTMLBody(); $this->assertEqual($string_1kb, $text_body); $this->assertTrue(!strlen($html_body)); } } diff --git a/src/applications/metamta/util/PhabricatorMailUtil.php b/src/applications/metamta/util/PhabricatorMailUtil.php index 17539a0add..672f80f666 100644 --- a/src/applications/metamta/util/PhabricatorMailUtil.php +++ b/src/applications/metamta/util/PhabricatorMailUtil.php @@ -1,111 +1,111 @@ getAddress(); $raw_address = phutil_utf8_strtolower($raw_address); $raw_address = trim($raw_address); // If a mailbox prefix is configured and present, strip it off. $prefix_key = 'metamta.single-reply-handler-prefix'; $prefix = PhabricatorEnv::getEnvConfig($prefix_key); $len = strlen($prefix); if ($len) { $prefix = $prefix.'+'; $len = $len + 1; if (!strncasecmp($raw_address, $prefix, $len)) { $raw_address = substr($raw_address, $len); } } return id(clone $address) ->setAddress($raw_address); } /** * Determine if two inbound email addresses are effectively identical. * * This method strips and normalizes addresses so that equivalent variations * are correctly detected as identical. For example, these addresses are all * considered to match one another: * * "Abraham Lincoln" * alincoln@example.com * * "Abraham" # With configured prefix. * * @param PhutilEmailAddress Email address. * @param PhutilEmailAddress Another email address. * @return bool True if addresses are effectively the same address. */ public static function matchAddresses( PhutilEmailAddress $u, PhutilEmailAddress $v) { $u = self::normalizeAddress($u); $v = self::normalizeAddress($v); return ($u->getAddress() === $v->getAddress()); } public static function isReservedAddress(PhutilEmailAddress $address) { $address = self::normalizeAddress($address); $local = $address->getLocalPart(); $reserved = array( 'admin', 'administrator', 'hostmaster', 'list', 'list-request', 'majordomo', 'postmaster', 'root', 'ssl-admin', 'ssladmin', 'ssladministrator', 'sslwebmaster', 'sysadmin', 'uucp', 'webmaster', 'noreply', 'no-reply', ); $reserved = array_fuse($reserved); if (isset($reserved[$local])) { return true; } - $default_address = id(new PhabricatorMetaMTAMail()) + $default_address = id(new PhabricatorMailEmailEngine()) ->newDefaultEmailAddress(); if (self::matchAddresses($address, $default_address)) { return true; } - $void_address = id(new PhabricatorMetaMTAMail()) + $void_address = id(new PhabricatorMailEmailEngine()) ->newVoidEmailAddress(); if (self::matchAddresses($address, $void_address)) { return true; } return false; } }