Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/audit/mail/PhabricatorAuditReplyHandler.php b/src/applications/audit/mail/PhabricatorAuditReplyHandler.php
index 7214943f76..1b51feaee0 100644
--- a/src/applications/audit/mail/PhabricatorAuditReplyHandler.php
+++ b/src/applications/audit/mail/PhabricatorAuditReplyHandler.php
@@ -1,55 +1,50 @@
<?php
final class PhabricatorAuditReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorRepositoryCommit)) {
throw new Exception('Mail receiver is not a commit!');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'C');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('C');
}
- public function getReplyHandlerDomain() {
- return $this->getCustomReplyHandlerDomainIfExists(
- 'metamta.diffusion.reply-handler-domain');
- }
-
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$commit = $this->getMailReceiver();
$actor = $this->getActor();
$message = $mail->getCleanTextBody();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
// TODO: Support !raise, !accept, etc.
$xactions = array();
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorAuditTransactionComment())
->setCommitPHID($commit->getPHID())
->setContent($message));
$editor = id(new PhabricatorAuditEditor())
->setActor($actor)
->setContentSource($content_source)
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setContinueOnMissingFields(true)
->applyTransactions($commit, $xactions);
}
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index 550574a199..df60dab08d 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,229 +1,240 @@
<?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 '.
'PhabricatorRemarkupCustomInlineRule or '.
'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');
+
$ancient_config += array(
'phid.external-loaders' =>
pht(
'External loaders have been replaced. Extend `PhabricatorPHIDType` '.
'to implement new PHID and handle types.'),
'maniphest.custom-task-extensions-class' =>
pht(
'Maniphest fields are now loaded automatically. You can configure '.
'them with `maniphest.fields`.'),
'maniphest.custom-fields' =>
pht(
'Maniphest fields are now defined in '.
'`maniphest.custom-field-definitions`. Existing definitions have '.
'been migrated.'),
'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 '.
'`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 `phabricator.show-prototypes` '.
'to emphasize the unfinished nature of many prototype applications. '.
'Your existing setting has been migrated.'),
'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 `config.hide` instead of this option.'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
'pool size with `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 '.
'`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,
);
return $ancient_config;
}
}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
index 7c0b990992..1bc8d21bdb 100644
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -1,354 +1,352 @@
<?php
final class PhabricatorMetaMTAConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Mail');
}
public function getDescription() {
return pht('Configure Mail.');
}
public function getFontIcon() {
return 'fa-send';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$send_as_user_desc = $this->deformat(pht(<<<EODOC
When a user takes an action which generates an email notification (like
commenting on a Differential revision), Phabricator can either send that mail
"From" the user's email address (like "alincoln@logcabin.com") or "From" the
'metamta.default-address' address.
The user experience is generally better if Phabricator uses the user's real
address as the "From" since the messages are easier to organize when they appear
in mail clients, but this will only work if the server is authorized to send
email on behalf of the "From" domain. Practically, this means:
- If you are doing an install for Example Corp and all the users will have
corporate @corp.example.com addresses and any hosts Phabricator is running
on are authorized to send email from corp.example.com, you can enable this
to make the user experience a little better.
- If you are doing an install for an open source project and your users will
be registering via Facebook and using personal email addresses, you probably
should not enable this or all of your outgoing email might vanish into SFP
blackholes.
- If your install is anything else, you're safer leaving this off, at least
initially, since the risk in turning it on is that your outgoing mail will
never arrive.
EODOC
));
$one_mail_per_recipient_desc = $this->deformat(pht(<<<EODOC
When a message is sent to multiple recipients (for example, several reviewers on
a code review), Phabricator can either deliver one email to everyone (e.g., "To:
alincoln, usgrant, htaft") or separate emails to each user (e.g., "To:
alincoln", "To: usgrant", "To: htaft"). The major advantages and disadvantages
of each approach are:
- One mail to everyone:
- Recipients can see To/Cc at a glance.
- If you use mailing lists, you won't get duplicate mail if you're
a normal recipient and also Cc'd on a mailing list.
- Getting threading to work properly is harder, and probably requires
making mail less useful by turning off options.
- Sometimes people will "Reply All" and everyone will get two mails,
one from the user and one from Phabricator turning their mail into
a comment.
- Not supported with a private reply-to address.
- Mails are sent in the server default translation.
- One mail to each user:
- Recipients need to look in the mail body to see To/Cc.
- If you use mailing lists, recipients may sometimes get duplicate
mail.
- Getting threading to work properly is easier, and threading settings
can be customzied by each user.
- "Reply All" no longer spams all other users.
- Required if private reply-to addresses are configured.
- Mails are sent in the language of user preference.
In the code, splitting one outbound email into one-per-recipient is sometimes
referred to as "multiplexing".
EODOC
));
$herald_hints_description = $this->deformat(pht(<<<EODOC
You can disable the Herald hints in email if users prefer smaller messages.
These are the links under the header "WHY DID I GET THIS EMAIL?". If you set
this to true, they will not appear in any mail. Users can still navigate to
the links via the web interface.
EODOC
));
$reply_hints_description = $this->deformat(pht(<<<EODOC
You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer
smaller messages. The actions themselves will still work properly.
EODOC
));
$recipient_hints_description = $this->deformat(pht(<<<EODOC
You can disable the "To:" and "Cc:" footers in mail if users prefer smaller
messages.
EODOC
));
$bulk_description = $this->deformat(pht(<<<EODOC
If this option is enabled, Phabricator will add a "Precedence: bulk" header to
transactional mail (e.g., Differential, Maniphest and Herald notifications).
This may improve the behavior of some auto-responder software and prevent it
from replying. However, it may also cause deliverability issues -- notably, you
currently can not send this header via Amazon SES, and enabling this option with
SES will prevent delivery of any affected mail.
EODOC
));
$email_preferences_description = $this->deformat(pht(<<<EODOC
You can disable the email preference link in emails if users prefer smaller
emails.
EODOC
));
$re_prefix_description = $this->deformat(pht(<<<EODOC
Mail.app on OS X Lion won't respect threading headers unless the subject is
prefixed with "Re:". If you enable this option, Phabricator will add "Re:" to
the subject line of all mail which is expected to thread. If you've set
'metamta.one-mail-per-recipient', users can override this setting in their
preferences.
EODOC
));
$vary_subjects_description = $this->deformat(pht(<<<EODOC
If true, allow MetaMTA to change mail subjects to put text like '[Accepted]' and
'[Commented]' in them. This makes subjects more useful, but might break
threading on some clients. If you've set 'metamta.one-mail-per-recipient', users
can override this setting in their preferences.
EODOC
));
$reply_to_description = $this->deformat(pht(<<<EODOC
If you enable {{metamta.public-replies}}, Phabricator uses "From" to
authenticate users. You can additionally enable this setting to try to
authenticate with 'Reply-To'. Note that this is completely spoofable and
insecure (any user can set any 'Reply-To' address) but depending on the nature
of your install or other deliverability conditions this might be okay.
Generally, you can't do much more by spoofing Reply-To than be annoying (you can
write but not read content). But this is still **COMPLETELY INSECURE**.
EODOC
));
$adapter_description = $this->deformat(pht(<<<EODOC
Adapter class to use to transmit mail to the MTA. The default uses
PHPMailerLite, which will invoke "sendmail". This is appropriate if sendmail
actually works on your host, but if you haven't configured mail it may not be so
great. A number of other mailers are available (e.g., SES, SendGrid, SMTP,
custom mailers), consult "Configuring Outbound Email" in the documentation for
details.
EODOC
));
$placeholder_description = $this->deformat(pht(<<<EODOC
When sending a message that has no To recipient (i.e. all recipients are CC'd,
for example when multiplexing mail), set the To field to the following value. If
no value is set, messages with no To will have their CCs upgraded to To.
EODOC
));
$public_replies_description = $this->deformat(pht(<<<EODOC
By default, Phabricator generates unique reply-to addresses and sends a separate
email to each recipient when you enable reply handling. This is more secure than
using "From" to establish user identity, but can mean users may receive multiple
emails when they are on mailing lists. Instead, you can use a single, non-unique
reply to address and authenticate users based on the "From" address by setting
this to 'true'. This trades away a little bit of security for convenience, but
it's reasonable in many installs. Object interactions are still protected using
hashes in the single public email address, so objects can not be replied to
blindly.
EODOC
));
$single_description = $this->deformat(pht(<<<EODOC
If you want to use a single mailbox for Phabricator reply mail, you can use this
and set a common prefix for reply addresses generated by Phabricator. It will
make use of the fact that a mail-address such as
`phabricator+D123+1hjk213h@example.com` will be delivered to the `phabricator`
user's mailbox. Set this to the left part of the email address and it will be
prepended to all generated reply addresses.
For example, if you want to use `phabricator@example.com`, this should be set
to `phabricator`.
EODOC
));
$address_description = $this->deformat(pht(<<<EODOC
When email is sent, what format should Phabricator use for user's email
addresses? Valid values are:
- `short`: 'gwashington <gwashington@example.com>'
- `real`: 'George Washington <gwashington@example.com>'
- `full`: 'gwashington (George Washington) <gwashington@example.com>'
The default is `full`.
EODOC
));
return array(
$this->newOption(
'metamta.default-address',
'string',
'noreply@phabricator.example.com')
->setDescription(pht('Default "From" address.')),
$this->newOption(
'metamta.domain',
'string',
'phabricator.example.com')
->setDescription(pht('Domain used to generate Message-IDs.')),
$this->newOption(
'metamta.mail-adapter',
'class',
'PhabricatorMailImplementationPHPMailerLiteAdapter')
->setBaseClass('PhabricatorMailImplementationAdapter')
->setSummary(pht('Control how mail is sent.'))
->setDescription($adapter_description),
$this->newOption(
'metamta.one-mail-per-recipient',
'bool',
true)
->setBoolOptions(
array(
pht('Send Mail To Each Recipient'),
pht('Send Mail To All Recipients'),
))
->setSummary(
pht(
'Controls whether Phabricator sends one email with multiple '.
'recipients in the "To:" line, or multiple emails, each with a '.
'single recipient in the "To:" line.'))
->setDescription($one_mail_per_recipient_desc),
$this->newOption('metamta.can-send-as-user', 'bool', false)
->setBoolOptions(
array(
pht('Send as User Taking Action'),
pht('Send as Phabricator'),
))
->setSummary(
pht(
'Controls whether Phabricator sends email "From" users.'))
->setDescription($send_as_user_desc),
$this->newOption(
'metamta.reply-handler-domain',
'string',
null)
->setLocked(true)
- ->setDescription(pht(
- 'Domain used for reply email addresses. Some applications can '.
- 'override this configuration with a different domain.'))
+ ->setDescription(pht('Domain used for reply email addresses.'))
->addExample('phabricator.example.com', ''),
$this->newOption('metamta.herald.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Herald Hints'),
pht('No Herald Hints'),
))
->setSummary(pht('Show hints about Herald rules in email.'))
->setDescription($herald_hints_description),
$this->newOption('metamta.recipients.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Recipient Hints'),
pht('No Recipient Hints'),
))
->setSummary(pht('Show "To:" and "Cc:" footer hints in email.'))
->setDescription($recipient_hints_description),
$this->newOption('metamta.email-preferences', 'bool', true)
->setBoolOptions(
array(
pht('Show Email Preferences Link'),
pht('No Email Preferences Link'),
))
->setSummary(pht('Show email preferences link in email.'))
->setDescription($email_preferences_description),
$this->newOption('metamta.precedence-bulk', 'bool', false)
->setBoolOptions(
array(
pht('Add "Precedence: bulk" Header'),
pht('No "Precedence: bulk" Header'),
))
->setSummary(pht('Control the "Precedence: bulk" header.'))
->setDescription($bulk_description),
$this->newOption('metamta.re-prefix', 'bool', false)
->setBoolOptions(
array(
pht('Force "Re:" Subject Prefix'),
pht('No "Re:" Subject Prefix'),
))
->setSummary(pht('Control "Re:" subject prefix, for Mail.app.'))
->setDescription($re_prefix_description),
$this->newOption('metamta.vary-subjects', 'bool', true)
->setBoolOptions(
array(
pht('Allow Varied Subjects'),
pht('Always Use the Same Thread Subject'),
))
->setSummary(pht('Control subject variance, for some mail clients.'))
->setDescription($vary_subjects_description),
$this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false)
->setBoolOptions(
array(
pht('Allow Insecure Reply-To Auth'),
pht('Disallow Reply-To Auth'),
))
->setSummary(pht('Trust "Reply-To" headers for authentication.'))
->setDescription($reply_to_description),
$this->newOption('metamta.placeholder-to-recipient', 'string', null)
->setSummary(pht('Placeholder for mail with only CCs.'))
->setDescription($placeholder_description),
$this->newOption('metamta.public-replies', 'bool', false)
->setBoolOptions(
array(
pht('Use Public Replies (Less Secure)'),
pht('Use Private Replies (More Secure)'),
))
->setSummary(
pht(
'Phabricator can use less-secure but mailing list friendly public '.
'reply addresses.'))
->setDescription($public_replies_description),
$this->newOption('metamta.single-reply-handler-prefix', 'string', null)
->setSummary(
pht('Allow Phabricator to use a single mailbox for all replies.'))
->setDescription($single_description),
$this->newOption('metamta.user-address-format', 'enum', 'full')
->setEnumOptions(
array(
'short' => 'short',
'real' => 'real',
'full' => 'full',
))
->setSummary(pht('Control how Phabricator renders user names in mail.'))
->setDescription($address_description)
->addExample('gwashington <gwashington@example.com>', 'short')
->addExample('George Washington <gwashington@example.com>', 'real')
->addExample(
'gwashington (George Washington) <gwashington@example.com>',
'full'),
$this->newOption('metamta.email-body-limit', 'int', 524288)
->setDescription(
pht(
'You can set a limit for the maximum byte size of outbound mail. '.
'Mail which is larger than this limit will be truncated before '.
'being sent. This can be useful if your MTA rejects mail which '.
'exceeds some limit (this is reasonably common). Specify a value '.
'in bytes.'))
->setSummary(pht('Global cap for size of generated emails (bytes).'))
->addExample(524288, pht('Truncate at 512KB'))
->addExample(1048576, pht('Truncate at 1MB')),
);
}
}
diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
index 72596bf71a..08492aa644 100644
--- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
+++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
@@ -1,318 +1,311 @@
<?php
final class PhabricatorDifferentialConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Differential');
}
public function getDescription() {
return pht('Configure Differential code review.');
}
public function getFontIcon() {
return 'fa-cog';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$caches_href = PhabricatorEnv::getDoclink('Managing Caches');
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields = array(
new DifferentialTitleField(),
new DifferentialSummaryField(),
new DifferentialTestPlanField(),
new DifferentialAuthorField(),
new DifferentialReviewersField(),
new DifferentialProjectReviewersField(),
new DifferentialReviewedByField(),
new DifferentialSubscribersField(),
new DifferentialRepositoryField(),
new DifferentialLintField(),
new DifferentialProjectsField(),
new DifferentialUnitField(),
new DifferentialViewPolicyField(),
new DifferentialEditPolicyField(),
new DifferentialDependsOnField(),
new DifferentialDependenciesField(),
new DifferentialManiphestTasksField(),
new DifferentialCommitsField(),
new DifferentialJIRAIssuesField(),
new DifferentialAsanaRepresentationField(),
new DifferentialChangesSinceLastUpdateField(),
new DifferentialBranchField(),
new DifferentialBlameRevisionField(),
new DifferentialPathField(),
new DifferentialHostField(),
new DifferentialRevertPlanField(),
new DifferentialApplyPatchField(),
new DifferentialRevisionIDField(),
);
$default_fields = array();
foreach ($fields as $field) {
$default_fields[$field->getFieldKey()] = array(
'disabled' => $field->shouldDisableByDefault(),
);
}
return array(
$this->newOption(
'differential.fields',
$custom_field_type,
$default_fields)
->setCustomData(
id(new DifferentialRevision())->getCustomFieldBaseClass())
->setDescription(
pht(
"Select and reorder revision fields.\n\n".
"NOTE: This feature is under active development and subject ".
"to change.")),
$this->newOption(
'differential.whitespace-matters',
'list<regex>',
array(
'/\.py$/',
'/\.l?hs$/',
))
->setDescription(
pht(
"List of file regexps where whitespace is meaningful and should ".
"not use 'ignore-all' by default")),
$this->newOption('differential.require-test-plan-field', 'bool', true)
->setBoolOptions(
array(
pht("Require 'Test Plan' field"),
pht("Make 'Test Plan' field optional"),
))
->setSummary(pht('Require "Test Plan" field?'))
->setDescription(
pht(
"Differential has a required 'Test Plan' field by default. You ".
"can make it optional by setting this to false. You can also ".
"completely remove it above, if you prefer.")),
$this->newOption('differential.enable-email-accept', 'bool', false)
->setBoolOptions(
array(
pht('Enable Email "!accept" Action'),
pht('Disable Email "!accept" Action'),
))
->setSummary(pht('Enable or disable "!accept" action via email.'))
->setDescription(
pht(
'If inbound email is configured, users can interact with '.
'revisions by using "!actions" in email replies (for example, '.
'"!resign" or "!rethink"). However, by default, users may not '.
'"!accept" revisions via email: email authentication can be '.
'configured to be very weak, and email "!accept" is kind of '.
'sketchy and implies the revision may not actually be receiving '.
'thorough review. You can enable "!accept" by setting this '.
'option to true.')),
$this->newOption('differential.generated-paths', 'list<regex>', array())
->setSummary(pht('File regexps to treat as automatically generated.'))
->setDescription(
pht(
'List of file regexps that should be treated as if they are '.
'generated by an automatic process, and thus be hidden by '.
'default in Differential.'.
"\n\n".
'NOTE: This property is cached, so you will need to purge the '.
'cache after making changes if you want the new configuration '.
'to affect existing revisions. For instructions, see '.
'**[[ %s | Managing Caches ]]** in the documentation.',
$caches_href))
->addExample("/config\.h$/\n#/autobuilt/#", pht('Valid Setting')),
$this->newOption('differential.sticky-accept', 'bool', true)
->setBoolOptions(
array(
pht('Accepts persist across updates'),
pht('Accepts are reset by updates'),
))
->setSummary(
pht('Should "Accepted" revisions remain "Accepted" after updates?'))
->setDescription(
pht(
'Normally, when revisions that have been "Accepted" are updated, '.
'they remain "Accepted". This allows reviewers to suggest minor '.
'alterations when accepting, and encourages authors to update '.
'if they make minor changes in response to this feedback.'.
"\n\n".
'If you want updates to always require re-review, you can disable '.
'the "stickiness" of the "Accepted" status with this option. '.
'This may make the process for minor changes much more burdensome '.
'to both authors and reviewers.')),
$this->newOption('differential.allow-self-accept', 'bool', false)
->setBoolOptions(
array(
pht('Allow self-accept'),
pht('Disallow self-accept'),
))
->setSummary(pht('Allows users to accept their own revisions.'))
->setDescription(
pht(
"If you set this to true, users can accept their own revisions. ".
"This action is disabled by default because it's most likely not ".
"a behavior you want, but it proves useful if you are working ".
"alone on a project and want to make use of all of ".
"differential's features.")),
$this->newOption('differential.always-allow-close', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to close accepted revisions.'))
->setDescription(
pht(
'If you set this to true, any user can close any revision so '.
'long as it has been accepted. This can be useful depending on '.
'your development model. For example, github-style pull requests '.
'where the reviewer is often the actual committer can benefit '.
'from turning this option to true. If false, only the submitter '.
'can close a revision.')),
$this->newOption('differential.always-allow-abandon', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to abandon revisions.'))
->setDescription(
pht(
'If you set this to true, any user can abandon any revision. If '.
'false, only the submitter can abandon a revision.')),
$this->newOption('differential.allow-reopen', 'bool', false)
->setBoolOptions(
array(
pht('Enable reopen'),
pht('Disable reopen'),
))
->setSummary(pht('Allows any user to reopen a closed revision.'))
->setDescription(
pht('If you set this to true, any user can reopen a revision so '.
'long as it has been closed. This can be useful if a revision '.
'is accidentally closed or if a developer changes his or her '.
'mind after closing a revision. If it is false, reopening '.
'is not allowed.')),
$this->newOption('differential.close-on-accept', 'bool', false)
->setBoolOptions(
array(
pht('Treat Accepted Revisions as "Closed"'),
pht('Treat Accepted Revisions as "Open"'),
))
->setSummary(pht('Allows "Accepted" to act as a closed status.'))
->setDescription(
pht(
'Normally, Differential revisions remain on the dashboard when '.
'they are "Accepted", and the author then commits the changes '.
'to "Close" the revision and move it off the dashboard.'.
"\n\n".
'If you have an unusual workflow where Differential is used for '.
'post-commit review (normally called "Audit", elsewhere in '.
'Phabricator), you can set this flag to treat the "Accepted" '.
'state as a "Closed" state and end the review workflow early.'.
"\n\n".
'This sort of workflow is very unusual. Very few installs should '.
'need to change this option.')),
$this->newOption('differential.days-fresh', 'int', 1)
->setSummary(
pht(
"For how many business days should a revision be considered ".
"'fresh'?"))
->setDescription(
pht(
'Revisions newer than this number of days are marked as fresh in '.
'Action Required and Revisions Waiting on You views. Only work '.
'days (not weekends and holidays) are included. Set to 0 to '.
'disable this feature.')),
$this->newOption('differential.days-stale', 'int', 3)
->setSummary(
pht("After this many days, a revision will be considered 'stale'."))
->setDescription(
pht(
"Similar to `differential.days-fresh` but marks stale revisions. ".
"If the revision is even older than it is when marked as 'old'.")),
- $this->newOption(
- 'metamta.differential.reply-handler-domain',
- 'string',
- null)
- ->setLocked(true)
- ->setDescription(
- pht('Inbound email domain for Differential replies.')),
$this->newOption(
'metamta.differential.reply-handler',
'class',
'DifferentialReplyHandler')
->setLocked(true)
->setBaseClass('PhabricatorMailReplyHandler')
->setDescription(pht('Alternate reply handler class.')),
$this->newOption(
'metamta.differential.subject-prefix',
'string',
'[Differential]')
->setDescription(pht('Subject prefix for Differential mail.')),
$this->newOption(
'metamta.differential.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
->setSummary(pht('Attach patches to email, as text attachments.'))
->setDescription(
pht(
'If you set this to true, Phabricator will attach patches to '.
'Differential mail (as text attachments). This will not work if '.
'you are using SendGrid as your mail adapter.')),
$this->newOption(
'metamta.differential.inline-patches',
'int',
0)
->setSummary(pht('Inline patches in email, as body text.'))
->setDescription(
pht(
"To include patches inline in email bodies, set this to a ".
"positive integer. Patches will be inlined if they are at most ".
"that many lines. For instance, a value of 100 means 'inline ".
"patches if they are no longer than 100 lines'. By default, ".
"patches are not inlined.")),
// TODO: Implement 'enum'? Options are 'unified' or 'git'.
$this->newOption(
'metamta.differential.patch-format',
'string',
'unified')
->setDescription(
pht("Format for inlined or attached patches: 'git' or 'unified'.")),
$this->newOption(
'metamta.differential.unified-comment-context',
'bool',
false)
->setBoolOptions(
array(
pht('Show context'),
pht('Do not show context'),
))
->setSummary(pht('Show diff context around inline comments in email.'))
->setDescription(
pht(
'Normally, inline comments in emails are shown with a file and '.
'line but without any diff context. Enabling this option adds '.
'diff context and the comment thread.')),
);
}
}
diff --git a/src/applications/differential/mail/DifferentialReplyHandler.php b/src/applications/differential/mail/DifferentialReplyHandler.php
index 05a92cd4ff..c9bca29d2f 100644
--- a/src/applications/differential/mail/DifferentialReplyHandler.php
+++ b/src/applications/differential/mail/DifferentialReplyHandler.php
@@ -1,112 +1,107 @@
<?php
final class DifferentialReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof DifferentialRevision)) {
throw new Exception('Receiver is not a DifferentialRevision!');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'D');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('D');
}
- public function getReplyHandlerDomain() {
- return $this->getCustomReplyHandlerDomainIfExists(
- 'metamta.differential.reply-handler-domain');
- }
-
public function getSupportedCommands() {
$actions = array(
DifferentialAction::ACTION_COMMENT,
DifferentialAction::ACTION_REJECT,
DifferentialAction::ACTION_ABANDON,
DifferentialAction::ACTION_RECLAIM,
DifferentialAction::ACTION_RESIGN,
DifferentialAction::ACTION_RETHINK,
'unsubscribe',
);
if (PhabricatorEnv::getEnvConfig('differential.enable-email-accept')) {
$actions[] = DifferentialAction::ACTION_ACCEPT;
}
return $actions;
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$revision = $this->getMailReceiver();
$viewer = $this->getActor();
$body_data = $mail->parseBody();
$body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$template = id(new DifferentialTransaction());
$commands = $body_data['commands'];
foreach ($commands as $command_argv) {
$command = head($command_argv);
switch ($command) {
case 'unsubscribe':
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'-' => array($viewer->getPHID()),
));
break;
case DifferentialAction::ACTION_ACCEPT:
$accept_key = 'differential.enable-email-accept';
$can_accept = PhabricatorEnv::getEnvConfig($accept_key);
if (!$can_accept) {
throw new Exception(
pht(
'You can not !accept revisions over email because '.
'Differential is configured to disallow this.'));
}
// Fall through...
case DifferentialAction::ACTION_REJECT:
case DifferentialAction::ACTION_ABANDON:
case DifferentialAction::ACTION_RECLAIM:
case DifferentialAction::ACTION_RESIGN:
case DifferentialAction::ACTION_RETHINK:
case DifferentialAction::ACTION_CLAIM:
case DifferentialAction::ACTION_REOPEN:
$xactions[] = id(clone $template)
->setTransactionType(DifferentialTransaction::TYPE_ACTION)
->setNewValue($command);
break;
}
}
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new DifferentialTransactionComment())
->setContent($body));
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setContentSource($content_source)
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($revision, $xactions);
}
}
diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
index 8bf13db16e..f6dfb92762 100644
--- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
+++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
@@ -1,143 +1,134 @@
<?php
final class PhabricatorDiffusionConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Diffusion');
}
public function getDescription() {
return pht('Configure Diffusion repository browsing.');
}
public function getFontIcon() {
return 'fa-code';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
return array(
$this->newOption(
'metamta.diffusion.subject-prefix',
'string',
'[Diffusion]')
->setDescription(pht('Subject prefix for Diffusion mail.')),
- $this->newOption(
- 'metamta.diffusion.reply-handler-domain',
- 'string',
- null)
- ->setLocked(true)
- ->setDescription(
- pht(
- 'See {{metamta.maniphest.reply-handler}}. This does the same '.
- 'thing, but affects Diffusion.')),
$this->newOption(
'metamta.diffusion.reply-handler',
'class',
'PhabricatorAuditReplyHandler')
->setLocked(true)
->setBaseClass('PhabricatorMailReplyHandler')
->setDescription(pht('Override mail reply handler class.')),
$this->newOption(
'metamta.diffusion.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
->setDescription(pht(
'Set this to true if you want patches to be attached to commit '.
'notifications from Diffusion.')),
$this->newOption('metamta.diffusion.inline-patches', 'int', 0)
->setSummary(pht('Include patches in Diffusion mail as body text.'))
->setDescription(
pht(
'To include patches in Diffusion email bodies, set this to a '.
'positive integer. Patches will be inlined if they are at most '.
'that many lines. By default, patches are not inlined.')),
$this->newOption('metamta.diffusion.byte-limit', 'int', 1024 * 1024)
->setDescription(pht('Hard byte limit on including patches in email.')),
$this->newOption('metamta.diffusion.time-limit', 'int', 60)
->setDescription(pht('Hard time limit on generating patches.')),
$this->newOption(
'audit.can-author-close-audit',
'bool',
false)
->setBoolOptions(
array(
pht('Enable Closing Audits'),
pht('Disable Closing Audits'),
))
->setDescription(pht('Controls whether Author can Close Audits.')),
$this->newOption('bugtraq.url', 'string', '')
->addExample('https://bugs.php.net/%BUGID%', pht('PHP bugs'))
->addExample('/%BUGID%', pht('Local Maniphest URL'))
->setDescription(pht(
'URL of external bug tracker used by Diffusion. %s will be '.
'substituted by the bug ID.',
'%BUGID%')),
$this->newOption('bugtraq.logregex', 'list<regex>', array())
->addExample(array('/\B#([1-9]\d*)\b/'), pht('Issue #123'))
->addExample(
array('/[Ii]ssues?:?(\s*,?\s*#\d+)+/', '/(\d+)/'),
pht('Issue #123, #456'))
->addExample(array('/(?<!#)\b(T[1-9]\d*)\b/'), pht('Task T123'))
->addExample('/[A-Z]{2,}-\d+/', pht('JIRA-1234'))
->setDescription(pht(
'Regular expression to link external bug tracker. See '.
'http://tortoisesvn.net/docs/release/TortoiseSVN_en/'.
'tsvn-dug-bugtracker.html for further explanation.')),
$this->newOption('diffusion.allow-http-auth', 'bool', false)
->setBoolOptions(
array(
pht('Allow HTTP Basic Auth'),
pht('Disable HTTP Basic Auth'),
))
->setSummary(pht('Enable HTTP Basic Auth for repositories.'))
->setDescription(
pht(
"Phabricator can serve repositories over HTTP, using HTTP basic ".
"auth.\n\n".
"Because HTTP basic auth is less secure than SSH auth, it is ".
"disabled by default. You can enable it here if you'd like to use ".
"it anyway. There's nothing fundamentally insecure about it as ".
"long as Phabricator uses HTTPS, but it presents a much lower ".
"barrier to attackers than SSH does.\n\n".
"Consider using SSH for authenticated access to repositories ".
"instead of HTTP.")),
$this->newOption('diffusion.ssh-user', 'string', null)
->setLocked(true)
->setSummary(pht('Login username for SSH connections to repositories.'))
->setDescription(
pht(
'When constructing clone URIs to show to users, Diffusion will '.
'fill in this login username. If you have configured a VCS user '.
'like `git`, you should provide it here.')),
$this->newOption('diffusion.ssh-port', 'int', null)
->setLocked(true)
->setSummary(pht('Port for SSH connections to repositories.'))
->setDescription(
pht(
'When constructing clone URIs to show to users, Diffusion by '.
'default will not display a port assuming the default for your '.
'VCS. Explicitly declare when running on a non-standard port.')),
$this->newOption('diffusion.ssh-host', 'string', null)
->setLocked(true)
->setSummary(pht('Host for SSH connections to repositories.'))
->setDescription(
pht(
'If you accept Phabricator SSH traffic on a different host '.
'from web traffic (for example, if you use different SSH and '.
'web load balancers), you can set the SSH hostname here. This '.
'is an advanced option.')),
);
}
}
diff --git a/src/applications/macro/config/PhabricatorMacroConfigOptions.php b/src/applications/macro/config/PhabricatorMacroConfigOptions.php
index 95f322d96d..2ab913a733 100644
--- a/src/applications/macro/config/PhabricatorMacroConfigOptions.php
+++ b/src/applications/macro/config/PhabricatorMacroConfigOptions.php
@@ -1,33 +1,29 @@
<?php
final class PhabricatorMacroConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Macro');
}
public function getDescription() {
return pht('Configure Macro.');
}
public function getFontIcon() {
return 'fa-file-image-o';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
return array(
- $this->newOption('metamta.macro.reply-handler-domain', 'string', null)
- ->setLocked(true)
- ->setDescription(pht(
- 'As {{metamta.maniphest.reply-handler-domain}}, but affects Macro.')),
$this->newOption('metamta.macro.subject-prefix', 'string', '[Macro]')
->setDescription(pht('Subject prefix for Macro email.')),
);
}
}
diff --git a/src/applications/macro/mail/PhabricatorMacroReplyHandler.php b/src/applications/macro/mail/PhabricatorMacroReplyHandler.php
index 1b70b8872a..380b3cb3d0 100644
--- a/src/applications/macro/mail/PhabricatorMacroReplyHandler.php
+++ b/src/applications/macro/mail/PhabricatorMacroReplyHandler.php
@@ -1,30 +1,25 @@
<?php
final class PhabricatorMacroReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorFileImageMacro)) {
throw new Exception('Mail receiver is not a PhabricatorFileImageMacro!');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'MCRO');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('MCRO');
}
- public function getReplyHandlerDomain() {
- return $this->getCustomReplyHandlerDomainIfExists(
- 'metamta.macro.reply-handler-domain');
- }
-
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// TODO: Implement this.
return null;
}
}
diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
index a77f4ef089..cef998fb66 100644
--- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
+++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
@@ -1,383 +1,362 @@
<?php
final class PhabricatorManiphestConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Maniphest');
}
public function getDescription() {
return pht('Configure Maniphest.');
}
public function getFontIcon() {
return 'fa-anchor';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$priority_defaults = array(
100 => array(
'name' => pht('Unbreak Now!'),
'short' => pht('Unbreak!'),
'color' => 'indigo',
),
90 => array(
'name' => pht('Needs Triage'),
'short' => pht('Triage'),
'color' => 'violet',
),
80 => array(
'name' => pht('High'),
'short' => pht('High'),
'color' => 'red',
),
50 => array(
'name' => pht('Normal'),
'short' => pht('Normal'),
'color' => 'orange',
),
25 => array(
'name' => pht('Low'),
'short' => pht('Low'),
'color' => 'yellow',
),
0 => array(
'name' => pht('Wishlist'),
'short' => pht('Wish'),
'color' => 'sky',
),
);
$status_type = 'custom:ManiphestStatusConfigOptionType';
$status_defaults = array(
'open' => array(
'name' => pht('Open'),
'special' => ManiphestTaskStatus::SPECIAL_DEFAULT,
),
'resolved' => array(
'name' => pht('Resolved'),
'name.full' => pht('Closed, Resolved'),
'closed' => true,
'special' => ManiphestTaskStatus::SPECIAL_CLOSED,
'prefixes' => array(
'closed',
'closes',
'close',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved',
),
'suffixes' => array(
'as resolved',
'as fixed',
),
),
'wontfix' => array(
'name' => pht('Wontfix'),
'name.full' => pht('Closed, Wontfix'),
'closed' => true,
'prefixes' => array(
'wontfix',
'wontfixes',
'wontfixed',
),
'suffixes' => array(
'as wontfix',
),
),
'invalid' => array(
'name' => pht('Invalid'),
'name.full' => pht('Closed, Invalid'),
'closed' => true,
'prefixes' => array(
'invalidate',
'invalidates',
'invalidated',
),
'suffixes' => array(
'as invalid',
),
),
'duplicate' => array(
'name' => pht('Duplicate'),
'name.full' => pht('Closed, Duplicate'),
'transaction.icon' => 'fa-times',
'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE,
'closed' => true,
),
'spite' => array(
'name' => pht('Spite'),
'name.full' => pht('Closed, Spite'),
'name.action' => pht('Spited'),
'transaction.icon' => 'fa-thumbs-o-down',
'silly' => true,
'closed' => true,
'prefixes' => array(
'spite',
'spites',
'spited',
),
'suffixes' => array(
'out of spite',
'as spite',
),
),
);
$status_description = $this->deformat(pht(<<<EOTEXT
Allows you to edit, add, or remove the task statuses available in Maniphest,
like "Open", "Resolved" and "Invalid". The configuration should contain a map
of status constants to status specifications (see defaults below for examples).
The constant for each status should be 1-12 characters long and contain only
lowercase letters and digits. Valid examples are "open", "closed", and
"invalid". Users will not normally see these values.
The keys you can provide in a specification are:
- `name` //Required string.// Name of the status, like "Invalid".
- `name.full` //Optional string.// Longer name, like "Closed, Invalid". This
appears on the task detail view in the header.
- `name.action` //Optional string.// Action name for email subjects, like
"Marked Invalid".
- `closed` //Optional bool.// Statuses are either "open" or "closed".
Specifying `true` here will mark the status as closed (like "Resolved" or
"Invalid"). By default, statuses are open.
- `special` //Optional string.// Mark this status as special. The special
statuses are:
- `default` This is the default status for newly created tasks. You must
designate one status as default, and it must be an open status.
- `closed` This is the default status for closed tasks (for example, tasks
closed via the "!close" action in email or via the quick close button in
Maniphest). You must designate one status as the default closed status,
and it must be a closed status.
- `duplicate` This is the status used when tasks are merged into one
another as duplicates. You must designate one status for duplicates,
and it must be a closed status.
- `transaction.icon` //Optional string.// Allows you to choose a different
icon to use for this status when showing status changes in the transaction
log. Please see UIExamples, Icons and Images for a list.
- `transaction.color` //Optional string.// Allows you to choose a different
color to use for this status when showing status changes in the transaction
log.
- `silly` //Optional bool.// Marks this status as silly, and thus wholly
inappropriate for use by serious businesses.
- `prefixes` //Optional list<string>.// Allows you to specify a list of
text prefixes which will trigger a task transition into this status
when mentioned in a commit message. For example, providing "closes" here
will allow users to move tasks to this status by writing `Closes T123` in
commit messages.
- `suffixes` //Optional list<string>.// Allows you to specify a list of
text suffixes which will trigger a task transition into this status
when mentioned in a commit message, after a valid prefix. For example,
providing "as invalid" here will allow users to move tasks
to this status by writing `Closes T123 as invalid`, even if another status
is selected by the "Closes" prefix.
Statuses will appear in the UI in the order specified. Note the status marked
`special` as `duplicate` is not settable directly and will not appear in UI
elements, and that any status marked `silly` does not appear if Phabricator
is configured with `phabricator.serious-business` set to true.
Examining the default configuration and examples below will probably be helpful
in understanding these options.
EOTEXT
));
$status_example = array(
'open' => array(
'name' => 'Open',
'special' => 'default',
),
'closed' => array(
'name' => 'Closed',
'special' => 'closed',
'closed' => true,
),
'duplicate' => array(
'name' => 'Duplicate',
'special' => 'duplicate',
'closed' => true,
),
);
$json = new PhutilJSON();
$status_example = $json->encodeFormatted($status_example);
// This is intentionally blank for now, until we can move more Maniphest
// logic to custom fields.
$default_fields = array();
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
return array(
$this->newOption('maniphest.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Maniphest fields.'))
->setDescription(
pht(
'Array of custom fields for Maniphest tasks. For details on '.
'adding custom fields to Maniphest, see "Configuring Custom '.
'Fields" in the documentation.'))
->addExample(
'{"mycompany:estimated-hours": {"name": "Estimated Hours", '.
'"type": "int", "caption": "Estimated number of hours this will '.
'take."}}',
pht('Valid Setting')),
$this->newOption('maniphest.fields', $custom_field_type, $default_fields)
->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder task fields.')),
$this->newOption('maniphest.priorities', 'wild', $priority_defaults)
->setSummary(pht('Configure Maniphest priority names.'))
->setDescription(
pht(
'Allows you to edit or override the default priorities available '.
'in Maniphest, like "High", "Normal" and "Low". The configuration '.
'should contain a map of priority constants to priority '.
'specifications (see defaults below for examples).'.
"\n\n".
'The keys you can define for a priority are:'.
"\n\n".
' - `name` Name of the priority.'."\n".
' - `short` Alternate shorter name, used in UIs where there is '.
' not much space available.'."\n".
' - `color` A color for this priority, like "red" or "blue".'.
"\n\n".
'You can choose which priority is the default for newly created '.
'tasks with `maniphest.default-priority`.')),
$this->newOption('maniphest.statuses', $status_type, $status_defaults)
->setSummary(pht('Configure Maniphest task statuses.'))
->setDescription($status_description)
->addExample($status_example, pht('Minimal Valid Config')),
$this->newOption('maniphest.default-priority', 'int', 90)
->setSummary(pht('Default task priority for create flows.'))
->setDescription(
pht(
'Choose a default priority for newly created tasks. You can '.
'review and adjust available priorities by using the '.
'{{maniphest.priorities}} configuration option. The default value '.
'(`90`) corresponds to the default "Needs Triage" priority.')),
- $this->newOption(
- 'metamta.maniphest.reply-handler-domain',
- 'string',
- null)
- ->setLocked(true)
- ->setSummary(pht('Enable replying to tasks via email.'))
- ->setDescription(
- pht(
- 'You can configure a reply handler domain so that email sent from '.
- 'Maniphest will have a special "Reply To" address like '.
- '"T123+82+af19f@example.com" that allows recipients to reply by '.
- 'email and interact with tasks. For instructions on configurating '.
- 'reply handlers, see the article "Configuring Inbound Email" in '.
- 'the Phabricator documentation. By default, this is set to `null` '.
- 'and Phabricator will use a generic `noreply@` address or the '.
- 'address of the acting user instead of a special reply handler '.
- 'address (see `metamta.default-address`). If you set a domain '.
- 'here, Phabricator will begin generating private reply handler '.
- 'addresses. See also `metamta.maniphest.reply-handler` to further '.
- 'configure behavior. This key should be set to the domain part '.
- 'after the @, like "example.com".')),
$this->newOption(
'metamta.maniphest.reply-handler',
'class',
'ManiphestReplyHandler')
->setLocked(true)
->setBaseClass('PhabricatorMailReplyHandler')
->setDescription(pht('Override reply handler class.')),
$this->newOption(
'metamta.maniphest.subject-prefix',
'string',
'[Maniphest]')
->setDescription(pht('Subject prefix for Maniphest mail.')),
$this->newOption(
'metamta.maniphest.public-create-email',
'string',
null)
->setLocked(true)
->setLockedMessage(pht(
'This configuration is deprecated. See description for details.'))
->setSummary(pht('DEPRECATED - Allow filing bugs via email.'))
->setDescription(
pht(
'This config has been deprecated in favor of [[ '.
'/applications/view/PhabricatorManiphestApplication/ | '.
'application settings ]], which allow for multiple email '.
'addresses and other functionality.'."\n\n".
'You can configure an email address like '.
'"bugs@phabricator.example.com" which will automatically create '.
'Maniphest tasks when users send email to it. This relies on the '.
'"From" address to authenticate users, so it is is not completely '.
'secure. To set this up, enter a complete email address like '.
'"bugs@phabricator.example.com" and then configure mail to that '.
'address so it routed to Phabricator (if you\'ve already '.
'configured reply handlers, you\'re probably already done). See '.
'"Configuring Inbound Email" in the documentation for more '.
'information.')),
$this->newOption(
'metamta.maniphest.default-public-author',
'string',
null)
->setLocked(true)
->setLockedMessage(pht(
'This configuration is deprecated. See description for details.'))
->setSummary(pht(
'DEPRECATED - Username anonymous bugs are filed under.'))
->setDescription(
pht(
'This config has been deprecated in favor of [[ '.
'/applications/view/PhabricatorManiphestApplication/ | '.
'application settings ]], which allow for multiple email '.
'addresses each with its own default author, and other '.
'functionality.'."\n\n".
'If you enable `metamta.maniphest.public-create-email` and create '.
'an email address like "bugs@phabricator.example.com", it will '.
'default to rejecting mail which doesn\'t come from a known user. '.
'However, you might want to let anyone send email to this '.
'address; to do so, set a default author here (a Phabricator '.
'username). A typical use of this might be to create a "System '.
'Agent" user called "bugs" and use that name here. If you specify '.
'a valid username, mail will always be accepted and used to '.
'create a task, even if the sender is not a system user. The '.
'original email address will be stored in an `From Email` field '.
'on the task.')),
$this->newOption(
'maniphest.priorities.unbreak-now',
'int',
100)
->setSummary(pht('Priority used to populate "Unbreak Now" on home.'))
->setDescription(
pht(
'Temporary setting. If set, this priority is used to populate the '.
'"Unbreak Now" panel on the home page. You should adjust this if '.
'you adjust priorities using `maniphest.priorities`.')),
$this->newOption(
'maniphest.priorities.needs-triage',
'int',
90)
->setSummary(pht('Priority used to populate "Needs Triage" on home.'))
->setDescription(
pht(
'Temporary setting. If set, this priority is used to populate the '.
'"Needs Triage" panel on the home page. You should adjust this if '.
'you adjust priorities using `maniphest.priorities`.')),
);
}
}
diff --git a/src/applications/maniphest/mail/ManiphestReplyHandler.php b/src/applications/maniphest/mail/ManiphestReplyHandler.php
index e4bfc5c319..78f9dae251 100644
--- a/src/applications/maniphest/mail/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/mail/ManiphestReplyHandler.php
@@ -1,188 +1,183 @@
<?php
final class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ManiphestTask)) {
throw new Exception('Mail receiver is not a ManiphestTask!');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('T');
}
- public function getReplyHandlerDomain() {
- return $this->getCustomReplyHandlerDomainIfExists(
- 'metamta.maniphest.reply-handler-domain');
- }
-
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// NOTE: We'll drop in here on both the "reply to a task" and "create a
// new task" workflows! Make sure you test both if you make changes!
$task = $this->getMailReceiver();
$is_new_task = !$task->getID();
$user = $this->getActor();
$body_data = $mail->parseBody();
$body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$template = new ManiphestTransaction();
$is_unsub = false;
if ($is_new_task) {
$task = ManiphestTask::initializeNewTask($user);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_STATUS)
->setNewValue(ManiphestTaskStatus::getDefaultStatus());
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setNewValue(nonempty($mail->getSubject(), pht('Untitled Task')));
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION)
->setNewValue($body);
} else {
$commands = $body_data['commands'];
// TODO: Support multiple commands.
if ($commands) {
$command_argv = head($commands);
} else {
$command_argv = array();
}
$command = idx($command_argv, 0);
$command_value = idx($command_argv, 1);
$ttype = PhabricatorTransactions::TYPE_COMMENT;
$new_value = null;
switch ($command) {
case 'close':
$ttype = ManiphestTransaction::TYPE_STATUS;
$new_value = ManiphestTaskStatus::getDefaultClosedStatus();
break;
case 'claim':
$ttype = ManiphestTransaction::TYPE_OWNER;
$new_value = $user->getPHID();
break;
case 'assign':
$ttype = ManiphestTransaction::TYPE_OWNER;
if ($command_value) {
$assign_users = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withUsernames(array($command_value))
->execute();
if ($assign_users) {
$assign_user = head($assign_users);
$new_value = $assign_user->getPHID();
}
}
// assign to the user by default
if (!$new_value) {
$new_value = $user->getPHID();
}
break;
case 'unsubscribe':
$is_unsub = true;
$ttype = PhabricatorTransactions::TYPE_SUBSCRIBERS;
$ccs = $task->getSubscriberPHIDs();
foreach ($ccs as $k => $phid) {
if ($phid == $user->getPHID()) {
unset($ccs[$k]);
}
}
$new_value = array('=' => array_values($ccs));
break;
}
if ($ttype != PhabricatorTransactions::TYPE_COMMENT) {
$xaction = clone $template;
$xaction->setTransactionType($ttype);
$xaction->setNewValue($new_value);
$xactions[] = $xaction;
}
if (strlen($body)) {
$xaction = clone $template;
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($body));
$xactions[] = $xaction;
}
}
$ccs = $mail->loadCCPHIDs();
$old_ccs = $task->getSubscriberPHIDs();
$new_ccs = array_merge($old_ccs, $ccs);
if (!$is_unsub) {
$new_ccs[] = $user->getPHID();
}
$new_ccs = array_unique($new_ccs);
if (array_diff($new_ccs, $old_ccs)) {
$cc_xaction = clone $template;
$cc_xaction->setTransactionType(
PhabricatorTransactions::TYPE_SUBSCRIBERS);
$cc_xaction->setNewValue(array('=' => $new_ccs));
$xactions[] = $cc_xaction;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'mail' => $mail,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$xactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setParentMessageID($mail->getMessageID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source);
if ($this->getApplicationEmail()) {
$editor->setApplicationEmail($this->getApplicationEmail());
}
$editor->applyTransactions($task, $xactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index d3518fe65c..bc31707f53 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,373 +1,365 @@
<?php
abstract class PhabricatorMailReplyHandler {
private $mailReceiver;
private $applicationEmail;
private $actor;
private $excludePHIDs = array();
final public function setMailReceiver($mail_receiver) {
$this->validateMailReceiver($mail_receiver);
$this->mailReceiver = $mail_receiver;
return $this;
}
final public function getMailReceiver() {
return $this->mailReceiver;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final public function getActor() {
return $this->actor;
}
final public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
final public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle);
+
public function getReplyHandlerDomain() {
- return $this->getDefaultReplyHandlerDomain();
- }
- protected function getCustomReplyHandlerDomainIfExists($config_key) {
- $domain = PhabricatorEnv::getEnvConfig($config_key);
- if ($domain) {
- return $domain;
- }
- return $this->getDefaultReplyHandlerDomain();
- }
- private function getDefaultReplyHandlerDomain() {
- return PhabricatorEnv::getEnvConfig(
- 'metamta.reply-handler-domain');
+ return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
}
abstract protected function receiveEmail(
PhabricatorMetaMTAReceivedMail $mail);
public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
$this->dropEmptyMail($mail);
return $this->receiveEmail($mail);
}
private function dropEmptyMail(PhabricatorMetaMTAReceivedMail $mail) {
$body = $mail->getCleanTextBody();
$attachments = $mail->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($mail->getToAddresses()) == 1) &&
(count($mail->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.'));
}
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain() &&
!$this->supportsPublicReplies();
}
public function supportsPublicReplies() {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
return false;
}
+
if (!$this->getReplyHandlerDomain()) {
return false;
}
+
return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
$this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
final public function getRecipientsSummary(
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$body = '';
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
if ($to_handles) {
$body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
}
if ($cc_handles) {
$body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
}
}
return $body;
}
final public function getRecipientsSummaryHTML(
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
$body = array();
if ($to_handles) {
$body[] = phutil_tag('strong', array(), 'To: ');
$body[] = phutil_implode_html(', ', mpull($to_handles, 'getName'));
$body[] = phutil_tag('br');
}
if ($cc_handles) {
$body[] = phutil_tag('strong', array(), 'Cc: ');
$body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName'));
$body[] = phutil_tag('br');
}
return phutil_tag('div', array(), $body);
} else {
return '';
}
}
final public function multiplexMail(
PhabricatorMetaMTAMail $mail_template,
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$result = array();
// If MetaMTA is configured to always multiplex, skip the single-email
// case.
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
// If private replies are not supported, simply send one email to all
// recipients and CCs. This covers cases where we have no reply handler,
// or we have a public reply handler.
if (!$this->supportsPrivateReplies()) {
$mail = clone $mail_template;
$mail->addTos(mpull($to_handles, 'getPHID'));
$mail->addCCs(mpull($cc_handles, 'getPHID'));
if ($this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
return $result;
}
}
// TODO: This is pretty messy. We should really be doing all of this
// multiplexing in the task queue, but that requires significant rewriting
// in the general case. ApplicationTransactions can do it fairly easily,
// but other mail sites currently can not, so we need to support this
// junky version until they catch up and we can swap things over.
$to_handles = $this->expandRecipientHandles($to_handles);
$cc_handles = $this->expandRecipientHandles($cc_handles);
$tos = mpull($to_handles, null, 'getPHID');
$ccs = mpull($cc_handles, null, 'getPHID');
// Merge all the recipients together. TODO: We could keep the CCs as real
// CCs and send to a "noreply@domain.com" type address, but keep it simple
// for now.
$recipients = $tos + $ccs;
// When multiplexing mail, explicitly include To/Cc information in the
// message body and headers.
$mail_template = clone $mail_template;
$mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
$mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));
$body = $mail_template->getBody();
$body .= "\n";
$body .= $this->getRecipientsSummary($to_handles, $cc_handles);
$html_body = $mail_template->getHTMLBody();
if (strlen($html_body)) {
$html_body .= hsprintf('%s',
$this->getRecipientsSummaryHTML($to_handles, $cc_handles));
}
foreach ($recipients as $phid => $recipient) {
$mail = clone $mail_template;
if (isset($to_handles[$phid])) {
$mail->addTos(array($phid));
} else if (isset($cc_handles[$phid])) {
$mail->addCCs(array($phid));
} else {
// not good - they should be a to or a cc
continue;
}
$mail->setBody($body);
$mail->setHTMLBody($html_body);
$reply_to = null;
if (!$reply_to && $this->supportsPrivateReplies()) {
$reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
}
if (!$reply_to && $this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
}
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
}
return $result;
}
protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$domain = $this->getReplyHandlerDomain();
// We compute a hash using the object's own PHID to prevent an attacker
// from blindly interacting with objects that they haven't ever received
// mail about by just sending to D1@, D2@, etc...
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$receiver->getPHID());
$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
protected function getSingleReplyHandlerPrefix($address) {
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
return ($single_handle_prefix)
? $single_handle_prefix.'+'.$address
: $address;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle,
$prefix) {
if ($handle->getType() != PhabricatorPeopleUserPHIDType::TYPECONST) {
// You must be a real user to get a private reply handler address.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($handle->getPHID()))
->executeOne();
if (!$user) {
// This may happen if a user was subscribed to something, and was then
// deleted.
return null;
}
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $user->getID();
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$handle->getPHID());
$domain = $this->getReplyHandlerDomain();
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
final protected function enhanceBodyWithAttachments(
$body,
array $attachments,
$format = '- {F%d, layout=link}') {
if (!$attachments) {
return $body;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($attachments)
->execute();
// if we have some text then double return before adding our file list
if ($body) {
$body .= "\n\n";
}
foreach ($files as $file) {
$file_str = sprintf($format, $file->getID());
$body .= $file_str."\n";
}
return rtrim($body);
}
private function expandRecipientHandles(array $handles) {
if (!$handles) {
return array();
}
$phids = mpull($handles, 'getPHID');
$results = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($phids)
->executeExpansion();
return id(new PhabricatorHandleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($results)
->execute();
}
}
diff --git a/src/applications/owners/mail/OwnersPackageReplyHandler.php b/src/applications/owners/mail/OwnersPackageReplyHandler.php
index 3eb8a8a3c4..b0f397249e 100644
--- a/src/applications/owners/mail/OwnersPackageReplyHandler.php
+++ b/src/applications/owners/mail/OwnersPackageReplyHandler.php
@@ -1,26 +1,22 @@
<?php
final class OwnersPackageReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorOwnersPackage)) {
throw new Exception('Receiver is not a PhabricatorOwnersPackage!');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return null;
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
- public function getReplyHandlerDomain() {
- return null;
- }
-
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
return;
}
}
diff --git a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php b/src/applications/pholio/config/PhabricatorPholioConfigOptions.php
index 4fe6255769..db079a4b2b 100644
--- a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php
+++ b/src/applications/pholio/config/PhabricatorPholioConfigOptions.php
@@ -1,35 +1,29 @@
<?php
final class PhabricatorPholioConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Pholio');
}
public function getDescription() {
return pht('Configure Pholio.');
}
public function getFontIcon() {
return 'fa-camera-retro';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
return array(
- $this->newOption('metamta.pholio.reply-handler-domain', 'string', null)
- ->setLocked(true)
- ->setDescription(
- pht(
- 'Like {{metamta.maniphest.reply-handler-domain}}, but affects '.
- 'Pholio.')),
$this->newOption('metamta.pholio.subject-prefix', 'string', '[Pholio]')
->setDescription(pht('Subject prefix for Pholio email.')),
);
}
}
diff --git a/src/applications/pholio/mail/PholioReplyHandler.php b/src/applications/pholio/mail/PholioReplyHandler.php
index 0e8249f78d..0c842d8285 100644
--- a/src/applications/pholio/mail/PholioReplyHandler.php
+++ b/src/applications/pholio/mail/PholioReplyHandler.php
@@ -1,30 +1,25 @@
<?php
final class PholioReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PholioMock)) {
throw new Exception('Mail receiver is not a PholioMock!');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'M');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('M');
}
- public function getReplyHandlerDomain() {
- return $this->getCustomReplyHandlerDomainIfExists(
- 'metamta.pholio.reply-handler-domain');
- }
-
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// TODO: Implement this.
return null;
}
}
diff --git a/src/applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php b/src/applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php
index d902396d56..d418fa3445 100644
--- a/src/applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php
+++ b/src/applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php
@@ -1,23 +1,19 @@
<?php
final class PhabricatorRepositoryPushReplyHandler
extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
return;
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return null;
}
- public function getReplyHandlerDomain() {
- return null;
- }
-
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
return;
}
}
diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner
index 5b823741c4..7d57f01e3a 100644
--- a/src/docs/user/configuration/configuring_inbound_email.diviner
+++ b/src/docs/user/configuration/configuring_inbound_email.diviner
@@ -1,256 +1,253 @@
@title Configuring Inbound Email
@group config
This document contains instructions for configuring inbound email, so users
may interact with some Phabricator applications via email.
= Preamble =
This can be extremely difficult to configure correctly. This is doubly true if
you use a local MTA.
There are a few approaches available:
| Receive Mail With | Setup | Cost | Notes |
|--------|-------|------|-------|
| Mailgun | Easy | Cheap | Recommended |
| SendGrid | Easy | Cheap | |
| Local MTA | Extremely Difficult | Free | Strongly discouraged! |
The remainder of this document walks through configuring Phabricator to
receive mail, and then configuring your chosen transport to deliver mail
to Phabricator.
= Configuring Phabricator =
By default, Phabricator uses a `noreply@phabricator.example.com` email address
as the 'From' (configurable with `metamta.default-address`) and sets
'Reply-To' to the user generating the email (e.g., by making a comment), if the
mail was generated by a user action. This means that users can reply (or
reply-all) to email to discuss changes, but the conversation won't be recorded
in Phabricator and users will not be able to take actions like claiming tasks or
requesting changes to revisions.
To change this behavior so that users can interact with objects in Phabricator
over email, change the configuration key `metamta.reply-handler-domain` to some
domain you configure according to the instructions below, e.g.
`phabricator.example.com`. Once you set this key, emails will use a
'Reply-To' like `T123+273+af310f9220ad@phabricator.example.com`, which -- when
configured correctly, according to the instructions below -- will parse incoming
email and allow users to interact with Differential revisions, Maniphest tasks,
etc. over email.
If you don't want Phabricator to take up an entire domain (or subdomain) you
can configure a general prefix so you can use a single mailbox to receive mail
on. To make use of this set `metamta.single-reply-handler-prefix` to the
prefix of your choice, and Phabricator will prepend this to the 'Reply-To'
mail address. This works because everything up to the first (optional) '+'
character in an email-address is considered the receiver, and everything
after is essentially ignored.
You can also set up application email addresses to allow users to create
application objects via email. For example, you could configure
`bugs@phabricator.example.com` to create a Maniphest task out of any email
which is sent to it. To do this, see application settings for a given
application at
{nav icon=home, name=Home >
name=Applications >
icon=cog, name=Settings}
= Security =
The email reply channel is "somewhat" authenticated. Each reply-to address is
unique to the recipient and includes a hash of user information and a unique
object ID, so it can only be used to update that object and only be used to act
on behalf of the recipient.
However, if an address is leaked (which is fairly easy -- for instance,
forwarding an email will leak a live reply address, or a user might take a
screenshot), //anyone// who can send mail to your reply-to domain may interact
with the object the email relates to as the user who leaked the mail. Because
the authentication around email has this weakness, some actions (like accepting
revisions) are not permitted over email.
This implementation is an attempt to balance utility and security, but makes
some sacrifices on both sides to achieve it because of the difficulty of
authenticating senders in the general case (e.g., where you are an open source
project and need to interact with users whose email accounts you have no control
over).
If you leak a bunch of reply-to addresses by accident, you can change
`phabricator.mail-key` in your configuration to invalidate all the old hashes.
You can also set `metamta.public-replies`, which will change how Phabricator
delivers email. Instead of sending each recipient a unique mail with a personal
reply-to address, it will send a single email to everyone with a public reply-to
address. This decreases security because anyone who can spoof a "From" address
can act as another user, but increases convenience if you use mailing lists and,
practically, is a reasonable setting for many installs. The reply-to address
will still contain a hash unique to the object it represents, so users who have
not received an email about an object can not blindly interact with it.
If you enable application email addresses, those addresses also use the weaker
"From" authentication mechanism.
NOTE: Phabricator does not currently attempt to verify "From" addresses because
this is technically complex, seems unreasonably difficult in the general case,
and no installs have had a need for it yet. If you have a specific case where a
reasonable mechanism exists to provide sender verification (e.g., DKIM
signatures are sufficient to authenticate the sender under your configuration,
or you are willing to require all users to sign their email), file a feature
request.
= Testing and Debugging Inbound Email =
You can use the `bin/mail` utility to test and review inbound mail. This can
help you determine if mail is being delivered to Phabricator or not:
phabricator/ $ ./bin/mail list-inbound # List inbound messages.
phabricator/ $ ./bin/mail show-inbound # Show details about a message.
You can also test receiving mail, but note that this just simulates receiving
the mail and doesn't send any information over the network. It is
primarily aimed at developing email handlers: it will still work properly
if your inbound email configuration is incorrect or even disabled.
phabricator/ $ ./bin/mail receive-test # Receive test message.
Run `bin/mail help <command>` for detailed help on using these commands.
= Mailgun Setup =
To use Mailgun, you need a Mailgun account. You can sign up at
<http://www.mailgun.com>. Provided you have such an account, configure it
like this:
- Configure a mail domain according to Mailgun's instructions.
- Add a Mailgun route with a `catch_all()` rule which takes the action
`forward("https://phabricator.example.com/mail/mailgun/")`. Replace the
example domain with your actual domain.
= SendGrid Setup =
To use SendGrid, you need a SendGrid account with access to the "Parse API" for
inbound email. Provided you have such an account, configure it like this:
- Configure an MX record according to SendGrid's instructions, i.e. add
- ##phabricator.example.com MX 10 mx.sendgrid.net.## or similar.
+ `phabricator.example.com MX 10 mx.sendgrid.net.` or similar.
- Go to the "Parse Incoming Emails" page on SendGrid
(<http://sendgrid.com/developer/reply>) and add the domain as the
"Hostname".
- - Add the URL ##https://phabricator.example.com/mail/sendgrid/## as the "Url",
+ - Add the URL `https://phabricator.example.com/mail/sendgrid/` as the "Url",
using your domain (and HTTP instead of HTTPS if you are not configured with
SSL).
- If you get an error that the hostname "can't be located or verified", it
means your MX record is either incorrectly configured or hasn't propagated
yet.
- - Set ##metamta.maniphest.reply-handler-domain## and/or
- ##metamta.differential.reply-handler-domain## to
- "##phabricator.example.com##" (whatever you configured the MX record for),
- depending on whether you want to support email replies for Maniphest,
- Differential, or both.
+ - Set `metamta.reply-handler-domain` to `phabricator.example.com`"
+ (whatever you configured the MX record for).
That's it! If everything is working properly you should be able to send email
-to ##anything@phabricator.example.com## and it should appear in
+to `anything@phabricator.example.com` and it should appear in
`bin/mail list-inbound` within a few seconds.
= Local MTA: Installing Mailparse =
If you're going to run your own MTA, you need to install the PECL mailparse
extension. In theory, you can do that with:
$ sudo pecl install mailparse
You may run into an error like "needs mbstring". If so, try:
$ sudo yum install php-mbstring # or equivalent
$ sudo pecl install -n mailparse
If you get a linker error like this:
COUNTEREXAMPLE
PHP Warning: PHP Startup: Unable to load dynamic library
'/usr/lib64/php/modules/mailparse.so' - /usr/lib64/php/modules/mailparse.so:
undefined symbol: mbfl_name2no_encoding in Unknown on line 0
...you need to edit your php.ini file so that mbstring.so is loaded **before**
mailparse.so. This is not the default if you have individual files in
`php.d/`.
= Local MTA: Configuring Sendmail =
Before you can configure Sendmail, you need to install Mailparse. See the
section "Installing Mailparse" above.
Sendmail is very difficult to configure. First, you need to configure it for
your domain so that mail can be delivered correctly. In broad strokes, this
probably means something like this:
- add an MX record;
- make sendmail listen on external interfaces;
- open up port 25 if necessary (e.g., in your EC2 security policy);
- add your host to /etc/mail/local-host-names; and
- restart sendmail.
Now, you can actually configure sendmail to deliver to Phabricator. In
`/etc/aliases`, add an entry like this:
phabricator: "| /path/to/phabricator/scripts/mail/mail_handler.php"
If you use the `PHABRICATOR_ENV` environmental variable to select a
configuration, you can pass the value to the script as an argument:
.../path/to/mail_handler.php <ENV>
This is an advanced feature which is rarely used. Most installs should run
without an argument.
After making this change, run `sudo newaliases`. Now you likely need to symlink
this script into `/etc/smrsh/`:
sudo ln -s /path/to/phabricator/scripts/mail/mail_handler.php /etc/smrsh/
-Finally, edit ##/etc/mail/virtusertable## and add an entry like this:
+Finally, edit `/etc/mail/virtusertable` and add an entry like this:
@yourdomain.com phabricator@localhost
That will forward all mail to @yourdomain.com to the Phabricator processing
-script. Run ##sudo /etc/mail/make## or similar and then restart sendmail with
+script. Run `sudo /etc/mail/make` or similar and then restart sendmail with
`sudo /etc/init.d/sendmail restart`.
= Local MTA: Configuring Lamson =
Before you can configure Lamson, you need to install Mailparse. See the section
"Installing Mailparse" above.
In contrast to Sendmail, Lamson is relatively easy to configure. It is fairly
minimal, and is suitable for a development or testing environment. Lamson
listens for incoming SMTP mails and passes the content directly to Phabricator.
To get started, follow the provided instructions
(<http://lamsonproject.org/docs/getting_started.html>) to set up an instance.
One likely deployment issue is that binding to port 25 requires root
privileges. Lamson is capable of starting as root then dropping privileges, but
-you must supply ##-uid## and ##-gid## arguments to do so, as demonstrated by
+you must supply `-uid` and `-gid` arguments to do so, as demonstrated by
Step 8 in Lamson's deployment tutorial (located here:
<http://lamsonproject.org/docs/deploying_oneshotblog.html>).
The Lamson handler code itself is very concise; it merely needs to pass the
content of the email to Phabricator:
import logging, subprocess
from lamson.routing import route, stateless
from lamson import view
PHABRICATOR_ROOT = "/path/to/phabricator"
PHABRICATOR_ENV = "custom/myconf"
LOGGING_ENABLED = True
@route("(address)@(host)", address=".+")
@stateless
def START(message, address=None, host=None):
if LOGGING_ENABLED:
logging.debug("%s", message.original)
process = subprocess.Popen([PHABRICATOR_ROOT + "scripts/mail/mail_handler.php",PHABRICATOR_ENV],stdin=subprocess.PIPE)
process.communicate(message.original)

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 6:40 PM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
140742
Default Alt Text
(102 KB)

Event Timeline