diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php index db8168d12b..d113e776ac 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php @@ -1,101 +1,101 @@ getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { $received = new PhabricatorMetaMTAReceivedMail(); $header_content = array( 'Message-ID' => Filesystem::readRandomCharacters(12), ); $from = $request->getStr('sender'); $to = $request->getStr('receiver'); $uri = '/mail/received/'; if (!empty($from)) { $header_content['from'] = $from; } if (preg_match('/.+@.+/', $to)) { $header_content['to'] = $to; } else { $receiver = PhabricatorMetaMTAReceivedMail::loadReceiverObject($to); if (!$receiver) { throw new Exception(pht("No such task or revision!")); } - $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( - $receiver->getMailKey(), - $user->getPHID()); + $hash = PhabricatorObjectMailReceiver::computeMailHash( + $receiver->getMailKey(), + $user->getPHID()); $header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@'; } $received->setHeaders($header_content); $received->setBodies( array( 'text' => $request->getStr('body'), )); $received->save(); $received->processReceivedMail(); return id(new AphrontRedirectResponse())->setURI($uri); } $form = new AphrontFormView(); $form->setUser($request->getUser()); $form->setAction($this->getApplicationURI('/receive/')); $form ->appendChild(hsprintf( '

%s

', pht( 'This form will simulate sending mail to an object '. 'or an email address.'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('From')) ->setName('sender')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('To')) ->setName('receiver') ->setCaption(pht( 'e.g. %s or %s', phutil_tag('tt', array(), 'D1234'), phutil_tag('tt', array(), 'bugs@example.com')))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Body')) ->setName('body')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Receive Mail'))); $panel = new AphrontPanelView(); $panel->setHeader(pht('Receive Email')); $panel->appendChild($form); $panel->setNoBackground(); $nav = $this->buildSideNavView(); $nav->selectFilter('receive'); $nav->appendChild($panel); return $this->buildApplicationPage( $nav, array( 'title' => pht('Receive Test'), 'device' => true, )); } } diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php index 893e7c36f4..2fb0632be5 100644 --- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php @@ -1,37 +1,43 @@ getAddressRegexp(); foreach ($mail->getToAddresses() as $address) { $address = self::stripMailboxPrefix($address); $local = id(new PhutilEmailAddress($address))->getLocalPart(); if (preg_match($regexp, $local)) { return true; } } return false; } private function getAddressRegexp() { $pattern = $this->getObjectPattern(); $regexp = '(^'. '(?P'.$pattern.')'. '\\+'. '(?P\w+)'. '\\+'. '(?P[a-f0-9]{16})'. '$)U'; return $regexp; } + public static function computeMailHash($mail_key, $phid) { + $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); + + $hash = PhabricatorHash::digest($mail_key.$global_mail_key.$phid); + return substr($hash, 0, 16); + } } diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index 67d8550edc..7bae7e51a6 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -1,328 +1,328 @@ 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(); $tos = array( $phid => PhabricatorObjectHandleData::loadOneHandle( $phid, // TODO: This could be cleaner (T603). PhabricatorUser::getOmnipotentUser()), ); $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 = <<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 = PhabricatorMetaMTAReceivedMail::computeMailHash( + $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() != PhabricatorPHIDConstants::PHID_TYPE_USER) { // You must be a real user to get a private reply handler address. return null; } $user = head(id(new PhabricatorPeopleQuery()) ->withPhids(array($handle->getPHID())) ->execute()); $receiver = $this->getMailReceiver(); $receiver_id = $receiver->getID(); $user_id = $user->getID(); - $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( + $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; } $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); } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index dd55adf4a9..ff1ec8f631 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -1,566 +1,561 @@ array( 'headers' => self::SERIALIZATION_JSON, 'bodies' => self::SERIALIZATION_JSON, 'attachments' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setHeaders(array $headers) { // Normalize headers to lowercase. $normalized = array(); foreach ($headers as $name => $value) { $name = $this->normalizeMailHeaderName($name); if ($name == 'message-id') { $this->setMessageIDHash(PhabricatorHash::digestForIndex($value)); } $normalized[$name] = $value; } $this->headers = $normalized; return $this; } public function getHeader($key, $default = null) { $key = $this->normalizeMailHeaderName($key); return idx($this->headers, $key, $default); } private function normalizeMailHeaderName($name) { return strtolower($name); } public function getMessageID() { return $this->getHeader('Message-ID'); } public function getSubject() { return $this->getHeader('Subject'); } public function getCCAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'cc')); } public function getToAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'to')); } private function loadExcludeMailRecipientPHIDs() { $addresses = array_merge( $this->getToAddresses(), $this->getCCAddresses()); return $this->loadPHIDsFromAddresses($addresses); } final public function loadCCPHIDs() { return $this->loadPHIDsFromAddresses($this->getCCAddresses()); } private function loadPHIDsFromAddresses(array $addresses) { if (empty($addresses)) { return array(); } $users = id(new PhabricatorUserEmail()) ->loadAllWhere('address IN (%Ls)', $addresses); $user_phids = mpull($users, 'getUserPHID'); $mailing_lists = id(new PhabricatorMetaMTAMailingList()) ->loadAllWhere('email in (%Ls)', $addresses); $mailing_list_phids = mpull($mailing_lists, 'getPHID'); return array_merge($user_phids, $mailing_list_phids); } /** * Parses "to" addresses, looking for a public create email address * first and if not found parsing the "to" address for reply handler * information: receiver name, user id, and hash. If nothing can be * found, it then loads user phids for as many to: email addresses as * it can, theoretically falling back to create a conpherence amongst * those users. */ private function getPhabricatorToInformation() { // Only one "public" create address so far $create_task = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); // For replies, look for an object address with a format like: // D291+291+b0a41ca848d66dcc@example.com $single_handle_prefix = PhabricatorEnv::getEnvConfig( 'metamta.single-reply-handler-prefix'); $prefixPattern = ($single_handle_prefix) ? preg_quote($single_handle_prefix, '/') . '\+' : ''; $pattern = "/^{$prefixPattern}((?:D|T|C|E)\d+)\+([\w]+)\+([a-f0-9]{16})@/U"; $phabricator_address = null; $receiver_name = null; $user_id = null; $hash = null; $user_phids = array(); $user_names = array(); foreach ($this->getToAddresses() as $address) { if ($address == $create_task) { $phabricator_address = $address; // it's okay to stop here because we just need to map a create // address to an application and don't need / won't have more // information in these cases. break; } $matches = null; $ok = preg_match( $pattern, $address, $matches); if ($ok) { $phabricator_address = $address; $receiver_name = $matches[1]; $user_id = $matches[2]; $hash = $matches[3]; break; } $parts = explode('@', $address); $maybe_name = trim($parts[0]); $maybe_domain = trim($parts[1]); $mail_domain = PhabricatorEnv::getEnvConfig('metamta.domain'); if ($mail_domain == $maybe_domain && PhabricatorUser::validateUsername($maybe_name)) { $user_names[] = $maybe_name; } } // since we haven't found a phabricator address, maybe this is // someone trying to create a conpherence? if (!$phabricator_address && $user_names) { $users = id(new PhabricatorUser()) ->loadAllWhere('userName IN (%Ls)', $user_names); $user_phids = mpull($users, 'getPHID'); } return array( $phabricator_address, $receiver_name, $user_id, $hash, $user_phids ); } public function processReceivedMail() { try { $this->dropMailFromPhabricator(); $this->dropMailAlreadyReceived(); $receiver = $this->loadReceiver(); $sender = $receiver->loadSender($this); } catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) { $this ->setStatus($ex->getStatusCode()) ->setMessage($ex->getMessage()) ->save(); return $this; } list($to, $receiver_name, $user_id, $hash, $user_phids) = $this->getPhabricatorToInformation(); if (!$to && !$user_phids) { $raw_to = idx($this->headers, 'to'); return $this->setMessage("Unrecognized 'to' format: {$raw_to}")->save(); } $from = idx($this->headers, 'from'); // TODO -- make this a switch statement / better if / when we add more // public create email addresses! $create_task = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); if ($create_task && $to == $create_task) { $receiver = new ManiphestTask(); $user = $this->lookupSender(); if ($user) { $this->setAuthorPHID($user->getPHID()); } else { $allow_email_users = PhabricatorEnv::getEnvConfig( 'phabricator.allow-email-users'); if ($allow_email_users) { $email = new PhutilEmailAddress($from); $xuser = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain IS NULL and accountID = %s', 'email', $email->getAddress()); if (!$xuser) { $xuser = new PhabricatorExternalAccount(); $xuser->setAccountID($email->getAddress()); $xuser->setAccountType('email'); $xuser->setDisplayName($email->getDisplayName()); $xuser->save(); } $user = $xuser->getPhabricatorUser(); } else { $default_author = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.default-public-author'); if ($default_author) { $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $default_author); if (!$user) { throw new Exception( "Phabricator is misconfigured, the configuration key ". "'metamta.maniphest.default-public-author' is set to user ". "'{$default_author}' but that user does not exist."); } } else { // TODO: We should probably bounce these since from the user's // perspective their email vanishes into a black hole. return $this->setMessage("Invalid public user '{$from}'.")->save(); } } } $receiver->setAuthorPHID($user->getPHID()); $receiver->setOriginalEmailSource($from); $receiver->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); $editor = new ManiphestTransactionEditor(); $editor->setActor($user); $handler = $editor->buildReplyHandler($receiver); $handler->setActor($user); $handler->setExcludeMailRecipientPHIDs( $this->loadExcludeMailRecipientPHIDs()); $handler->processEmail($this); $this->setRelatedPHID($receiver->getPHID()); $this->setMessage('OK'); return $this->save(); } // means we're creating a conpherence...! if ($user_phids) { // we must have a valid user who created this conpherence $user = $this->lookupSender(); if (!$user) { return $this->setMessage("Invalid public user '{$from}'.")->save(); } $conpherence = id(new ConpherenceReplyHandler()) ->setMailReceiver(new ConpherenceThread()) ->setMailAddedParticipantPHIDs($user_phids) ->setActor($user) ->setExcludeMailRecipientPHIDs($this->loadExcludeMailRecipientPHIDs()) ->processEmail($this); $this->setRelatedPHID($conpherence->getPHID()); $this->setMessage('OK'); return $this->save(); } if ($user_id == 'public') { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { return $this->setMessage("Public replies not enabled.")->save(); } $user = $this->lookupSender(); if (!$user) { return $this->setMessage("Invalid public user '{$from}'.")->save(); } $use_user_hash = false; } else { $user = id(new PhabricatorUser())->load($user_id); if (!$user) { return $this->setMessage("Invalid private user '{$user_id}'.")->save(); } $use_user_hash = true; } if ($user->getIsDisabled()) { return $this->setMessage("User '{$user_id}' is disabled")->save(); } $this->setAuthorPHID($user->getPHID()); $receiver = self::loadReceiverObject($receiver_name); if (!$receiver) { return $this->setMessage("Invalid object '{$receiver_name}'")->save(); } $this->setRelatedPHID($receiver->getPHID()); if ($use_user_hash) { // This is a private reply-to address, check that the user hash is // correct. $check_phid = $user->getPHID(); } else { // This is a public reply-to address, check that the object hash is // correct. $check_phid = $receiver->getPHID(); } - $expect_hash = self::computeMailHash($receiver->getMailKey(), $check_phid); + $expect_hash = PhabricatorObjectMailReceiver::computeMailHash( + $receiver->getMailKey(), + $check_phid); if ($expect_hash != $hash) { return $this->setMessage("Invalid mail hash!")->save(); } if ($receiver instanceof ManiphestTask) { $editor = new ManiphestTransactionEditor(); $editor->setActor($user); $handler = $editor->buildReplyHandler($receiver); } else if ($receiver instanceof DifferentialRevision) { $handler = DifferentialMail::newReplyHandlerForRevision($receiver); } else if ($receiver instanceof PhabricatorRepositoryCommit) { $handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit( $receiver); } else if ($receiver instanceof ConpherenceThread) { $handler = id(new ConpherenceReplyHandler()) ->setMailReceiver($receiver); } $handler->setActor($user); $handler->setExcludeMailRecipientPHIDs( $this->loadExcludeMailRecipientPHIDs()); $handler->processEmail($this); $this->setMessage('OK'); return $this->save(); } public function getCleanTextBody() { $body = idx($this->bodies, 'text'); $parser = new PhabricatorMetaMTAEmailBodyParser(); return $parser->stripTextBody($body); } public function getRawTextBody() { return idx($this->bodies, 'text'); } public static function loadReceiverObject($receiver_name) { if (!$receiver_name) { return null; } $receiver_type = $receiver_name[0]; $receiver_id = substr($receiver_name, 1); $class_obj = null; switch ($receiver_type) { case 'T': $class_obj = new ManiphestTask(); break; case 'D': $class_obj = new DifferentialRevision(); break; case 'C': $class_obj = new PhabricatorRepositoryCommit(); break; case 'E': $class_obj = new ConpherenceThread(); break; default: return null; } return $class_obj->load($receiver_id); } - public static function computeMailHash($mail_key, $phid) { - $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); - - $hash = PhabricatorHash::digest($mail_key.$global_mail_key.$phid); - return substr($hash, 0, 16); - } - /** * Strip an email address down to the actual user@domain.tld part if * necessary, since sometimes it will have formatting like * '"Abraham Lincoln" '. */ private function getRawEmailAddress($address) { $matches = null; $ok = preg_match('/<(.*)>/', $address, $matches); if ($ok) { $address = $matches[1]; } return $address; } private function getRawEmailAddresses($addresses) { $raw_addresses = array(); foreach (explode(',', $addresses) as $address) { $raw_addresses[] = $this->getRawEmailAddress($address); } return array_filter($raw_addresses); } private function lookupSender() { $from = idx($this->headers, 'from'); $from = $this->getRawEmailAddress($from); $user = PhabricatorUser::loadOneWithEmailAddress($from); // If Phabricator is configured to allow "Reply-To" authentication, try // the "Reply-To" address if we failed to match the "From" address. $config_key = 'metamta.insecure-auth-with-reply-to'; $allow_reply_to = PhabricatorEnv::getEnvConfig($config_key); if (!$user && $allow_reply_to) { $reply_to = idx($this->headers, 'reply-to'); $reply_to = $this->getRawEmailAddress($reply_to); if ($reply_to) { $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); } } $allow_email_users = PhabricatorEnv::getEnvConfig( 'phabricator.allow-email-users'); if (!$user && $allow_email_users) { $xusr = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain IS NULL and accountID = %s', 'email', $from); $user = $xusr->getPhabricatorUser(); } return $user; } /** * If Phabricator sent the mail, always drop it immediately. This prevents * loops where, e.g., the public bug address is also a user email address * and creating a bug sends them an email, which loops. */ private function dropMailFromPhabricator() { if (!$this->getHeader('x-phabricator-sent-this-message')) { return; } throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR, "Ignoring email with 'X-Phabricator-Sent-This-Message' header to avoid ". "loops."); } /** * If this mail has the same message ID as some other mail, and isn't the * first mail we we received with that message ID, we drop it as a duplicate. */ private function dropMailAlreadyReceived() { $message_id_hash = $this->getMessageIDHash(); if (!$message_id_hash) { // No message ID hash, so we can't detect duplicates. This should only // happen with very old messages. return; } $messages = $this->loadAllWhere( 'messageIDHash = %s ORDER BY id ASC LIMIT 2', $message_id_hash); $messages_count = count($messages); if ($messages_count <= 1) { // If we only have one copy of this message, we're good to process it. return; } $first_message = reset($messages); if ($first_message->getID() == $this->getID()) { // If this is the first copy of the message, it is okay to process it. // We may not have been able to to process it immediately when we received // it, and could may have received several copies without processing any // yet. return; } $message = sprintf( 'Ignoring email with message id hash "%s" that has been seen %d '. 'times, including this message.', $message_id_hash, $messages_count); throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_DUPLICATE, $message); } /** * Load a concrete instance of the @{class:PhabricatorMailReceiver} which * accepts this mail, if one exists. */ private function loadReceiver() { $receivers = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorMailReceiver') ->loadObjects(); $accept = array(); foreach ($receivers as $key => $receiver) { if (!$receiver->isEnabled()) { continue; } if ($receiver->canAcceptMail($this)) { $accept[$key] = $receiver; } } if (!$accept) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS, "No concrete, enabled subclasses of `PhabricatorMailReceiver` can ". "accept this mail."); } if (count($accept) > 1) { $names = implode(', ', array_keys($accept)); throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS, "More than one `PhabricatorMailReceiver` claims to accept this mail."); } return head($accept); } }