diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index c9f757aee8..05c908e838 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,177 +1,190 @@ identifiers = $identifiers; return $this; } /** * If a default repository is provided, ambiguous commit identifiers will * be assumed to belong to the default repository. * * For example, "r123" appearing in a commit message in repository X is * likely to be unambiguously "rX123". Normally the reference would be * considered ambiguous, but if you provide a default repository it will * be correctly resolved. */ public function withDefaultRepository(PhabricatorRepository $repository) { $this->defaultRepository = $repository; return $this; } + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } protected function loadPage() { $table = new PhabricatorRepositoryCommit(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } public function willFilterPage(array $commits) { if (!$commits) { return array(); } $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIDs($repository_ids) ->execute(); foreach ($commits as $key => $commit) { $repo = idx($repos, $commit->getRepositoryID()); if ($repo) { $commit->attachRepository($repo); } else { unset($commits[$key]); } } return $commits; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->identifiers) { $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $refs = array(); $bare = array(); foreach ($this->identifiers as $identifier) { $matches = null; preg_match('/^(?:r([A-Z]+))?(.*)$/', $identifier, $matches); $repo = nonempty($matches[1], null); $identifier = nonempty($matches[2], null); if ($repo === null) { if ($this->defaultRepository) { $repo = $this->defaultRepository->getCallsign(); } } if ($repo === null) { if (strlen($identifier) < $min_unqualified) { continue; } $bare[] = $identifier; } else { $refs[] = array( 'callsign' => $repo, 'identifier' => $identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn_r, '(commitIdentifier LIKE %> AND LENGTH(commitIdentifier) = 40)', $identifier); } if ($refs) { $callsigns = ipull($refs, 'callsign'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withCallsigns($callsigns) ->execute(); $repos = mpull($repos, null, 'getCallsign'); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['callsign']); if (!$repo) { continue; } if ($repo->isSVN()) { if (!ctype_digit($ref['identifier'])) { continue; } $sql[] = qsprintf( $conn_r, '(repositoryID = %d AND commitIdentifier = %d)', $repo->getID(), $ref['identifier']); } else { if (strlen($ref['identifier']) < $min_qualified) { continue; } $sql[] = qsprintf( $conn_r, '(repositoryID = %d AND commitIdentifier LIKE %>)', $repo->getID(), $ref['identifier']); } } } if (!$sql) { // If we discarded all possible identifiers (e.g., they all referenced // bogus repositories or were all too short), make sure the query finds // nothing. throw new PhabricatorEmptyQueryException('No commit identifiers.'); } $where[] = '('.implode(' OR ', $sql).')'; } + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } } diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php index d113e776ac..91c12c2617 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php @@ -1,101 +1,142 @@ 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; + } else { + // If the user doesn't provide a "From" address, use their primary + // address. + $header_content['from'] = $user->loadPrimaryEmail()->getAddress(); } if (preg_match('/.+@.+/', $to)) { $header_content['to'] = $to; } else { - $receiver = PhabricatorMetaMTAReceivedMail::loadReceiverObject($to); + + // We allow the user to use an object name instead of a real address + // as a convenience. To build the mail, we build a similar message and + // look for a receiver which will accept it. + $pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y'); + $pseudomail = id(new PhabricatorMetaMTAReceivedMail()) + ->setHeaders( + array( + 'to' => $to.'+1+'.$pseudohash, + )); + + $receivers = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorMailReceiver') + ->loadObjects(); + + $receiver = null; + foreach ($receivers as $possible_receiver) { + if (!$possible_receiver->isEnabled()) { + continue; + } + if (!$possible_receiver->canAcceptMail($pseudomail)) { + continue; + } + $receiver = $possible_receiver; + break; + } if (!$receiver) { - throw new Exception(pht("No such task or revision!")); + throw new Exception( + "No configured mail receiver can accept mail to '{$to}'."); + } + + if (!($receiver instanceof PhabricatorObjectMailReceiver)) { + $class = get_class($receiver); + throw new Exception( + "Receiver '{$class}' accepts mail to '{$to}', but is not a ". + "subclass of PhabricatorObjectMailReceiver."); + } + + $object = $receiver->loadMailReceiverObject($to, $user); + if (!$object) { + throw new Exception("No such object '{$to}'!"); } $hash = PhabricatorObjectMailReceiver::computeMailHash( - $receiver->getMailKey(), + $object->getMailKey(), $user->getPHID()); - $header_content['to'] = - $to.'+'.$user->getID().'+'.$hash.'@'; + $header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@test.com'; } $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 f429106f7d..7dc37661ab 100644 --- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php @@ -1,143 +1,147 @@ loadObject($pattern, $viewer); + } + public function validateSender( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { parent::validateSender($mail, $sender); foreach ($mail->getToAddresses() as $address) { $parts = $this->matchObjectAddress($address); if ($parts) { break; } } try { $object = $this->loadObject($parts['pattern'], $sender); } catch (PhabricatorPolicyException $policy_exception) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM, pht( "This mail is addressed to an object you are not permitted ". "to see: %s", $policy_exception->getMessage())); } if (!$object) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT, pht( "This mail is addressed to an object ('%s'), but that object ". "does not exist.", $parts['pattern'])); } $sender_identifier = $parts['sender']; if ($sender_identifier === 'public') { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL, pht( "This mail is addressed to an object's public address, but ". "public replies are not enabled (`metamta.public-replies`).")); } $check_phid = $object->getPHID(); } else { if ($sender_identifier != $sender->getID()) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH, pht( "This mail is addressed to an object's private address, but ". "the sending user and the private address owner are not the ". "same user.")); } $check_phid = $sender->getPHID(); } $expect_hash = self::computeMailHash($object->getMailKey(), $check_phid); if ($expect_hash != $parts['hash']) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH, pht( "The hash in this object's address does not match the expected ". "value.")); } } final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { foreach ($mail->getToAddresses() as $address) { if ($this->matchObjectAddress($address)) { return true; } } return false; } private function matchObjectAddress($address) { $regexp = $this->getAddressRegexp(); $address = self::stripMailboxPrefix($address); $local = id(new PhutilEmailAddress($address))->getLocalPart(); $matches = null; if (!preg_match($regexp, $local, $matches)) { return false; } return $matches; } 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); } }