Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
index 9dc70e9d8a..9b55151747 100644
--- a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
+++ b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorAuditMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorDiffusionApplication');
}
protected function getObjectPattern() {
return 'COMMIT[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)preg_replace('/^COMMIT/', '', $pattern);
+ $id = (int)preg_replace('/^COMMIT/i', '', $pattern);
return id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIDs(array($id))
->needAuditRequests(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorAuditReplyHandler();
}
}
diff --git a/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php b/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php
index 8536907486..01a0367234 100644
--- a/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php
+++ b/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorCalendarEventMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorCalendarApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'E[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'E');
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorCalendarReplyHandler();
}
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index e597c52897..12b9463166 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,402 +1,406 @@
<?php
final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$ancient_config = self::getAncientConfig();
$all_keys = PhabricatorEnv::getAllConfigKeys();
$all_keys = array_keys($all_keys);
sort($all_keys);
$defined_keys = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($all_keys as $key) {
if (isset($defined_keys[$key])) {
continue;
}
if (isset($ancient_config[$key])) {
$summary = pht(
'This option has been removed. You may delete it at your '.
'convenience.');
$message = pht(
"The configuration option '%s' has been removed. You may delete ".
"it at your convenience.".
"\n\n%s",
$key,
$ancient_config[$key]);
$short = pht('Obsolete Config');
$name = pht('Obsolete Configuration Option "%s"', $key);
} else {
$summary = pht('This option is not recognized. It may be misspelled.');
$message = pht(
"The configuration option '%s' is not recognized. It may be ".
"misspelled, or it might have existed in an older version of ".
"Phabricator. It has no effect, and should be corrected or deleted.",
$key);
$short = pht('Unknown Config');
$name = pht('Unknown Configuration Option "%s"', $key);
}
$issue = $this->newIssue('config.unknown.'.$key)
->setShortName($short)
->setName($name)
->setSummary($summary);
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
$found = array();
$found_local = false;
$found_database = false;
foreach ($stack as $source_key => $source) {
$value = $source->getKeys(array($key));
if ($value) {
$found[] = $source->getName();
if ($source instanceof PhabricatorConfigDatabaseSource) {
$found_database = true;
}
if ($source instanceof PhabricatorConfigLocalSource) {
$found_local = true;
}
}
}
$message = $message."\n\n".pht(
'This configuration value is defined in these %d '.
'configuration source(s): %s.',
count($found),
implode(', ', $found));
$issue->setMessage($message);
if ($found_local) {
$command = csprintf('phabricator/ $ ./bin/config delete %s', $key);
$issue->addCommand($command);
}
if ($found_database) {
$issue->addPhabricatorConfig($key);
}
}
}
/**
* Return a map of deleted config options. Keys are option keys; values are
* explanations of what happened to the option.
*/
public static function getAncientConfig() {
$reason_auth = pht(
'This option has been migrated to the "Auth" application. Your old '.
'configuration is still in effect, but now stored in "Auth" instead of '.
'configuration. Going forward, you can manage authentication from '.
'the web UI.');
$auth_config = array(
'controller.oauth-registration',
'auth.password-auth-enabled',
'facebook.auth-enabled',
'facebook.registration-enabled',
'facebook.auth-permanent',
'facebook.application-id',
'facebook.application-secret',
'facebook.require-https-auth',
'github.auth-enabled',
'github.registration-enabled',
'github.auth-permanent',
'github.application-id',
'github.application-secret',
'google.auth-enabled',
'google.registration-enabled',
'google.auth-permanent',
'google.application-id',
'google.application-secret',
'ldap.auth-enabled',
'ldap.hostname',
'ldap.port',
'ldap.base_dn',
'ldap.search_attribute',
'ldap.search-first',
'ldap.username-attribute',
'ldap.real_name_attributes',
'ldap.activedirectory_domain',
'ldap.version',
'ldap.referrals',
'ldap.anonymous-user-name',
'ldap.anonymous-user-password',
'ldap.start-tls',
'disqus.auth-enabled',
'disqus.registration-enabled',
'disqus.auth-permanent',
'disqus.application-id',
'disqus.application-secret',
'phabricator.oauth-uri',
'phabricator.auth-enabled',
'phabricator.registration-enabled',
'phabricator.auth-permanent',
'phabricator.application-id',
'phabricator.application-secret',
);
$ancient_config = array_fill_keys($auth_config, $reason_auth);
$markup_reason = pht(
'Custom remarkup rules are now added by subclassing '.
'%s or %s.',
'PhabricatorRemarkupCustomInlineRule',
'PhabricatorRemarkupCustomBlockRule');
$session_reason = pht(
'Sessions now expire and are garbage collected rather than having an '.
'arbitrary concurrency limit.');
$differential_field_reason = pht(
'All Differential fields are now managed through the configuration '.
'option "%s". Use that option to configure which fields are shown.',
'differential.fields');
$reply_domain_reason = pht(
'Individual application reply handler domains have been removed. '.
'Configure a reply domain with "%s".',
'metamta.reply-handler-domain');
$reply_handler_reason = pht(
'Reply handlers can no longer be overridden with configuration.');
$monospace_reason = pht(
'Phabricator no longer supports global customization of monospaced '.
'fonts.');
$public_mail_reason = pht(
'Inbound mail addresses are now configured for each application '.
'in the Applications tool.');
$gc_reason = pht(
'Garbage collectors are now configured with "%s".',
'bin/garbage set-policy');
$aphlict_reason = pht(
'Configuration of the notification server has changed substantially. '.
'For discussion, see T10794.');
$stale_reason = pht(
'The Differential revision list view age UI elements have been removed '.
'to simplify the interface.');
$global_settings_reason = pht(
'The "Re: Prefix" and "Vary Subjects" settings are now configured '.
'in global settings.');
$dashboard_reason = pht(
'This option has been removed, you can use Dashboards to provide '.
'homepage customization. See T11533 for more details.');
$elastic_reason = pht(
'Elasticsearch is now configured with "%s".',
'cluster.search');
$mailers_reason = pht(
'Inbound and outbound mail is now configured with "cluster.mailers".');
$ancient_config += array(
'phid.external-loaders' =>
pht(
'External loaders have been replaced. Extend `%s` '.
'to implement new PHID and handle types.',
'PhabricatorPHIDType'),
'maniphest.custom-task-extensions-class' =>
pht(
'Maniphest fields are now loaded automatically. '.
'You can configure them with `%s`.',
'maniphest.fields'),
'maniphest.custom-fields' =>
pht(
'Maniphest fields are now defined in `%s`. '.
'Existing definitions have been migrated.',
'maniphest.custom-field-definitions'),
'differential.custom-remarkup-rules' => $markup_reason,
'differential.custom-remarkup-block-rules' => $markup_reason,
'auth.sshkeys.enabled' => pht(
'SSH keys are now actually useful, so they are always enabled.'),
'differential.anonymous-access' => pht(
'Phabricator now has meaningful global access controls. See `%s`.',
'policy.allow-public'),
'celerity.resource-path' => pht(
'An alternate resource map is no longer supported. Instead, use '.
'multiple maps. See T4222.'),
'metamta.send-immediately' => pht(
'Mail is now always delivered by the daemons.'),
'auth.sessions.conduit' => $session_reason,
'auth.sessions.web' => $session_reason,
'tokenizer.ondemand' => pht(
'Phabricator now manages typeahead strategies automatically.'),
'differential.revision-custom-detail-renderer' => pht(
'Obsolete; use standard rendering events instead.'),
'differential.show-host-field' => $differential_field_reason,
'differential.show-test-plan-field' => $differential_field_reason,
'differential.field-selector' => $differential_field_reason,
'phabricator.show-beta-applications' => pht(
'This option has been renamed to `%s` to emphasize the '.
'unfinished nature of many prototype applications. '.
'Your existing setting has been migrated.',
'phabricator.show-prototypes'),
'notification.user' => pht(
'The notification server no longer requires root permissions. Start '.
'the server as the user you want it to run under.'),
'notification.debug' => pht(
'Notifications no longer have a dedicated debugging mode.'),
'translation.provider' => pht(
'The translation implementation has changed and providers are no '.
'longer used or supported.'),
'config.mask' => pht(
'Use `%s` instead of this option.',
'config.hide'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
'pool size with `%s`.',
'phd.taskmasters'),
'storage.engine-selector' => pht(
'Phabricator now automatically discovers available storage engines '.
'at runtime.'),
'storage.upload-size-limit' => pht(
'Phabricator now supports arbitrarily large files. Consult the '.
'documentation for configuration details.'),
'security.allow-outbound-http' => pht(
'This option has been replaced with the more granular option `%s`.',
'security.outbound-blacklist'),
'metamta.reply.show-hints' => pht(
'Phabricator no longer shows reply hints in mail.'),
'metamta.differential.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler-domain' => $reply_domain_reason,
'metamta.macro.reply-handler-domain' => $reply_domain_reason,
'metamta.maniphest.reply-handler-domain' => $reply_domain_reason,
'metamta.pholio.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler' => $reply_handler_reason,
'metamta.differential.reply-handler' => $reply_handler_reason,
'metamta.maniphest.reply-handler' => $reply_handler_reason,
'metamta.package.reply-handler' => $reply_handler_reason,
'metamta.precedence-bulk' => pht(
'Phabricator now always sends transaction mail with '.
'"Precedence: bulk" to improve deliverability.'),
'style.monospace' => $monospace_reason,
'style.monospace.windows' => $monospace_reason,
'search.engine-selector' => pht(
'Phabricator now automatically discovers available search engines '.
'at runtime.'),
'metamta.files.public-create-email' => $public_mail_reason,
'metamta.maniphest.public-create-email' => $public_mail_reason,
'metamta.maniphest.default-public-author' => $public_mail_reason,
'metamta.paste.public-create-email' => $public_mail_reason,
'security.allow-conduit-act-as-user' => pht(
'Impersonating users over the API is no longer supported.'),
'feed.public' => pht('The framable public feed is no longer supported.'),
'auth.login-message' => pht(
'This configuration option has been replaced with a modular '.
'handler. See T9346.'),
'gcdaemon.ttl.herald-transcripts' => $gc_reason,
'gcdaemon.ttl.daemon-logs' => $gc_reason,
'gcdaemon.ttl.differential-parse-cache' => $gc_reason,
'gcdaemon.ttl.markup-cache' => $gc_reason,
'gcdaemon.ttl.task-archive' => $gc_reason,
'gcdaemon.ttl.general-cache' => $gc_reason,
'gcdaemon.ttl.conduit-logs' => $gc_reason,
'phd.variant-config' => pht(
'This configuration is no longer relevant because daemons '.
'restart automatically on configuration changes.'),
'notification.ssl-cert' => $aphlict_reason,
'notification.ssl-key' => $aphlict_reason,
'notification.pidfile' => $aphlict_reason,
'notification.log' => $aphlict_reason,
'notification.enabled' => $aphlict_reason,
'notification.client-uri' => $aphlict_reason,
'notification.server-uri' => $aphlict_reason,
'metamta.differential.unified-comment-context' => pht(
'Inline comments are now always rendered with a limited amount '.
'of context.'),
'differential.days-fresh' => $stale_reason,
'differential.days-stale' => $stale_reason,
'metamta.re-prefix' => $global_settings_reason,
'metamta.vary-subjects' => $global_settings_reason,
'ui.custom-header' => pht(
'This option has been replaced with `ui.logo`, which provides more '.
'flexible configuration options.'),
'welcome.html' => $dashboard_reason,
'maniphest.priorities.unbreak-now' => $dashboard_reason,
'maniphest.priorities.needs-triage' => $dashboard_reason,
'mysql.implementation' => pht(
'Phabricator now automatically selects the best available '.
'MySQL implementation.'),
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
'search.elastic.host' => $elastic_reason,
'search.elastic.namespace' => $elastic_reason,
'metamta.mail-adapter' => $mailers_reason,
'amazon-ses.access-key' => $mailers_reason,
'amazon-ses.secret-key' => $mailers_reason,
'amazon-ses.endpoint' => $mailers_reason,
'mailgun.domain' => $mailers_reason,
'mailgun.api-key' => $mailers_reason,
'phpmailer.mailer' => $mailers_reason,
'phpmailer.smtp-host' => $mailers_reason,
'phpmailer.smtp-port' => $mailers_reason,
'phpmailer.smtp-protocol' => $mailers_reason,
'phpmailer.smtp-user' => $mailers_reason,
'phpmailer.smtp-password' => $mailers_reason,
'phpmailer.smtp-encoding' => $mailers_reason,
'sendgrid.api-user' => $mailers_reason,
'sendgrid.api-key' => $mailers_reason,
'celerity.resource-hash' => pht(
'This option generally did not prove useful. Resource hash keys '.
'are now managed automatically.'),
'celerity.enable-deflate' => pht(
'Resource deflation is now managed automatically.'),
'celerity.minify' => pht(
'Resource minification is now managed automatically.'),
'metamta.domain' => pht(
'Mail thread IDs are now generated automatically.'),
'metamta.placeholder-to-recipient' => pht(
'Placeholder recipients are now generated automatically.'),
'metamta.mail-key' => pht(
'Mail object address hash keys are now generated automatically.'),
'phabricator.csrf-key' => pht(
'CSRF HMAC keys are now managed automatically.'),
'metamta.insecure-auth-with-reply-to' => pht(
'Authenticating users based on "Reply-To" is no longer supported.'),
+
+ 'phabricator.allow-email-users' => pht(
+ 'Public email is now accepted if the associated address has a '.
+ 'default author, and rejected otherwise.'),
);
return $ancient_config;
}
}
diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php
index 08266217ea..48f6f24491 100644
--- a/src/applications/config/option/PhabricatorCoreConfigOptions.php
+++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php
@@ -1,324 +1,316 @@
<?php
final class PhabricatorCoreConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Core');
}
public function getDescription() {
return pht('Configure core options, including URIs.');
}
public function getIcon() {
return 'fa-bullseye';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
if (phutil_is_windows()) {
$paths = array();
} else {
$paths = array(
'/bin',
'/usr/bin',
'/usr/local/bin',
);
}
$path = getenv('PATH');
$proto_doc_href = PhabricatorEnv::getDoclink(
'User Guide: Prototype Applications');
$proto_doc_name = pht('User Guide: Prototype Applications');
$applications_app_href = '/applications/';
$silent_description = $this->deformat(pht(<<<EOREMARKUP
This option allows you to stop Phabricator from sending data to most external
services: it will disable email, SMS, repository mirroring, remote builds,
Doorkeeper writes, and webhooks.
This option is intended to allow a Phabricator instance to be exported, copied,
imported, and run in a test environment without impacting users. For example,
if you are migrating to new hardware, you could perform a test migration first
with this flag set, make sure things work, and then do a production cutover
later with higher confidence and less disruption.
Without making use of this flag to silence the temporary test environment,
users would receive duplicate email during the time the test instance and old
production instance were both in operation.
EOREMARKUP
));
return array(
$this->newOption('phabricator.base-uri', 'string', null)
->setLocked(true)
->setSummary(pht('URI where Phabricator is installed.'))
->setDescription(
pht(
'Set the URI where Phabricator is installed. Setting this '.
'improves security by preventing cookies from being set on other '.
'domains, and allows daemons to send emails with links that have '.
'the correct domain.'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.production-uri', 'string', null)
->setSummary(
pht('Primary install URI, for multi-environment installs.'))
->setDescription(
pht(
'If you have multiple Phabricator environments (like a '.
'development/staging environment for working on testing '.
'Phabricator, and a production environment for deploying it), '.
'set the production environment URI here so that emails and other '.
'durable URIs will always generate with links pointing at the '.
'production environment. If unset, defaults to `%s`. Most '.
'installs do not need to set this option.',
'phabricator.base-uri'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.allowed-uris', 'list<string>', array())
->setLocked(true)
->setSummary(pht('Alternative URIs that can access Phabricator.'))
->setDescription(
pht(
"These alternative URIs will be able to access 'normal' pages ".
"on your Phabricator install. Other features such as OAuth ".
"won't work. The major use case for this is moving installs ".
"across domains."))
->addExample(
"http://phabricator2.example.com/\n".
"http://phabricator3.example.com/",
pht('Valid Setting')),
$this->newOption('phabricator.timezone', 'string', null)
->setSummary(
pht('The timezone Phabricator should use.'))
->setDescription(
pht(
"PHP requires that you set a timezone in your php.ini before ".
"using date functions, or it will emit a warning. If this isn't ".
"possible (for instance, because you are using HPHP) you can set ".
"some valid constant for %s here and Phabricator will set it on ".
"your behalf, silencing the warning.",
'date_default_timezone_set()'))
->addExample('America/New_York', pht('US East (EDT)'))
->addExample('America/Chicago', pht('US Central (CDT)'))
->addExample('America/Boise', pht('US Mountain (MDT)'))
->addExample('America/Los_Angeles', pht('US West (PDT)')),
$this->newOption('phabricator.cookie-prefix', 'string', null)
->setLocked(true)
->setSummary(
pht(
'Set a string Phabricator should use to prefix cookie names.'))
->setDescription(
pht(
'Cookies set for x.com are also sent for y.x.com. Assuming '.
'Phabricator instances are running on both domains, this will '.
'create a collision preventing you from logging in.'))
->addExample('dev', pht('Prefix cookie with "%s"', 'dev')),
$this->newOption('phabricator.show-prototypes', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Enable Prototypes'),
pht('Disable Prototypes'),
))
->setSummary(
pht(
'Install applications which are still under development.'))
->setDescription(
pht(
"IMPORTANT: The upstream does not provide support for prototype ".
"applications.".
"\n\n".
"Phabricator includes prototype applications which are in an ".
"**early stage of development**. By default, prototype ".
"applications are not installed, because they are often not yet ".
"developed enough to be generally usable. You can enable ".
"this option to install them if you're developing Phabricator ".
"or are interested in previewing upcoming features.".
"\n\n".
"To learn more about prototypes, see [[ %s | %s ]].".
"\n\n".
"After enabling prototypes, you can selectively uninstall them ".
"(like normal applications).",
$proto_doc_href,
$proto_doc_name)),
$this->newOption('phabricator.serious-business', 'bool', false)
->setBoolOptions(
array(
pht('Serious business'),
pht('Shenanigans'), // That should be interesting to translate. :P
))
->setSummary(
pht('Allows you to remove levity and jokes from the UI.'))
->setDescription(
pht(
'By default, Phabricator includes some flavor text in the UI, '.
'like a prompt to "Weigh In" rather than "Add Comment" in '.
'Maniphest. If you\'d prefer more traditional UI strings like '.
'"Add Comment", you can set this flag to disable most of the '.
'extra flavor.')),
$this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/')
->setSummary(
pht('Text values that match this regex and are also object names '.
'will not be linked.'))
->setDescription(
pht(
'By default, Phabricator links object names in Remarkup fields '.
'to the corresponding object. This regex can be used to modify '.
'this behavior; object names that match this regex will not be '.
'linked.')),
$this->newOption('environment.append-paths', 'list<string>', $paths)
->setSummary(
pht(
'These paths get appended to your %s environment variable.',
'$PATH'))
->setDescription(
pht(
"Phabricator occasionally shells out to other binaries on the ".
"server. An example of this is the `%s` command, used to ".
"syntax-highlight code written in languages other than PHP. By ".
"default, it is assumed that these binaries are in the %s of the ".
"user running Phabricator (normally 'apache', 'httpd', or ".
"'nobody'). Here you can add extra directories to the %s ".
"environment variable, for when these binaries are in ".
"non-standard locations.\n\n".
"Note that you can also put binaries in `%s` (for example, by ".
"symlinking them).\n\n".
"The current value of PATH after configuration is applied is:\n\n".
" lang=text\n".
" %s",
'pygmentize',
'$PATH',
'$PATH',
'phabricator/support/bin/',
$path))
->setLocked(true)
->addExample('/usr/local/bin', pht('Add One Path'))
->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')),
$this->newOption('config.lock', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to lock.')),
$this->newOption('config.hide', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to hide.')),
$this->newOption('config.ignore-issues', 'set', array())
->setLocked(true)
->setDescription(pht('Setup issues to ignore.')),
$this->newOption('phabricator.env', 'string', null)
->setLocked(true)
->setDescription(pht('Internal.')),
$this->newOption('test.value', 'wild', null)
->setLocked(true)
->setDescription(pht('Unit test value.')),
$this->newOption('phabricator.uninstalled-applications', 'set', array())
->setLocked(true)
->setLockedMessage(pht(
'Use the %s to manage installed applications.',
phutil_tag(
'a',
array(
'href' => $applications_app_href,
),
pht('Applications application'))))
->setDescription(
pht('Array containing list of uninstalled applications.')),
$this->newOption('phabricator.application-settings', 'wild', array())
->setLocked(true)
->setDescription(
pht('Customized settings for Phabricator applications.')),
$this->newOption('phabricator.cache-namespace', 'string', 'phabricator')
->setLocked(true)
->setDescription(pht('Cache namespace.')),
- $this->newOption('phabricator.allow-email-users', 'bool', false)
- ->setBoolOptions(
- array(
- pht('Allow'),
- pht('Disallow'),
- ))
- ->setDescription(
- pht('Allow non-members to interact with tasks over email.')),
$this->newOption('phabricator.silent', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Run Silently'),
pht('Run Normally'),
))
->setSummary(pht('Stop Phabricator from sending any email, etc.'))
->setDescription($silent_description),
);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
$key = $option->getKey();
if ($key == 'phabricator.base-uri' ||
$key == 'phabricator.production-uri') {
$uri = new PhutilURI($value);
$protocol = $uri->getProtocol();
if ($protocol !== 'http' && $protocol !== 'https') {
throw new PhabricatorConfigValidationException(
pht(
'Config option "%s" is invalid. The URI must start with '.
'"%s" or "%s".',
$key,
'http://',
'https://'));
}
$domain = $uri->getDomain();
if (strpos($domain, '.') === false) {
throw new PhabricatorConfigValidationException(
pht(
'Config option "%s" is invalid. The URI must contain a dot '.
'("%s"), like "%s", not just a bare name like "%s". Some web '.
'browsers will not set cookies on domains with no TLD.',
$key,
'.',
'http://example.com/',
'http://example/'));
}
$path = $uri->getPath();
if ($path !== '' && $path !== '/') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must NOT have a path, ".
"e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ".
"on an entire domain; it can not be installed on a path.",
$key,
'http://phabricator.example.com/',
'http://example.com/phabricator/'));
}
}
if ($key === 'phabricator.timezone') {
$old = date_default_timezone_get();
$ok = @date_default_timezone_set($value);
@date_default_timezone_set($old);
if (!$ok) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The timezone identifier must ".
"be a valid timezone identifier recognized by PHP, like '%s'. "."
You can find a list of valid identifiers here: %s",
$key,
'America/Los_Angeles',
'http://php.net/manual/timezones.php'));
}
}
}
}
diff --git a/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php b/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php
index e926d3cfc5..aeb4e28997 100644
--- a/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php
+++ b/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class ConpherenceThreadMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorConpherenceApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'Z[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'Z');
+ $id = (int)substr($pattern, 1);
return id(new ConpherenceThreadQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new ConpherenceReplyHandler();
}
}
diff --git a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php
index d0218de59b..9b448ef10f 100644
--- a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php
+++ b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorCountdownMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorCountdownApplication');
}
protected function getObjectPattern() {
return 'C[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)substr($pattern, 4);
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorCountdownQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorCountdownReplyHandler();
}
}
diff --git a/src/applications/differential/mail/DifferentialCreateMailReceiver.php b/src/applications/differential/mail/DifferentialCreateMailReceiver.php
index cfd7470099..0d4fd1a2be 100644
--- a/src/applications/differential/mail/DifferentialCreateMailReceiver.php
+++ b/src/applications/differential/mail/DifferentialCreateMailReceiver.php
@@ -1,119 +1,121 @@
<?php
final class DifferentialCreateMailReceiver
extends PhabricatorApplicationMailReceiver {
protected function newApplication() {
return new PhabricatorDifferentialApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+
+ $author = $this->getAuthor();
$attachments = $mail->getAttachments();
$files = array();
$errors = array();
if ($attachments) {
$files = id(new PhabricatorFileQuery())
- ->setViewer($sender)
+ ->setViewer($author)
->withPHIDs($attachments)
->execute();
foreach ($files as $index => $file) {
if ($file->getMimeType() != 'text/plain') {
$errors[] = pht(
'Could not parse file %s; only files with mimetype text/plain '.
'can be parsed via email.',
$file->getName());
unset($files[$index]);
}
}
}
$diffs = array();
foreach ($files as $file) {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $file->loadFileData(),
));
- $call->setUser($sender);
+ $call->setUser($author);
try {
$result = $call->execute();
$diffs[$file->getName()] = $result['uri'];
} catch (Exception $e) {
$errors[] = pht(
'Could not parse attachment %s; only attachments (and mail bodies) '.
'generated via "diff" commands can be parsed.',
$file->getName());
}
}
$body = $mail->getCleanTextBody();
if ($body) {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $body,
));
- $call->setUser($sender);
+ $call->setUser($author);
try {
$result = $call->execute();
$diffs[pht('Mail Body')] = $result['uri'];
} catch (Exception $e) {
$errors[] = pht(
'Could not parse mail body; only mail bodies (and attachments) '.
'generated via "diff" commands can be parsed.');
}
}
$subject_prefix =
PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
if (count($diffs)) {
$subject = pht(
'You successfully created %d diff(s).',
count($diffs));
} else {
$subject = pht(
'Diff creation failed; see body for %s error(s).',
phutil_count($errors));
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
if (count($diffs)) {
$text_body = '';
$html_body = array();
$body_label = pht('%s DIFF LINK(S)', phutil_count($diffs));
foreach ($diffs as $filename => $diff_uri) {
$text_body .= $filename.': '.$diff_uri."\n";
$html_body[] = phutil_tag(
'a',
array(
'href' => $diff_uri,
),
$filename);
$html_body[] = phutil_tag('br');
}
$body->addTextSection($body_label, $text_body);
$body->addHTMLSection($body_label, $html_body);
}
if (count($errors)) {
$body_section = new PhabricatorMetaMTAMailSection();
$body_label = pht('%s ERROR(S)', phutil_count($errors));
foreach ($errors as $error) {
$body_section->addFragment($error);
}
$body->addTextSection($body_label, $body_section);
}
id(new PhabricatorMetaMTAMail())
- ->addTos(array($sender->getPHID()))
+ ->addTos(array($author->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
- ->setFrom($sender->getPHID())
+ ->setFrom($author->getPHID())
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
index 929ee72647..0fe8583a1d 100644
--- a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
+++ b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
@@ -1,31 +1,31 @@
<?php
final class DifferentialRevisionMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorDifferentialApplication');
}
protected function getObjectPattern() {
return 'D[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'D');
+ $id = (int)substr($pattern, 1);
return id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($id))
->needReviewers(true)
->needReviewerAuthority(true)
->needActiveDiffs(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new DifferentialReplyHandler();
}
}
diff --git a/src/applications/files/mail/FileCreateMailReceiver.php b/src/applications/files/mail/FileCreateMailReceiver.php
index 2cf946aea8..fe314f1f0a 100644
--- a/src/applications/files/mail/FileCreateMailReceiver.php
+++ b/src/applications/files/mail/FileCreateMailReceiver.php
@@ -1,53 +1,59 @@
<?php
final class FileCreateMailReceiver
extends PhabricatorApplicationMailReceiver {
protected function newApplication() {
return new PhabricatorFilesApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+ $author = $this->getAuthor();
$attachment_phids = $mail->getAttachments();
if (empty($attachment_phids)) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION,
pht(
'Ignoring email to create files that did not include attachments.'));
}
$first_phid = head($attachment_phids);
$mail->setRelatedPHID($first_phid);
+ $sender = $this->getSender();
+ if (!$sender) {
+ return;
+ }
+
$attachment_count = count($attachment_phids);
if ($attachment_count > 1) {
$subject = pht('You successfully uploaded %d files.', $attachment_count);
} else {
$subject = pht('You successfully uploaded a file.');
}
$subject_prefix =
PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix');
$file_uris = array();
foreach ($attachment_phids as $phid) {
$file_uris[] =
PhabricatorEnv::getProductionURI('/file/info/'.$phid.'/');
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
$body->addTextSection(pht('FILE LINKS'), implode("\n", $file_uris));
id(new PhabricatorMetaMTAMail())
->addTos(array($sender->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
->setFrom($sender->getPHID())
->setRelatedPHID($first_phid)
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/files/mail/FileMailReceiver.php b/src/applications/files/mail/FileMailReceiver.php
index cdad22c5ce..f4074d9ba0 100644
--- a/src/applications/files/mail/FileMailReceiver.php
+++ b/src/applications/files/mail/FileMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class FileMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorFilesApplication');
}
protected function getObjectPattern() {
return 'F[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'F');
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new FileReplyHandler();
}
}
diff --git a/src/applications/legalpad/mail/LegalpadMailReceiver.php b/src/applications/legalpad/mail/LegalpadMailReceiver.php
index 5fe0dfd4e8..679812e4ad 100644
--- a/src/applications/legalpad/mail/LegalpadMailReceiver.php
+++ b/src/applications/legalpad/mail/LegalpadMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class LegalpadMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorLegalpadApplication');
}
protected function getObjectPattern() {
return 'L[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'L');
+ $id = (int)substr($pattern, 1);
return id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($id))
->needDocumentBodies(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new LegalpadReplyHandler();
}
}
diff --git a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
index 64bdaa2ed5..22c09fdf60 100644
--- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
@@ -1,31 +1,36 @@
<?php
final class ManiphestCreateMailReceiver
extends PhabricatorApplicationMailReceiver {
protected function newApplication() {
return new PhabricatorManiphestApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
- $task = ManiphestTask::initializeNewTask($sender);
- $task->setOriginalEmailSource($mail->getHeader('From'));
+ $author = $this->getAuthor();
+ $task = ManiphestTask::initializeNewTask($author);
+
+ $from_address = $mail->newFromAddress();
+ if ($from_address) {
+ $task->setOriginalEmailSource((string)$from_address);
+ }
$handler = new ManiphestReplyHandler();
$handler->setMailReceiver($task);
- $handler->setActor($sender);
+ $handler->setActor($author);
$handler->setExcludeMailRecipientPHIDs(
$mail->loadAllRecipientPHIDs());
if ($this->getApplicationEmail()) {
$handler->setApplicationEmail($this->getApplicationEmail());
}
$handler->processEmail($mail);
$mail->setRelatedPHID($task->getPHID());
}
}
diff --git a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
index 220a888e8e..54ac72fd57 100644
--- a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
@@ -1,29 +1,29 @@
<?php
final class ManiphestTaskMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorManiphestApplication');
}
protected function getObjectPattern() {
return 'T[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'T');
+ $id = (int)substr($pattern, 1);
return id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new ManiphestReplyHandler();
}
}
diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
index afde9fee23..c13835e6f4 100644
--- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
+++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
@@ -1,408 +1,423 @@
<?php
final class PhabricatorMetaMTAApplicationEmailPanel
extends PhabricatorApplicationConfigurationPanel {
public function getPanelKey() {
return 'email';
}
public function shouldShowForApplication(
PhabricatorApplication $application) {
return $application->supportsEmailIntegration();
}
public function buildConfigurationPagePanel() {
$viewer = $this->getViewer();
$application = $this->getApplication();
$table = $this->buildEmailTable($is_edit = false, null);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
PhabricatorPolicyCapability::CAN_EDIT);
$header = id(new PHUIHeaderView())
->setHeader(pht('Application Emails'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Edit Application Emails'))
->setIcon('fa-pencil')
->setHref($this->getPanelURI())
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
return $box;
}
public function handlePanelRequest(
AphrontRequest $request,
PhabricatorController $controller) {
$viewer = $request->getViewer();
$application = $this->getApplication();
$path = $request->getURIData('path');
if (strlen($path)) {
return new Aphront404Response();
}
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
$new = $request->getStr('new');
$edit = $request->getInt('edit');
$delete = $request->getInt('delete');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $application);
}
if ($edit) {
return $this->returnEditAddressResponse($request, $uri, $edit);
}
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
$table = $this->buildEmailTable(
$is_edit = true,
$request->getInt('id'));
$form = id(new AphrontFormView())
->setUser($viewer);
$crumbs = $controller->buildPanelCrumbs($this);
$crumbs->addTextCrumb(pht('Edit Application Emails'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Application Emails: %s', $application->getName()))
->setSubheader($application->getAppEmailBlurb())
->setHeaderIcon('fa-pencil');
$icon = id(new PHUIIconView())
->setIcon('fa-plus');
$button = new PHUIButtonView();
$button->setText(pht('Add New Address'));
$button->setTag('a');
$button->setHref($uri->alter('new', 'true'));
$button->setIcon($icon);
$button->addSigil('workflow');
$header->addActionLink($button);
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Emails'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
$title = $application->getName();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($object_box);
return $controller->buildPanelPage(
$this,
$title,
$crumbs,
$view);
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorApplication $application) {
$viewer = $request->getUser();
$email_object =
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail($viewer)
->setApplicationPHID($application->getPHID());
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = true);
}
private function returnEditAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $request->getUser();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = false);
}
private function returnSaveAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorMetaMTAApplicationEmail $email_object,
$is_new) {
$viewer = $request->getUser();
$config_default =
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
$e_email = true;
$v_email = $email_object->getAddress();
$e_space = null;
$v_space = $email_object->getSpacePHID();
$v_default = $email_object->getConfigValue($config_default);
$validation_exception = null;
if ($request->isDialogFormPost()) {
$e_email = null;
$v_email = trim($request->getStr('email'));
$v_space = $request->getStr('spacePHID');
$v_default = $request->getArr($config_default);
$v_default = nonempty(head($v_default), null);
$type_address =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
$type_space = PhabricatorTransactions::TYPE_SPACE;
$type_config =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
$key_config = PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG;
$xactions = array();
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_address)
->setNewValue($v_email);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_space)
->setNewValue($v_space);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_config)
->setMetadataValue($key_config, $config_default)
->setNewValue($v_default);
$editor = id(new PhabricatorMetaMTAApplicationEmailEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($email_object, $xactions);
return id(new AphrontRedirectResponse())->setURI(
$uri->alter('highlight', $email_object->getID()));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_email = $ex->getShortMessage($type_address);
$e_space = $ex->getShortMessage($type_space);
}
}
if ($v_default) {
$v_default = array($v_default);
} else {
$v_default = array();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($v_email)
->setError($e_email));
if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
$form->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Space'))
->setName('spacePHID')
->setValue($v_space)
->setError($e_space)
->setOptions(
PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
$viewer,
$v_space)));
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setLabel(pht('Default Author'))
->setName($config_default)
->setLimit(1)
->setValue($v_default)
- ->setCaption(pht(
- 'Used if the "From:" address does not map to a known account.')));
+ ->setCaption(
+ pht(
+ 'Used if the "From:" address does not map to a user account. '.
+ 'Setting a default author will allow anyone on the public '.
+ 'internet to create objects in Phabricator by sending email to '.
+ 'this address.')));
if ($is_new) {
$title = pht('New Address');
} else {
$title = pht('Edit Address');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->setValidationException($validation_exception)
->appendForm($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
if ($is_new) {
$dialog->addHiddenInput('new', 'true');
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $this->getViewer();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
$engine = new PhabricatorDestructionEngine();
$engine->destroyObject($email_object);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $email_object_id)
->setTitle(pht('Delete Address'))
->appendParagraph(pht(
'Are you sure you want to delete this email address?'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function buildEmailTable($is_edit, $highlight) {
$viewer = $this->getViewer();
$application = $this->getApplication();
$uri = new PhutilURI($this->getPanelURI());
$emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withApplicationPHIDs(array($application->getPHID()))
->execute();
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_edit = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('edit', $email->getID()),
'sigil' => 'workflow',
),
pht('Edit'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Delete'));
if ($highlight == $email->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($email);
if ($space_phid) {
$email_space = $viewer->renderHandle($space_phid);
} else {
$email_space = null;
}
+ $default_author_phid = $email->getDefaultAuthorPHID();
+ if (!$default_author_phid) {
+ $default_author = phutil_tag('em', array(), pht('None'));
+ } else {
+ $default_author = $viewer->renderHandle($default_author_phid);
+ }
+
$rows[] = array(
$email_space,
$email->getAddress(),
+ $default_author,
$button_edit,
$button_remove,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No application emails created yet.'));
$table->setHeaders(
array(
pht('Space'),
pht('Email'),
+ pht('Default'),
pht('Edit'),
pht('Delete'),
));
$table->setColumnClasses(
array(
+ '',
'',
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer),
true,
+ true,
$is_edit,
$is_edit,
));
return $table;
}
}
diff --git a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
index 965bdbdbfe..2ec44b847a 100644
--- a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
+++ b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
@@ -1,42 +1,40 @@
<?php
final class MetaMTAReceivedMailStatus
extends Phobject {
const STATUS_DUPLICATE = 'err:duplicate';
const STATUS_FROM_PHABRICATOR = 'err:self';
const STATUS_NO_RECEIVERS = 'err:no-receivers';
- const STATUS_ABUNDANT_RECEIVERS = 'err:multiple-receivers';
const STATUS_UNKNOWN_SENDER = 'err:unknown-sender';
const STATUS_DISABLED_SENDER = 'err:disabled-sender';
const STATUS_NO_PUBLIC_MAIL = 'err:no-public-mail';
const STATUS_USER_MISMATCH = 'err:bad-user';
const STATUS_POLICY_PROBLEM = 'err:policy';
const STATUS_NO_SUCH_OBJECT = 'err:not-found';
const STATUS_HASH_MISMATCH = 'err:bad-hash';
const STATUS_UNHANDLED_EXCEPTION = 'err:exception';
const STATUS_EMPTY = 'err:empty';
const STATUS_EMPTY_IGNORED = 'err:empty-ignored';
public static function getHumanReadableName($status) {
$map = array(
self::STATUS_DUPLICATE => pht('Duplicate Message'),
self::STATUS_FROM_PHABRICATOR => pht('Phabricator Mail'),
self::STATUS_NO_RECEIVERS => pht('No Receivers'),
- self::STATUS_ABUNDANT_RECEIVERS => pht('Multiple Receivers'),
self::STATUS_UNKNOWN_SENDER => pht('Unknown Sender'),
self::STATUS_DISABLED_SENDER => pht('Disabled Sender'),
self::STATUS_NO_PUBLIC_MAIL => pht('No Public Mail'),
self::STATUS_USER_MISMATCH => pht('User Mismatch'),
self::STATUS_POLICY_PROBLEM => pht('Policy Error'),
self::STATUS_NO_SUCH_OBJECT => pht('No Such Object'),
self::STATUS_HASH_MISMATCH => pht('Bad Address'),
self::STATUS_UNHANDLED_EXCEPTION => pht('Unhandled Exception'),
self::STATUS_EMPTY => pht('Empty Mail'),
self::STATUS_EMPTY_IGNORED => pht('Ignored Empty Mail'),
);
return idx($map, $status, pht('Processing Exception'));
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
index 46c444571b..5c17b11321 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
@@ -1,166 +1,180 @@
<?php
final class PhabricatorMailManagementReceiveTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('receive-test')
->setSynopsis(
pht(
'Simulate receiving mail. This is primarily useful if you are '.
'developing new mail receivers.'))
->setExamples(
'**receive-test** --as alincoln --to D123 < body.txt')
->setArguments(
array(
array(
'name' => 'as',
'param' => 'user',
'help' => pht('Act as the specified user.'),
),
array(
'name' => 'from',
'param' => 'email',
'help' => pht('Simulate mail delivery "From:" the given user.'),
),
array(
'name' => 'to',
'param' => 'object',
'help' => pht('Simulate mail delivery "To:" the given object.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
$console = PhutilConsole::getConsole();
$to = $args->getArg('to');
if (!$to) {
throw new PhutilArgumentUsageException(
pht(
"Use '%s' to specify the receiving object or email address.",
'--to'));
}
$to_application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($this->getViewer())
->withAddresses(array($to))
->executeOne();
$as = $args->getArg('as');
if (!$as && $to_application_email) {
$default_phid = $to_application_email->getConfigValue(
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
if ($default_phid) {
$default_user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($default_phid))
->executeOne();
if ($default_user) {
$as = $default_user->getUsername();
}
}
}
if (!$as) {
throw new PhutilArgumentUsageException(
pht("Use '--as' to specify the acting user."));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($as))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $as));
}
$from = $args->getArg('from');
if (!$from) {
$from = $user->loadPrimaryEmail()->getAddress();
}
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$received = new PhabricatorMetaMTAReceivedMail();
$header_content = array(
'Message-ID' => Filesystem::readRandomCharacters(12),
'From' => $from,
);
if (preg_match('/.+@.+/', $to)) {
$header_content['to'] = $to;
} else {
+
// 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.
+
+ // In the general case, mail may be processed by multiple receivers,
+ // but mail to objects only ever has one receiver today.
+
$pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
+
+ $raw_target = $to.'+1+'.$pseudohash;
+ $target = new PhutilEmailAddress($raw_target.'@local.cli');
+
$pseudomail = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders(
array(
- 'to' => $to.'+1+'.$pseudohash,
+ 'to' => $raw_target,
));
$receivers = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorMailReceiver')
->setFilterMethod('isEnabled')
->execute();
$receiver = null;
foreach ($receivers as $possible_receiver) {
- if (!$possible_receiver->canAcceptMail($pseudomail)) {
+ $possible_receiver = id(clone $possible_receiver)
+ ->setViewer($viewer)
+ ->setSender($user);
+
+ if (!$possible_receiver->canAcceptMail($pseudomail, $target)) {
continue;
}
$receiver = $possible_receiver;
break;
}
if (!$receiver) {
throw new Exception(
pht("No configured mail receiver can accept mail to '%s'.", $to));
}
if (!($receiver instanceof PhabricatorObjectMailReceiver)) {
$class = get_class($receiver);
throw new Exception(
pht(
"Receiver '%s' accepts mail to '%s', but is not a ".
"subclass of PhabricatorObjectMailReceiver.",
$class,
$to));
}
$object = $receiver->loadMailReceiverObject($to, $user);
if (!$object) {
throw new Exception(pht("No such object '%s'!", $to));
}
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$mail_key,
$user->getPHID());
$header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@test.com';
}
$received->setHeaders($header_content);
$received->setBodies(
array(
'text' => $body,
));
$received->save();
$received->processReceivedMail();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-inbound --id %d\n\n",
pht('Mail received! You can view details by running this command:'),
$received->getID());
}
}
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
index 18b8063ee1..269b9824a5 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
@@ -1,166 +1,166 @@
<?php
final class PhabricatorMetaMTAActorQuery extends PhabricatorQuery {
private $phids = array();
private $viewer;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function execute() {
$phids = array_fuse($this->phids);
$actors = array();
$type_map = array();
foreach ($phids as $phid) {
$type_map[phid_get_type($phid)][] = $phid;
$actors[$phid] = id(new PhabricatorMetaMTAActor())->setPHID($phid);
}
// TODO: Move this to PhabricatorPHIDType, or the objects, or some
// interface.
foreach ($type_map as $type => $phids) {
switch ($type) {
case PhabricatorPeopleUserPHIDType::TYPECONST:
$this->loadUserActors($actors, $phids);
break;
case PhabricatorPeopleExternalPHIDType::TYPECONST:
$this->loadExternalUserActors($actors, $phids);
break;
default:
$this->loadUnknownActors($actors, $phids);
break;
}
}
return $actors;
}
private function loadUserActors(array $actors, array $phids) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID IN (%Ls) AND isPrimary = 1',
$phids);
$emails = mpull($emails, null, 'getUserPHID');
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs($phids)
->needUserSettings(true)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($phids as $phid) {
$actor = $actors[$phid];
$user = idx($users, $phid);
if (!$user) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
} else {
$actor->setName($this->getUserName($user));
if ($user->getIsDisabled()) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_DISABLED);
}
if ($user->getIsSystemAgent()) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_BOT);
}
// NOTE: We do send email to unapproved users, and to unverified users,
// because it would otherwise be impossible to get them to verify their
// email addresses. Possibly we should white-list this kind of mail and
// deny all other types of mail.
}
$email = idx($emails, $phid);
if (!$email) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_NO_ADDRESS);
} else {
$actor->setEmailAddress($email->getAddress());
$actor->setIsVerified($email->getIsVerified());
}
}
}
private function loadExternalUserActors(array $actors, array $phids) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$xusers = id(new PhabricatorExternalAccountQuery())
->setViewer($this->getViewer())
->withPHIDs($phids)
->execute();
$xusers = mpull($xusers, null, 'getPHID');
foreach ($phids as $phid) {
$actor = $actors[$phid];
$xuser = idx($xusers, $phid);
if (!$xuser) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
continue;
}
$actor->setName($xuser->getDisplayName());
if ($xuser->getAccountType() != 'email') {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_EXTERNAL_TYPE);
continue;
}
$actor->setEmailAddress($xuser->getAccountID());
- // NOTE: This effectively drops all outbound mail to unrecognized
- // addresses unless "phabricator.allow-email-users" is set. See T12237
- // for context.
- $allow_key = 'phabricator.allow-email-users';
- $allow_value = PhabricatorEnv::getEnvConfig($allow_key);
- $actor->setIsVerified((bool)$allow_value);
+ // Circa T7477, it appears that we never intentionally send email to
+ // external users (even when they email "bugs@" to create a task).
+ // Mark these users as unverified so mail to them is always dropped.
+ // See also T12237. In the future, we might change this behavior.
+
+ $actor->setIsVerified(false);
}
}
private function loadUnknownActors(array $actors, array $phids) {
foreach ($phids as $phid) {
$actor = $actors[$phid];
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNMAILABLE);
}
}
/**
* Small helper function to make sure we format the username properly as
* specified by the `metamta.user-address-format` configuration value.
*/
private function getUserName(PhabricatorUser $user) {
$format = PhabricatorEnv::getEnvConfig('metamta.user-address-format');
switch ($format) {
case 'short':
$name = $user->getUserName();
break;
case 'real':
$name = strlen($user->getRealName()) ?
$user->getRealName() : $user->getUserName();
break;
case 'full':
default:
$name = $user->getFullName();
break;
}
return $name;
}
}
diff --git a/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php b/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php
index 11646351f2..546f622e10 100644
--- a/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php
@@ -1,34 +1,107 @@
<?php
abstract class PhabricatorApplicationMailReceiver
extends PhabricatorMailReceiver {
+ private $applicationEmail;
+ private $emailList;
+ private $author;
+
abstract protected function newApplication();
+ final protected function setApplicationEmail(
+ PhabricatorMetaMTAApplicationEmail $email) {
+ $this->applicationEmail = $email;
+ return $this;
+ }
+
+ final protected function getApplicationEmail() {
+ return $this->applicationEmail;
+ }
+
+ final protected function setAuthor(PhabricatorUser $author) {
+ $this->author = $author;
+ return $this;
+ }
+
+ final protected function getAuthor() {
+ return $this->author;
+ }
+
final public function isEnabled() {
return $this->newApplication()->isInstalled();
}
- final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- $application = $this->newApplication();
+ final public function canAcceptMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target) {
+
$viewer = $this->getViewer();
+ $sender = $this->getSender();
+
+ foreach ($this->loadApplicationEmailList() as $application_email) {
+ $create_address = $application_email->newAddress();
+
+ if (!PhabricatorMailUtil::matchAddresses($create_address, $target)) {
+ continue;
+ }
- $application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
- ->setViewer($viewer)
- ->withApplicationPHIDs(array($application->getPHID()))
- ->execute();
-
- foreach ($mail->newTargetAddresses() as $address) {
- foreach ($application_emails as $application_email) {
- $create_address = $application_email->newAddress();
- if (PhabricatorMailUtil::matchAddresses($create_address, $address)) {
- $this->setApplicationEmail($application_email);
- return true;
+ if ($sender) {
+ $author = $sender;
+ } else {
+ $author_phid = $application_email->getDefaultAuthorPHID();
+
+ // If this mail isn't from a recognized sender and the target address
+ // does not have a default author, we can't accept it, and it's an
+ // error because you tried to send it here.
+
+ // You either need to be sending from a real address or be sending to
+ // an address which accepts mail from the public internet.
+
+ if (!$author_phid) {
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
+ pht(
+ 'You are sending from an unrecognized email address to '.
+ 'an address which does not support public email ("%s").',
+ (string)$target));
+ }
+
+ $author = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($author_phid))
+ ->executeOne();
+ if (!$author) {
+ throw new Exception(
+ pht(
+ 'Application email ("%s") has an invalid default author ("%s").',
+ (string)$create_address,
+ $author_phid));
}
}
+
+ $this
+ ->setApplicationEmail($application_email)
+ ->setAuthor($author);
+
+ return true;
}
return false;
}
+ private function loadApplicationEmailList() {
+ if ($this->emailList === null) {
+ $viewer = $this->getViewer();
+ $application = $this->newApplication();
+
+ $this->emailList = id(new PhabricatorMetaMTAApplicationEmailQuery())
+ ->setViewer($viewer)
+ ->withApplicationPHIDs(array($application->getPHID()))
+ ->execute();
+ }
+
+ return $this->emailList;
+ }
+
}
diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
index 9f2e979815..738f8d9e26 100644
--- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
@@ -1,168 +1,41 @@
<?php
abstract class PhabricatorMailReceiver extends Phobject {
- private $applicationEmail;
+ private $viewer;
+ private $sender;
- public function setApplicationEmail(
- PhabricatorMetaMTAApplicationEmail $email) {
- $this->applicationEmail = $email;
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
return $this;
}
- public function getApplicationEmail() {
- return $this->applicationEmail;
+ final public function getViewer() {
+ return $this->viewer;
}
- abstract public function isEnabled();
- abstract public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail);
-
- abstract protected function processReceivedMail(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender);
-
- final public function receiveMail(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
- $this->processReceivedMail($mail, $sender);
+ final public function setSender(PhabricatorUser $sender) {
+ $this->sender = $sender;
+ return $this;
}
- public function getViewer() {
- return PhabricatorUser::getOmnipotentUser();
+ final public function getSender() {
+ return $this->sender;
}
- public function validateSender(
+ abstract public function isEnabled();
+ abstract public function canAcceptMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
-
- $failure_reason = null;
- if ($sender->getIsDisabled()) {
- $failure_reason = pht(
- 'Your account (%s) is disabled, so you can not interact with '.
- 'Phabricator over email.',
- $sender->getUsername());
- } else if ($sender->getIsStandardUser()) {
- if (!$sender->getIsApproved()) {
- $failure_reason = pht(
- 'Your account (%s) has not been approved yet. You can not interact '.
- 'with Phabricator over email until your account is approved.',
- $sender->getUsername());
- } else if (PhabricatorUserEmail::isEmailVerificationRequired() &&
- !$sender->getIsEmailVerified()) {
- $failure_reason = pht(
- 'You have not verified the email address for your account (%s). '.
- 'You must verify your email address before you can interact '.
- 'with Phabricator over email.',
- $sender->getUsername());
- }
- }
-
- if ($failure_reason) {
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
- $failure_reason);
- }
- }
-
- /**
- * Identifies the sender's user account for a piece of received mail. Note
- * that this method does not validate that the sender is who they say they
- * are, just that they've presented some credential which corresponds to a
- * recognizable user.
- */
- public function loadSender(PhabricatorMetaMTAReceivedMail $mail) {
- $raw_from = $mail->getHeader('From');
- $from = self::getRawAddress($raw_from);
+ PhutilEmailAddress $target);
- $reasons = array();
-
- // Try to find a user with this email address.
- $user = PhabricatorUser::loadOneWithEmailAddress($from);
- if ($user) {
- return $user;
- } else {
- $reasons[] = pht(
- 'This email was sent from "%s", but that address is not recognized by '.
- 'Phabricator and does not correspond to any known user account.',
- $raw_from);
- }
-
- // If we don't know who this user is, load or create an external user
- // account for them if we're configured for it.
- $email_key = 'phabricator.allow-email-users';
- $allow_email_users = PhabricatorEnv::getEnvConfig($email_key);
- if ($allow_email_users) {
- $from_obj = new PhutilEmailAddress($from);
- $xuser = id(new PhabricatorExternalAccountQuery())
- ->setViewer($this->getViewer())
- ->withAccountTypes(array('email'))
- ->withAccountDomains(array($from_obj->getDomainName(), 'self'))
- ->withAccountIDs(array($from_obj->getAddress()))
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->loadOneOrCreate();
- return $xuser->getPhabricatorUser();
- } else {
- // NOTE: Currently, we'll always drop this mail (since it's headed to
- // an unverified recipient). See T12237. These details are still useful
- // because they'll appear in the mail logs and Mail web UI.
-
- $reasons[] = pht(
- 'Phabricator is also not configured to allow unknown external users '.
- 'to send mail to the system using just an email address.');
- $reasons[] = pht(
- 'To interact with Phabricator, add this address ("%s") to your '.
- 'account.',
- $raw_from);
- }
-
- if ($this->getApplicationEmail()) {
- $application_email = $this->getApplicationEmail();
- $default_user_phid = $application_email->getConfigValue(
- PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
-
- if ($default_user_phid) {
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'phid = %s',
- $default_user_phid);
- if ($user) {
- return $user;
- }
-
- $reasons[] = pht(
- 'Phabricator is misconfigured: the application email '.
- '"%s" is set to user "%s", but that user does not exist.',
- $application_email->getAddress(),
- $default_user_phid);
- }
- }
-
- $reasons = implode("\n\n", $reasons);
-
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
- $reasons);
- }
+ abstract protected function processReceivedMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target);
- /**
- * Reduce an email address to its canonical form. For example, an address
- * like:
- *
- * "Abraham Lincoln" < ALincoln@example.com >
- *
- * ...will be reduced to:
- *
- * alincoln@example.com
- *
- * @param string Email address in noncanonical form.
- * @return string Canonical email address.
- */
- public static function getRawAddress($address) {
- $address = id(new PhutilEmailAddress($address))->getAddress();
- return trim(phutil_utf8_strtolower($address));
+ final public function receiveMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target) {
+ $this->processReceivedMail($mail, $target);
}
}
diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
index 9f92d33f21..16950c1577 100644
--- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
@@ -1,208 +1,190 @@
<?php
abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver {
/**
* Return a regular expression fragment which matches the name of an
* object which can receive mail. For example, Differential uses:
*
* D[1-9]\d*
*
* ...to match `D123`, etc., identifying Differential Revisions.
*
* @return string Regular expression fragment.
*/
abstract protected function getObjectPattern();
/**
* Load the object receiving mail, based on an identifying pattern. Normally
* this pattern is some sort of object ID.
*
* @param string A string matched by @{method:getObjectPattern}
* fragment.
* @param PhabricatorUser The viewing user.
* @return void
*/
abstract protected function loadObject($pattern, PhabricatorUser $viewer);
final protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
-
- $object = $this->loadObjectFromMail($mail, $sender);
- $mail->setRelatedPHID($object->getPHID());
-
- $this->processReceivedObjectMail($mail, $object, $sender);
-
- return $this;
- }
-
- protected function processReceivedObjectMail(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorLiskDAO $object,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
- $handler = $this->getTransactionReplyHandler();
- if ($handler) {
- return $handler
- ->setMailReceiver($object)
- ->setActor($sender)
- ->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs())
- ->processEmail($mail);
+ $parts = $this->matchObjectAddress($target);
+ if (!$parts) {
+ // We should only make it here if we matched already in "canAcceptMail()",
+ // so this is a surprise.
+ throw new Exception(
+ pht(
+ 'Failed to parse object address ("%s") during processing.',
+ (string)$target));
}
- throw new PhutilMethodNotImplementedException();
- }
-
- protected function getTransactionReplyHandler() {
- return null;
- }
-
- public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) {
- return $this->loadObject($pattern, $viewer);
- }
-
- public function validateSender(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
-
- parent::validateSender($mail, $sender);
-
- $parts = $this->matchObjectAddressInMail($mail);
$pattern = $parts['pattern'];
+ $sender = $this->getSender();
try {
- $object = $this->loadObjectFromMail($mail, $sender);
+ $object = $this->loadObject($pattern, $sender);
} catch (PhabricatorPolicyException $policy_exception) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM,
pht(
'This mail is addressed to an object ("%s") you do not have '.
'permission to see: %s',
$pattern,
$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.',
$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 the public email address of an object '.
'("%s"), but public replies are not enabled on this Phabricator '.
'install. An administrator may have recently disabled this '.
'setting, or you may have replied to an old message. Try '.
'replying to a more recent message instead.',
$pattern));
}
$check_phid = $object->getPHID();
} else {
if ($sender_identifier != $sender->getID()) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH,
pht(
'This mail is addressed to the private email address of an object '.
'("%s"), but you are not the user who is authorized to use the '.
'address you sent mail to. Each private address is unique to the '.
'user who received the original mail. Try replying to a message '.
'which was sent directly to you instead.',
$pattern));
}
$check_phid = $sender->getPHID();
}
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
$expect_hash = self::computeMailHash($mail_key, $check_phid);
if (!phutil_hashes_are_identical($expect_hash, $parts['hash'])) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH,
pht(
'This mail is addressed to an object ("%s"), but the address is '.
'not correct (the security hash is wrong). Check that the address '.
'is correct.',
$pattern));
}
+
+ $mail->setRelatedPHID($object->getPHID());
+ $this->processReceivedObjectMail($mail, $object, $sender);
+
+ return $this;
}
+ protected function processReceivedObjectMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhabricatorLiskDAO $object,
+ PhabricatorUser $sender) {
- final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- if ($this->matchObjectAddressInMail($mail)) {
- return true;
+ $handler = $this->getTransactionReplyHandler();
+ if ($handler) {
+ return $handler
+ ->setMailReceiver($object)
+ ->setActor($sender)
+ ->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs())
+ ->processEmail($mail);
}
- return false;
+ throw new PhutilMethodNotImplementedException();
}
- private function matchObjectAddressInMail(
- PhabricatorMetaMTAReceivedMail $mail) {
+ protected function getTransactionReplyHandler() {
+ return null;
+ }
- foreach ($mail->newTargetAddresses() as $address) {
- $parts = $this->matchObjectAddress($address);
- if ($parts) {
- return $parts;
- }
+ public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) {
+ return $this->loadObject($pattern, $viewer);
+ }
+
+ final public function canAcceptMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target) {
+
+ // If we don't have a valid sender user account, we can never accept
+ // mail to any object.
+ $sender = $this->getSender();
+ if (!$sender) {
+ return false;
}
- return null;
+ return (bool)$this->matchObjectAddress($target);
}
private function matchObjectAddress(PhutilEmailAddress $address) {
$address = PhabricatorMailUtil::normalizeAddress($address);
$local = $address->getLocalPart();
$regexp = $this->getAddressRegexp();
$matches = null;
if (!preg_match($regexp, $local, $matches)) {
return false;
}
return $matches;
}
private function getAddressRegexp() {
$pattern = $this->getObjectPattern();
$regexp =
'(^'.
'(?P<pattern>'.$pattern.')'.
'\\+'.
'(?P<sender>\w+)'.
'\\+'.
'(?P<hash>[a-f0-9]{16})'.
'$)Ui';
return $regexp;
}
- private function loadObjectFromMail(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
- $parts = $this->matchObjectAddressInMail($mail);
-
- return $this->loadObject(
- phutil_utf8_strtoupper($parts['pattern']),
- $sender);
- }
-
public static function computeMailHash($mail_key, $phid) {
$hash = PhabricatorHash::digestWithNamedKey(
$mail_key.$phid,
'mail.object-address-key');
return substr($hash, 0, 16);
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
index f9698c3928..f5673bed9d 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
@@ -1,151 +1,154 @@
<?php
final class PhabricatorMetaMTAApplicationEmail
extends PhabricatorMetaMTADAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface {
protected $applicationPHID;
protected $address;
protected $configData;
protected $spacePHID;
private $application = self::ATTACHABLE;
const CONFIG_DEFAULT_AUTHOR = 'config:default:author';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'sort128',
),
self::CONFIG_KEY_SCHEMA => array(
'key_address' => array(
'columns' => array('address'),
'unique' => true,
),
'key_application' => array(
'columns' => array('applicationPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST);
}
public static function initializeNewAppEmail(PhabricatorUser $actor) {
return id(new PhabricatorMetaMTAApplicationEmail())
->setSpacePHID($actor->getDefaultSpacePHID())
->setConfigData(array());
}
public function attachApplication(PhabricatorApplication $app) {
$this->application = $app;
return $this;
}
public function getApplication() {
return self::assertAttached($this->application);
}
public function setConfigValue($key, $value) {
$this->configData[$key] = $value;
return $this;
}
public function getConfigValue($key, $default = null) {
return idx($this->configData, $key, $default);
}
+ public function getDefaultAuthorPHID() {
+ return $this->getConfigValue(self::CONFIG_DEFAULT_AUTHOR);
+ }
public function getInUseMessage() {
$applications = PhabricatorApplication::getAllApplications();
$applications = mpull($applications, null, 'getPHID');
$application = idx(
$applications,
$this->getApplicationPHID());
if ($application) {
$message = pht(
'The address %s is configured to be used by the %s Application.',
$this->getAddress(),
$application->getName());
} else {
$message = pht(
'The address %s is configured to be used by an application.',
$this->getAddress());
}
return $message;
}
public function newAddress() {
return new PhutilEmailAddress($this->getAddress());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getApplication()->getPolicy($capability);
}
public function hasAutomaticCapability(
$capability,
PhabricatorUser $viewer) {
return $this->getApplication()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return $this->getApplication()->describeAutomaticCapability($capability);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorMetaMTAApplicationEmailEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorMetaMTAApplicationEmailTransaction();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
index 95f6048c01..0605c99b89 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
@@ -1,437 +1,531 @@
<?php
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $status = '';
protected $relatedPHID;
protected $authorPHID;
protected $message;
protected $messageIDHash = '';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'headers' => self::SERIALIZATION_JSON,
'bodies' => self::SERIALIZATION_JSON,
'attachments' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'relatedPHID' => 'phid?',
'authorPHID' => 'phid?',
'message' => 'text?',
'messageIDHash' => 'bytes12',
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'key_messageIDHash' => array(
'columns' => array('messageIDHash'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + 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'));
}
public function newTargetAddresses() {
$raw_addresses = array();
foreach ($this->getToAddresses() as $raw_address) {
$raw_addresses[] = $raw_address;
}
foreach ($this->getCCAddresses() as $raw_address) {
$raw_addresses[] = $raw_address;
}
$raw_addresses = array_unique($raw_addresses);
$addresses = array();
foreach ($raw_addresses as $raw_address) {
$addresses[] = new PhutilEmailAddress($raw_address);
}
return $addresses;
}
public function loadAllRecipientPHIDs() {
$addresses = array_merge(
$this->getToAddresses(),
$this->getCCAddresses());
return $this->loadPHIDsFromAddresses($addresses);
}
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);
return mpull($users, 'getUserPHID');
}
public function processReceivedMail() {
+ $viewer = $this->getViewer();
$sender = null;
try {
$this->dropMailFromPhabricator();
$this->dropMailAlreadyReceived();
$this->dropEmptyMail();
- $receiver = $this->loadReceiver();
- $sender = $receiver->loadSender($this);
- $receiver->validateSender($this, $sender);
-
- $this->setAuthorPHID($sender->getPHID());
-
- // Now that we've identified the sender, mark them as the author of
- // any attached files.
- $attachments = $this->getAttachments();
- if ($attachments) {
- $files = id(new PhabricatorFileQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs($attachments)
- ->execute();
- foreach ($files as $file) {
- $file->setAuthorPHID($sender->getPHID())->save();
+ $sender = $this->loadSender();
+ if ($sender) {
+ $this->setAuthorPHID($sender->getPHID());
+
+ // If we've identified the sender, mark them as the author of any
+ // attached files. We do this before we validate them (below), since
+ // they still authored these files even if their account is not allowed
+ // to interact via email.
+
+ $attachments = $this->getAttachments();
+ if ($attachments) {
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs($attachments)
+ ->execute();
+ foreach ($files as $file) {
+ $file->setAuthorPHID($sender->getPHID())->save();
+ }
+ }
+
+ $this->validateSender($sender);
+ }
+
+ $receivers = id(new PhutilClassMapQuery())
+ ->setAncestorClass('PhabricatorMailReceiver')
+ ->setFilterMethod('isEnabled')
+ ->execute();
+
+ $any_accepted = false;
+ $receiver_exception = null;
+
+ $targets = $this->newTargetAddresses();
+ foreach ($receivers as $receiver) {
+ $receiver = id(clone $receiver)
+ ->setViewer($viewer);
+
+ if ($sender) {
+ $receiver->setSender($sender);
+ }
+
+ foreach ($targets as $target) {
+ try {
+ if (!$receiver->canAcceptMail($this, $target)) {
+ continue;
+ }
+
+ $any_accepted = true;
+
+ $receiver->receiveMail($this, $target);
+ } catch (Exception $ex) {
+ // If receivers raise exceptions, we'll keep the first one in hope
+ // that it points at a root cause.
+ if (!$receiver_exception) {
+ $receiver_exception = $ex;
+ }
+ }
}
}
- $receiver->receiveMail($this, $sender);
+ if ($receiver_exception) {
+ throw $receiver_exception;
+ }
+
+ if (!$any_accepted) {
+ if (!$sender) {
+ // NOTE: Currently, we'll always drop this mail (since it's headed to
+ // an unverified recipient). See T12237. These details are still
+ // useful because they'll appear in the mail logs and Mail web UI.
+
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
+ pht(
+ 'This email was sent from an email address ("%s") that is not '.
+ 'associated with a Phabricator account. To interact with '.
+ 'Phabricator via email, add this address to your account.',
+ (string)$this->newFromAddress()));
+ } else {
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
+ pht(
+ 'Phabricator can not process this mail because no application '.
+ 'knows how to handle it. Check that the address you sent it to '.
+ 'is correct.'.
+ "\n\n".
+ '(No concrete, enabled subclass of PhabricatorMailReceiver can '.
+ 'accept this mail.)'));
+ }
+ }
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
switch ($ex->getStatusCode()) {
case MetaMTAReceivedMailStatus::STATUS_DUPLICATE:
case MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR:
// Don't send an error email back in these cases, since they're
// very unlikely to be the sender's fault.
break;
case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED:
// This error is explicitly ignored.
break;
default:
$this->sendExceptionMail($ex, $sender);
break;
}
$this
->setStatus($ex->getStatusCode())
->setMessage($ex->getMessage())
->save();
return $this;
} catch (Exception $ex) {
$this->sendExceptionMail($ex, $sender);
$this
->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
->save();
throw $ex;
}
return $this->setMessage('OK')->save();
}
public function getCleanTextBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->stripTextBody($body);
}
public function parseBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->parseBody($body);
}
public function getRawTextBody() {
return idx($this->bodies, 'text');
}
/**
* Strip an email address down to the actual user@domain.tld part if
* necessary, since sometimes it will have formatting like
* '"Abraham Lincoln" <alincoln@logcab.in>'.
*/
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);
}
/**
* 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,
pht(
"Ignoring email with '%s' header to avoid loops.",
'X-Phabricator-Sent-This-Message'));
}
/**
* 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 = pht(
'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);
}
private function dropEmptyMail() {
$body = $this->getCleanTextBody();
$attachments = $this->getAttachments();
if (strlen($body) || $attachments) {
return;
}
// 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.
$is_direct_mail = (count($this->getToAddresses()) == 1) &&
(count($this->getCCAddresses()) == 0);
if ($is_direct_mail) {
$status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY;
} else {
$status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED;
}
throw new PhabricatorMetaMTAReceivedMailProcessingException(
$status_code,
pht(
'Your message does not contain any body text or attachments, so '.
'Phabricator can not do anything useful with it. Make sure comment '.
'text appears at the top of your message: quoted replies, inline '.
'text, and signatures are discarded and ignored.'));
}
- /**
- * Load a concrete instance of the @{class:PhabricatorMailReceiver} which
- * accepts this mail, if one exists.
- */
- private function loadReceiver() {
- $receivers = id(new PhutilClassMapQuery())
- ->setAncestorClass('PhabricatorMailReceiver')
- ->setFilterMethod('isEnabled')
- ->execute();
-
- $accept = array();
- foreach ($receivers as $key => $receiver) {
- if ($receiver->canAcceptMail($this)) {
- $accept[$key] = $receiver;
- }
- }
-
- if (!$accept) {
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
- pht(
- 'Phabricator can not process this mail because no application '.
- 'knows how to handle it. Check that the address you sent it to is '.
- 'correct.'.
- "\n\n".
- '(No concrete, enabled subclass of PhabricatorMailReceiver can '.
- 'accept this mail.)'));
- }
-
- if (count($accept) > 1) {
- $names = implode(', ', array_keys($accept));
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS,
- pht(
- 'Phabricator is not able to process this mail because more than '.
- 'one application is willing to accept it, creating ambiguity. '.
- 'Mail needs to be accepted by exactly one receiving application.'.
- "\n\n".
- 'Accepting receivers: %s.',
- $names));
- }
-
- return head($accept);
- }
-
private function sendExceptionMail(
Exception $ex,
PhabricatorUser $viewer = null) {
// If we've failed to identify a legitimate sender, we don't send them
// an error message back. We want to avoid sending mail to unverified
// addresses. See T12491.
if (!$viewer) {
return;
}
if ($ex instanceof PhabricatorMetaMTAReceivedMailProcessingException) {
$status_code = $ex->getStatusCode();
$status_name = MetaMTAReceivedMailStatus::getHumanReadableName(
$status_code);
$title = pht('Error Processing Mail (%s)', $status_name);
$description = $ex->getMessage();
} else {
$title = pht('Error Processing Mail (%s)', get_class($ex));
$description = pht('%s: %s', get_class($ex), $ex->getMessage());
}
// TODO: Since headers don't necessarily have unique names, this may not
// really be all the headers. It would be nice to pass the raw headers
// through from the upper layers where possible.
// On the MimeMailParser pathway, we arrive here with a list value for
// headers that appeared multiple times in the original mail. Be
// accommodating until header handling gets straightened out.
$headers = array();
foreach ($this->headers as $key => $values) {
if (!is_array($values)) {
$values = array($values);
}
foreach ($values as $value) {
$headers[] = pht('%s: %s', $key, $value);
}
}
$headers = implode("\n", $headers);
$body = pht(<<<EOBODY
Your email to Phabricator was not processed, because an error occurred while
trying to handle it:
%s
-- Original Message Body -----------------------------------------------------
%s
-- Original Message Headers --------------------------------------------------
%s
EOBODY
,
wordwrap($description, 78),
$this->getRawTextBody(),
$headers);
$mail = id(new PhabricatorMetaMTAMail())
->setIsErrorEmail(true)
->setSubject($title)
->addTos(array($viewer->getPHID()))
->setBody($body)
->saveAndSend();
}
public function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorEmailContentSource::SOURCECONST,
array(
'id' => $this->getID(),
));
}
+ public function newFromAddress() {
+ $raw_from = $this->getHeader('From');
+
+ if (strlen($raw_from)) {
+ return new PhutilEmailAddress($raw_from);
+ }
+
+ return null;
+ }
+
+ private function getViewer() {
+ return PhabricatorUser::getOmnipotentUser();
+ }
+
+ /**
+ * Identify the sender's user account for a piece of received mail.
+ *
+ * Note that this method does not validate that the sender is who they say
+ * they are, just that they've presented some credential which corresponds
+ * to a recognizable user.
+ */
+ private function loadSender() {
+ $viewer = $this->getViewer();
+
+ // Try to identify the user based on their "From" address.
+ $from_address = $this->newFromAddress();
+ if ($from_address) {
+ $user = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withEmails(array($from_address->getAddress()))
+ ->executeOne();
+ if ($user) {
+ return $user;
+ }
+ }
+
+ return null;
+ }
+
+ private function validateSender(PhabricatorUser $sender) {
+ $failure_reason = null;
+ if ($sender->getIsDisabled()) {
+ $failure_reason = pht(
+ 'Your account ("%s") is disabled, so you can not interact with '.
+ 'Phabricator over email.',
+ $sender->getUsername());
+ } else if ($sender->getIsStandardUser()) {
+ if (!$sender->getIsApproved()) {
+ $failure_reason = pht(
+ 'Your account ("%s") has not been approved yet. You can not '.
+ 'interact with Phabricator over email until your account is '.
+ 'approved.',
+ $sender->getUsername());
+ } else if (PhabricatorUserEmail::isEmailVerificationRequired() &&
+ !$sender->getIsEmailVerified()) {
+ $failure_reason = pht(
+ 'You have not verified the email address for your account ("%s"). '.
+ 'You must verify your email address before you can interact '.
+ 'with Phabricator over email.',
+ $sender->getUsername());
+ }
+ }
+
+ if ($failure_reason) {
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
+ $failure_reason);
+ }
+ }
+
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
index 31a31b5ff2..2630a50feb 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
@@ -1,136 +1,136 @@
<?php
final class PhabricatorMetaMTAReceivedMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testDropSelfMail() {
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'X-Phabricator-Sent-This-Message' => 'yes',
));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
$mail->getStatus());
}
public function testDropDuplicateMail() {
$mail_a = new PhabricatorMetaMTAReceivedMail();
$mail_a->setHeaders(
array(
'Message-ID' => 'test@example.com',
));
$mail_a->save();
$mail_b = new PhabricatorMetaMTAReceivedMail();
$mail_b->setHeaders(
array(
'Message-ID' => 'test@example.com',
));
$mail_b->save();
$mail_a->processReceivedMail();
$mail_b->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
$mail_b->getStatus());
}
public function testDropUnreceivableMail() {
+ $user = $this->generateNewTestUser()
+ ->save();
+
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'To' => 'does+not+exist@example.com',
+ 'From' => $user->loadPrimaryEmail()->getAddress(),
));
$mail->setBodies(
array(
'text' => 'test',
));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
$mail->getStatus());
}
public function testDropUnknownSenderMail() {
$this->setManiphestCreateEmail();
- $env = PhabricatorEnv::beginScopedEnv();
- $env->overrideEnvConfig('phabricator.allow-email-users', false);
- $env->overrideEnvConfig('metamta.maniphest.default-public-author', null);
-
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'To' => 'bugs@example.com',
'From' => 'does+not+exist@example.com',
));
$mail->setBodies(
array(
'text' => 'test',
));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
$mail->getStatus());
}
public function testDropDisabledSenderMail() {
$this->setManiphestCreateEmail();
$user = $this->generateNewTestUser()
->setIsDisabled(true)
->save();
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'From' => $user->loadPrimaryEmail()->getAddress(),
'To' => 'bugs@example.com',
));
$mail->setBodies(
array(
'text' => 'test',
));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
$mail->getStatus());
}
private function setManiphestCreateEmail() {
$maniphest_app = new PhabricatorManiphestApplication();
try {
id(new PhabricatorMetaMTAApplicationEmail())
->setApplicationPHID($maniphest_app->getPHID())
->setAddress('bugs@example.com')
->setConfigData(array())
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {}
}
}
diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php
index 992d1e60b5..844c5f1acc 100644
--- a/src/applications/paste/mail/PasteCreateMailReceiver.php
+++ b/src/applications/paste/mail/PasteCreateMailReceiver.php
@@ -1,60 +1,65 @@
<?php
final class PasteCreateMailReceiver
extends PhabricatorApplicationMailReceiver {
protected function newApplication() {
return new PhabricatorPasteApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+ $author = $this->getAuthor();
$title = $mail->getSubject();
if (!$title) {
$title = pht('Email Paste');
}
$xactions = array();
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteContentTransaction::TRANSACTIONTYPE)
->setNewValue($mail->getCleanTextBody());
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title);
- $paste = PhabricatorPaste::initializeNewPaste($sender);
+ $paste = PhabricatorPaste::initializeNewPaste($author);
$content_source = $mail->newContentSource();
$editor = id(new PhabricatorPasteEditor())
- ->setActor($sender)
+ ->setActor($author)
->setContentSource($content_source)
->setContinueOnNoEffect(true);
$xactions = $editor->applyTransactions($paste, $xactions);
$mail->setRelatedPHID($paste->getPHID());
+ $sender = $this->getSender();
+ if (!$sender) {
+ return;
+ }
+
$subject_prefix =
PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix');
$subject = pht('You successfully created a paste.');
$paste_uri = PhabricatorEnv::getProductionURI($paste->getURI());
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
$body->addTextSection(pht('PASTE LINK'), $paste_uri);
id(new PhabricatorMetaMTAMail())
->addTos(array($sender->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
->setFrom($sender->getPHID())
->setRelatedPHID($paste->getPHID())
->setBody($body->render())
->saveAndSend();
}
-
}
diff --git a/src/applications/paste/mail/PasteMailReceiver.php b/src/applications/paste/mail/PasteMailReceiver.php
index cb5a6e8982..f0b2f4674c 100644
--- a/src/applications/paste/mail/PasteMailReceiver.php
+++ b/src/applications/paste/mail/PasteMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PasteMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPasteApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'P[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'P');
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorPasteQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PasteReplyHandler();
}
}
diff --git a/src/applications/phame/mail/PhamePostMailReceiver.php b/src/applications/phame/mail/PhamePostMailReceiver.php
index 3655e73906..3e2f493d3a 100644
--- a/src/applications/phame/mail/PhamePostMailReceiver.php
+++ b/src/applications/phame/mail/PhamePostMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhamePostMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorPhameApplication');
}
protected function getObjectPattern() {
return 'J[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)substr($pattern, 4);
+ $id = (int)substr($pattern, 1);
return id(new PhamePostQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhamePostReplyHandler();
}
}
diff --git a/src/applications/pholio/mail/PholioMockMailReceiver.php b/src/applications/pholio/mail/PholioMockMailReceiver.php
index 09c0eb3051..13fdc559ec 100644
--- a/src/applications/pholio/mail/PholioMockMailReceiver.php
+++ b/src/applications/pholio/mail/PholioMockMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PholioMockMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPholioApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'M[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'M');
+ $id = (int)substr($pattern, 1);
return id(new PholioMockQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PholioReplyHandler();
}
}
diff --git a/src/applications/ponder/mail/PonderAnswerMailReceiver.php b/src/applications/ponder/mail/PonderAnswerMailReceiver.php
index d7269ac861..dfa252c4a1 100644
--- a/src/applications/ponder/mail/PonderAnswerMailReceiver.php
+++ b/src/applications/ponder/mail/PonderAnswerMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PonderAnswerMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPonderApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'ANSR[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'ANSR');
+ $id = (int)substr($pattern, 4);
return id(new PonderAnswerQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PonderAnswerReplyHandler();
}
}
diff --git a/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php b/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php
index 1eb3269798..32669855ea 100644
--- a/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php
+++ b/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php
@@ -1,44 +1,44 @@
<?php
final class PonderQuestionCreateMailReceiver
extends PhabricatorApplicationMailReceiver {
protected function newApplication() {
return new PhabricatorPonderApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+ $author = $this->getAuthor();
$title = $mail->getSubject();
if (!strlen($title)) {
$title = pht('New Question');
}
$xactions = array();
$xactions[] = id(new PonderQuestionTransaction())
->setTransactionType(PonderQuestionTransaction::TYPE_TITLE)
->setNewValue($title);
$xactions[] = id(new PonderQuestionTransaction())
->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT)
->setNewValue($mail->getCleanTextBody());
- $question = PonderQuestion::initializeNewQuestion($sender);
+ $question = PonderQuestion::initializeNewQuestion($author);
$content_source = $mail->newContentSource();
$editor = id(new PonderQuestionEditor())
- ->setActor($sender)
+ ->setActor($author)
->setContentSource($content_source)
->setContinueOnNoEffect(true);
$xactions = $editor->applyTransactions($question, $xactions);
$mail->setRelatedPHID($question->getPHID());
-
}
}
diff --git a/src/applications/ponder/mail/PonderQuestionMailReceiver.php b/src/applications/ponder/mail/PonderQuestionMailReceiver.php
index 6388837af3..e68d6c0ecb 100644
--- a/src/applications/ponder/mail/PonderQuestionMailReceiver.php
+++ b/src/applications/ponder/mail/PonderQuestionMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PonderQuestionMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPonderApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'Q[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'Q');
+ $id = (int)substr($pattern, 1);
return id(new PonderQuestionQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PonderQuestionReplyHandler();
}
}
diff --git a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php
index 78e608231f..7b7459d4c3 100644
--- a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php
+++ b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorSlowvoteMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorSlowvoteApplication');
}
protected function getObjectPattern() {
return 'V[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)substr($pattern, 4);
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorSlowvoteQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorSlowvoteReplyHandler();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 31, 10:32 AM (18 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
312099
Default Alt Text
(128 KB)

Event Timeline