Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2895515
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
46 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/resources/sql/autopatches/20150120.maniphestdefaultauthor.php b/resources/sql/autopatches/20150120.maniphestdefaultauthor.php
new file mode 100644
index 0000000000..5aae97f863
--- /dev/null
+++ b/resources/sql/autopatches/20150120.maniphestdefaultauthor.php
@@ -0,0 +1,22 @@
+<?php
+
+$key = 'metamta.maniphest.default-public-author';
+echo "Migrating `$key` to new application email infrastructure...\n";
+$value = PhabricatorEnv::getEnvConfigIfExists($key);
+$maniphest = new PhabricatorManiphestApplication();
+$config_key =
+ PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
+
+if ($value) {
+ $app_emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withApplicationPHIDs($maniphest->getPHID())
+ ->execute();
+
+ foreach ($app_emails as $app_email) {
+ $app_email->setConfigValue($config_key, $value);
+ $app_email->save();
+ }
+}
+
+echo "Done.\n";
diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
index 337ebd8e43..167161f07a 100644
--- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
+++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
@@ -1,359 +1,368 @@
<?php
final class PhabricatorManiphestConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Maniphest');
}
public function getDescription() {
return pht('Configure Maniphest.');
}
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.
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)
->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')
->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)
- ->setSummary(pht('Username anonymous bugs are filed under.'))
+ ->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/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
index 3f7c132cb9..fecfca9410 100644
--- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
@@ -1,84 +1,49 @@
<?php
final class ManiphestCreateMailReceiver extends PhabricatorMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorManiphestApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
$maniphest_app = new PhabricatorManiphestApplication();
$application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($this->getViewer())
->withApplicationPHIDs(array($maniphest_app->getPHID()))
->execute();
foreach ($mail->getToAddresses() as $to_address) {
foreach ($application_emails as $application_email) {
$create_address = $application_email->getAddress();
if ($this->matchAddresses($create_address, $to_address)) {
+ $this->setApplicationEmail($application_email);
return true;
}
}
}
return false;
}
- public function loadSender(PhabricatorMetaMTAReceivedMail $mail) {
- try {
- // Try to load the sender normally.
- return parent::loadSender($mail);
- } catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
-
- // If we failed to load the sender normally, use this special legacy
- // black magic.
-
- // TODO: Deprecate and remove this.
-
- $default_author_key = 'metamta.maniphest.default-public-author';
- $default_author = PhabricatorEnv::getEnvConfig($default_author_key);
-
- if (!strlen($default_author)) {
- throw $ex;
- }
-
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'username = %s',
- $default_author);
-
- if ($user) {
- return $user;
- }
-
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
- pht(
- "Phabricator is misconfigured, the configuration key ".
- "'metamta.maniphest.default-public-author' is set to user ".
- "'%s' but that user does not exist.",
- $default_author));
- }
- }
-
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
$task = ManiphestTask::initializeNewTask($sender);
$task->setOriginalEmailSource($mail->getHeader('From'));
$handler = PhabricatorEnv::newObjectFromConfig(
'metamta.maniphest.reply-handler');
$handler->setMailReceiver($task);
$handler->setActor($sender);
$handler->setExcludeMailRecipientPHIDs(
$mail->loadExcludeMailRecipientPHIDs());
$handler->processEmail($mail);
$mail->setRelatedPHID($task->getPHID());
}
}
diff --git a/src/applications/meta/controller/PhabricatorApplicationEditEmailController.php b/src/applications/meta/controller/PhabricatorApplicationEditEmailController.php
index 052da430c1..8b37289e1a 100644
--- a/src/applications/meta/controller/PhabricatorApplicationEditEmailController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationEditEmailController.php
@@ -1,307 +1,335 @@
<?php
final class PhabricatorApplicationEditEmailController
extends PhabricatorApplicationsController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$application = $request->getURIData('application');
$application = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array($application))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$application) {
return new Aphront404Response();
}
$title = $application->getName();
$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);
}
$emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withApplicationPHIDs(array($application->getPHID()))
->execute();
$highlight = $request->getInt('highlight');
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_edit = javelin_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('edit', $email->getID()),
'sigil' => 'workflow',
),
pht('Edit'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Delete'));
if ($highlight == $email->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$rows[] = array(
$email->getAddress(),
$button_edit,
$button_remove,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No application emails created yet.'));
$table->setHeaders(
array(
pht('Email'),
pht('Edit'),
pht('Delete'),
));
$table->setColumnClasses(
array(
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
true,
true,
true,
));
$form = id(new AphrontFormView())
->setUser($viewer);
$view_uri = $this->getApplicationURI('view/'.get_class($application).'/');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($application->getName(), $view_uri);
$crumbs->addTextCrumb(pht('Edit Application Emails'));
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Application Emails: %s', $application->getName()));
$icon = id(new PHUIIconView())
->setIconFont('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())
->setHeader($header)
->appendChild($table)
->appendChild(
id(new PHUIBoxView())
->appendChild($application->getAppEmailBlurb())
->addPadding(PHUI::PADDING_MEDIUM));
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
private function validateApplicationEmail($email) {
$errors = array();
$e_email = true;
if (!strlen($email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
$user_emails = id(new PhabricatorUserEmail())
->loadAllWhere('address = %s', $email);
if ($user_emails) {
$e_email = pht('Duplicate');
$errors[] = pht('A user already has this email.');
}
return array($e_email, $errors);
}
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();
$e_email = true;
$email = null;
$errors = array();
+ $default_user_key =
+ PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
if ($request->isDialogFormPost()) {
$email = trim($request->getStr('email'));
list($e_email, $errors) = $this->validateApplicationEmail($email);
$email_object->setAddress($email);
+ $default_user = $request->getArr($default_user_key);
+ $default_user = reset($default_user);
+ if ($default_user) {
+ $email_object->setConfigValue($default_user_key, $default_user);
+ }
if (!$errors) {
try {
$email_object->save();
return id(new AphrontRedirectResponse())->setURI(
$uri->alter('highlight', $email_object->getID()));
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_email = pht('Duplicate');
$errors[] = pht(
'Another application is already configured to use this email '.
'address.');
}
}
}
if ($errors) {
$errors = id(new AphrontErrorView())
->setErrors($errors);
}
+ $default_user = $email_object->getConfigValue($default_user_key);
+ if ($default_user) {
+ $default_user_handle = $this->loadViewerHandles(array($default_user));
+ } else {
+ $default_user_handle = array();
+ }
+
$form = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($email_object->getAddress())
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
- ->setError($e_email));
-
+ ->setError($e_email))
+ ->appendChild(
+ id(new AphrontFormTokenizerControl())
+ ->setDatasource(new PhabricatorPeopleDatasource())
+ ->setLabel(pht('Default Author'))
+ ->setName($default_user_key)
+ ->setLimit(1)
+ ->setValue($default_user_handle)
+ ->setCaption(pht(
+ 'Used if the "From:" address does not map to a known account.')));
+ if ($is_new) {
+ $title = pht('New Address');
+ } else {
+ $title = pht('Edit Address');
+ }
$dialog = id(new AphrontDialogView())
->setUser($viewer)
- ->setTitle(pht('New Address'))
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->setTitle($title)
->appendChild($errors)
->appendChild($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 = $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();
}
if ($request->isDialogFormPost()) {
$email_object->delete();
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);
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
index ebaf76817c..2ee5fdf886 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
@@ -1,143 +1,163 @@
<?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' => 'Act as the specified user.',
),
array(
'name' => 'from',
'param' => 'email',
'help' => 'Simulate mail delivery "From:" the given user.',
),
array(
'name' => 'to',
'param' => 'object',
'help' => 'Simulate mail delivery "To:" the given object.',
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
+ $to = $args->getArg('to');
+ if (!$to) {
+ throw new PhutilArgumentUsageException(
+ "Use '--to' to specify the receiving object or email address.");
+ }
+
+ $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));
}
- $to = $args->getArg('to');
- if (!$to) {
- throw new PhutilArgumentUsageException(
- "Use '--to' to specify the receiving object or email address.");
- }
$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.
$pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
$pseudomail = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders(
array(
'to' => $to.'+1+'.$pseudohash,
));
$receivers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorMailReceiver')
->loadObjects();
$receiver = null;
foreach ($receivers as $possible_receiver) {
if (!$possible_receiver->isEnabled()) {
continue;
}
if (!$possible_receiver->canAcceptMail($pseudomail)) {
continue;
}
$receiver = $possible_receiver;
break;
}
if (!$receiver) {
throw new Exception(
pht("No configured mail receiver can accept mail to '%s'.", $to));
}
if (!($receiver instanceof PhabricatorObjectMailReceiver)) {
$class = get_class($receiver);
throw new Exception(
"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));
}
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$object->getMailKey(),
$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/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
index 2a17034fd7..ee309ffe0a 100644
--- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
@@ -1,216 +1,250 @@
<?php
abstract class PhabricatorMailReceiver {
+ private $applicationEmail;
+
+ public function setApplicationEmail(
+ PhabricatorMetaMTAApplicationEmail $email) {
+ $this->applicationEmail = $email;
+ return $this;
+ }
+
+ public function getApplicationEmail() {
+ return $this->applicationEmail;
+ }
+
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);
}
public function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
public function validateSender(
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);
$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 missed on "From", try "Reply-To" if we're configured for it.
$raw_reply_to = $mail->getHeader('Reply-To');
if (strlen($raw_reply_to)) {
$reply_to_key = 'metamta.insecure-auth-with-reply-to';
$allow_reply_to = PhabricatorEnv::getEnvConfig($reply_to_key);
if ($allow_reply_to) {
$reply_to = self::getRawAddress($raw_reply_to);
$user = PhabricatorUser::loadOneWithEmailAddress($reply_to);
if ($user) {
return $user;
} else {
$reasons[] = pht(
'Phabricator is configured to authenticate users using the '.
'"Reply-To" header, but the reply address ("%s") on this '.
'message does not correspond to any known user account.',
$raw_reply_to);
}
} else {
$reasons[] = pht(
'(Phabricator is not configured to authenticate users using the '.
'"Reply-To" header, so it was ignored.)');
}
}
// 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 {
$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);
}
/**
* Determine if two inbound email addresses are effectively identical. This
* method strips and normalizes addresses so that equivalent variations are
* correctly detected as identical. For example, these addresses are all
* considered to match one another:
*
* "Abraham Lincoln" <alincoln@example.com>
* alincoln@example.com
* <ALincoln@example.com>
* "Abraham" <phabricator+ALINCOLN@EXAMPLE.COM> # With configured prefix.
*
* @param string Email address.
* @param string Another email address.
* @return bool True if addresses match.
*/
public static function matchAddresses($u, $v) {
$u = self::getRawAddress($u);
$v = self::getRawAddress($v);
$u = self::stripMailboxPrefix($u);
$v = self::stripMailboxPrefix($v);
return ($u === $v);
}
/**
* Strip a global mailbox prefix from an address if it is present. Phabricator
* can be configured to prepend a prefix to all reply addresses, which can
* make forwarding rules easier to write. A prefix looks like:
*
* example@phabricator.example.com # No Prefix
* phabricator+example@phabricator.example.com # Prefix "phabricator"
*
* @param string Email address, possibly with a mailbox prefix.
* @return string Email address with any prefix stripped.
*/
public static function stripMailboxPrefix($address) {
$address = id(new PhutilEmailAddress($address))->getAddress();
$prefix_key = 'metamta.single-reply-handler-prefix';
$prefix = PhabricatorEnv::getEnvConfig($prefix_key);
$len = strlen($prefix);
if ($len) {
$prefix = $prefix.'+';
$len = $len + 1;
}
if ($len) {
if (!strncasecmp($address, $prefix, $len)) {
$address = substr($address, strlen($prefix));
}
}
return $address;
}
/**
* Reduce an email address to its canonical form. For example, an adddress
* 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));
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
index c001b4b70c..0cec7f1950 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
@@ -1,80 +1,91 @@
<?php
final class PhabricatorMetaMTAApplicationEmail
extends PhabricatorMetaMTADAO
implements PhabricatorPolicyInterface {
protected $applicationPHID;
protected $address;
protected $configData;
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())
->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);
+ }
+
/* -( 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);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 21:34 (6 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128997
Default Alt Text
(46 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment