From 360ebce00fe88fee0aa132092467116eacaf35af Mon Sep 17 00:00:00 2001 From: Marc Lindenberg Date: Mon, 14 Jan 2019 09:41:31 -0800 Subject: [PATCH 01/35] call qsprintf() without warnings Summary: After T13217 import_repository_symbols.php was showing a lot of warnings, using %LQ fixes that. I'm aware, that there are changes planned to the whole managing the symbols complex but until then less warnings are nice. Test Plan: No more warnings when updating symbols Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin, epriestley Differential Revision: https://secure.phabricator.com/D19962 --- scripts/symbols/import_repository_symbols.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/symbols/import_repository_symbols.php b/scripts/symbols/import_repository_symbols.php index b84aea3485..24a0624d64 100755 --- a/scripts/symbols/import_repository_symbols.php +++ b/scripts/symbols/import_repository_symbols.php @@ -110,9 +110,9 @@ function commit_symbols( $conn_w, 'INSERT INTO %T (repositoryPHID, symbolContext, symbolName, symbolType, - symbolLanguage, lineNumber, pathID) VALUES %Q', + symbolLanguage, lineNumber, pathID) VALUES %LQ', $symbol->getTableName(), - implode(', ', $chunk)); + $chunk); } } From 0b8f24dfd3f753d2f76247319f33f8186acd9b1a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 09:05:40 -0800 Subject: [PATCH 02/35] Fix bad "SMTP" and "cluster.mailers" default value Summary: See note in D19964. Test Plan: O_o Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19966 --- .../config/option/PhabricatorMetaMTAConfigOptions.php | 2 +- src/docs/user/configuration/configuring_outbound_email.diviner | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index c5d9cf027b..fb81401681 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -188,7 +188,7 @@ EODOC pht('Configuring Outbound Email'))); return array( - $this->newOption('cluster.mailers', 'cluster.mailers', null) + $this->newOption('cluster.mailers', 'cluster.mailers', array()) ->setHidden(true) ->setDescription($mailers_description), $this->newOption( diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index 10805b77d9..d593300b28 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -235,7 +235,7 @@ To use this mailer, set `type` to `sendmail`. There are no `options` to configure. -Mailer: STMP +Mailer: SMTP ============ You can use this adapter to send mail via an external SMTP server, like Gmail. From 3b94b3e812e7baf05690743dd3ef4121a8345d83 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 09:26:37 -0800 Subject: [PATCH 03/35] Correct a zero-based month tooltip on burnup charts Summary: See PHI1017. This is a trivial fix even though these burnups are headed toward a grisly fate. Test Plan: Moused over some January datapoints, saw "1" instead of "0". Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19967 --- resources/celerity/map.php | 16 ++++++++-------- .../controller/ManiphestReportController.php | 5 +++-- .../application/maniphest/behavior-line-chart.js | 5 ++++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 316357bb7a..8cada227c6 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -395,7 +395,7 @@ return array( 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'cffd39b4', - 'rsrc/js/application/maniphest/behavior-line-chart.js' => '3e9da12d', + 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'c8147a20', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '8400307c', 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9', @@ -614,7 +614,7 @@ return array( 'javelin-behavior-icon-composer' => '38a6cedb', 'javelin-behavior-launch-icon-composer' => 'a17b84f1', 'javelin-behavior-lightbox-attachments' => 'c7e748bf', - 'javelin-behavior-line-chart' => '3e9da12d', + 'javelin-behavior-line-chart' => 'c8147a20', 'javelin-behavior-linked-container' => '74446546', 'javelin-behavior-maniphest-batch-selector' => 'cffd39b4', 'javelin-behavior-maniphest-list-editor' => 'c687e867', @@ -1200,12 +1200,6 @@ return array( 'javelin-vector', 'javelin-dom', ), - '3e9da12d' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-vector', - 'phui-chart-css', - ), '3eed1f2b' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1950,6 +1944,12 @@ return array( 'phuix-icon-view', 'phabricator-busy', ), + 'c8147a20' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-vector', + 'phui-chart-css', + ), 'c9749dcd' => array( 'javelin-install', 'javelin-util', diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 4f546d04c5..77bd6c0d59 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -74,8 +74,6 @@ final class ManiphestReportController extends ManiphestController { $table = new ManiphestTransaction(); $conn = $table->establishConnection('r'); - $joins = ''; - $create_joins = ''; if ($project_phid) { $joins = qsprintf( $conn, @@ -91,6 +89,9 @@ final class ManiphestReportController extends ManiphestController { PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $project_phid); + } else { + $joins = qsprintf($conn, ''); + $create_joins = qsprintf($conn, ''); } $data = queryfx_all( diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js index 2f63657c56..b2290620ff 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js +++ b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js @@ -107,7 +107,10 @@ JX.behavior('line-chart', function(config) { .attr('cy', function(d) { return y(d.count); }) .on('mouseover', function(d) { var d_y = d.date.getFullYear(); - var d_m = d.date.getMonth(); + + // NOTE: Javascript months are zero-based. See PHI1017. + var d_m = d.date.getMonth() + 1; + var d_d = d.date.getDate(); div From b98d46ce7d28c27ac8653e80a03a23d866eeb322 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Fri, 11 Jan 2019 14:21:11 -0800 Subject: [PATCH 04/35] Resurrect setup check for cluster.mailers Summary: D19940 removed this file entirely, which has led to at least one user who was unsure how to proceed now that `cluster.mailers` is required for outbound mail: https://discourse.phabricator-community.org/t/invalid-argument-supplied-for-foreach-phabricatormetamtamail-php/2287 This isn't //always// a setup issue for installs that don't care about sending mail, but this at least this gives a sporting chance to users who don't follow the changelogs. Also, I'm not sure if there's a way to use `pht()` to generate links; right now the phurl is just in plain text. Test Plan: Removed `cluster.mailers` config; observed expected setup issue. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19964 --- src/__phutil_library_map__.php | 2 ++ .../check/PhabricatorMailSetupCheck.php | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/applications/config/check/PhabricatorMailSetupCheck.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 07ecac9a17..d2a7dcb3d8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3411,6 +3411,7 @@ phutil_register_library_map(array( 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', + 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 'PhabricatorMailUtil' => 'applications/metamta/util/PhabricatorMailUtil.php', @@ -9223,6 +9224,7 @@ phutil_register_library_map(array( 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', + 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', 'PhabricatorMailUtil' => 'Phobject', diff --git a/src/applications/config/check/PhabricatorMailSetupCheck.php b/src/applications/config/check/PhabricatorMailSetupCheck.php new file mode 100644 index 0000000000..c89e0036a2 --- /dev/null +++ b/src/applications/config/check/PhabricatorMailSetupCheck.php @@ -0,0 +1,24 @@ +newIssue('cluster.mailers') + ->setName(pht('Mailers Not Configured')) + ->setMessage($message) + ->addPhabricatorConfig('cluster.mailers'); + } +} From a62f334d95030f73bd9c6a926a4fefd6d41fc2a7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 28 Dec 2018 16:52:00 -0800 Subject: [PATCH 05/35] Add a skeleton for configurable MFA provider types Summary: Ref T13222. Ref T13231. See PHI912. I'm planning to turn MFA providers into concrete objects, so you can disable and configure them. Currently, we only support TOTP, which doesn't require any configuration, but other provider types (like Duo or Yubikey OTP) do require some configuration (server URIs, API keys, etc). TOTP //could// also have some configuration, like "bits of entropy" or "allowed window size" or whatever, if we want. Add concrete objects for this and standard transaction / policy / query support. These objects don't do anything interesting yet and don't actually interact with MFA, this is just skeleton code for now. Test Plan: {F6090444} {F6090445} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13231, T13222 Differential Revision: https://secure.phabricator.com/D19935 --- .../autopatches/20181228.auth.01.provider.sql | 9 ++ .../autopatches/20181228.auth.02.xaction.sql | 19 +++ .../sql/autopatches/20181228.auth.03.name.sql | 2 + src/__phutil_library_map__.php | 35 ++++- .../PhabricatorAuthApplication.php | 9 ++ .../config/PhabricatorAuthListController.php | 25 ++-- ...habricatorAuthProviderConfigController.php | 30 +--- .../PhabricatorAuthProviderController.php | 43 ++++++ ...habricatorAuthFactorProviderController.php | 11 ++ ...icatorAuthFactorProviderEditController.php | 65 +++++++++ ...icatorAuthFactorProviderListController.php | 72 ++++++++++ ...icatorAuthFactorProviderViewController.php | 100 +++++++++++++ ...habricatorAuthFactorProviderEditEngine.php | 115 +++++++++++++++ .../PhabricatorAuthFactorProviderEditor.php | 22 +++ .../auth/factor/PhabricatorAuthFactor.php | 6 + .../auth/factor/PhabricatorTOTPAuthFactor.php | 6 + ...bricatorAuthAuthFactorProviderPHIDType.php | 40 ++++++ .../PhabricatorAuthFactorProviderQuery.php | 67 +++++++++ ...atorAuthFactorProviderTransactionQuery.php | 10 ++ .../storage/PhabricatorAuthFactorProvider.php | 134 ++++++++++++++++++ ...abricatorAuthFactorProviderTransaction.php | 18 +++ ...catorAuthFactorProviderNameTransaction.php | 69 +++++++++ ...catorAuthFactorProviderTransactionType.php | 4 + 23 files changed, 871 insertions(+), 40 deletions(-) create mode 100644 resources/sql/autopatches/20181228.auth.01.provider.sql create mode 100644 resources/sql/autopatches/20181228.auth.02.xaction.sql create mode 100644 resources/sql/autopatches/20181228.auth.03.name.sql create mode 100644 src/applications/auth/controller/config/PhabricatorAuthProviderController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php create mode 100644 src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php create mode 100644 src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php create mode 100644 src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php create mode 100644 src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php create mode 100644 src/applications/auth/storage/PhabricatorAuthFactorProvider.php create mode 100644 src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php diff --git a/resources/sql/autopatches/20181228.auth.01.provider.sql b/resources/sql/autopatches/20181228.auth.01.provider.sql new file mode 100644 index 0000000000..4ffd23c846 --- /dev/null +++ b/resources/sql/autopatches/20181228.auth.01.provider.sql @@ -0,0 +1,9 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_factorprovider ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + providerFactorKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20181228.auth.02.xaction.sql b/resources/sql/autopatches/20181228.auth.02.xaction.sql new file mode 100644 index 0000000000..c595cdd8fc --- /dev/null +++ b/resources/sql/autopatches/20181228.auth.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_factorprovidertransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20181228.auth.03.name.sql b/resources/sql/autopatches/20181228.auth.03.name.sql new file mode 100644 index 0000000000..856c10287d --- /dev/null +++ b/resources/sql/autopatches/20181228.auth.03.name.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_factorprovider + ADD name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d2a7dcb3d8..e30f3258d3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2190,6 +2190,7 @@ phutil_register_library_map(array( 'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php', 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', + 'PhabricatorAuthAuthFactorProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 'PhabricatorAuthCSRFEngine' => 'applications/auth/engine/PhabricatorAuthCSRFEngine.php', 'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php', @@ -2207,6 +2208,18 @@ phutil_register_library_map(array( 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', + 'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php', + 'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php', + 'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php', + 'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php', + 'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php', + 'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php', + 'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php', + 'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php', + 'PhabricatorAuthFactorProviderTransaction' => 'applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php', + 'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php', + 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php', + 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php', 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', @@ -2277,6 +2290,7 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php', 'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php', 'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php', + 'PhabricatorAuthProviderController' => 'applications/auth/controller/config/PhabricatorAuthProviderController.php', 'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php', @@ -7835,6 +7849,7 @@ phutil_register_library_map(array( 'PhabricatorAuthAccountView' => 'AphrontView', 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthAuthFactorProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthCSRFEngine' => 'Phobject', 'PhabricatorAuthChallenge' => array( @@ -7855,6 +7870,23 @@ phutil_register_library_map(array( 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', + 'PhabricatorAuthFactorProvider' => array( + 'PhabricatorAuthDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + ), + 'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController', + 'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType', + 'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorResult' => 'Phobject', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', @@ -7931,11 +7963,12 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), - 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController', + 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthProviderController', 'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthProviderController' => 'PhabricatorAuthController', 'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index ff4ed1f136..62f86a00f8 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -85,6 +85,15 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'view/(?P\d+)/' => 'PhabricatorAuthSSHKeyViewController', ), 'password/' => 'PhabricatorAuthSetPasswordController', + + 'mfa/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorAuthFactorProviderListController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorAuthFactorProviderEditController', + '(?P[1-9]\d*)/' => + 'PhabricatorAuthFactorProviderViewController', + ), ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index c5d4f7ad77..bb118d798e 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -91,7 +91,7 @@ final class PhabricatorAuthListController pht('Add Authentication Provider')))); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Auth Providers')); + $crumbs->addTextCrumb(pht('Login and Registration')); $crumbs->setBorder(true); $guidance_context = new PhabricatorAuthProvidersGuidanceContext(); @@ -102,12 +102,12 @@ final class PhabricatorAuthListController ->newInfoView(); $button = id(new PHUIButtonView()) - ->setTag('a') - ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) - ->setHref($this->getApplicationURI('config/new/')) - ->setIcon('fa-plus') - ->setDisabled(!$can_manage) - ->setText(pht('Add Provider')); + ->setTag('a') + ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) + ->setHref($this->getApplicationURI('config/new/')) + ->setIcon('fa-plus') + ->setDisabled(!$can_manage) + ->setText(pht('Add Provider')); $list->setFlush(true); $list = id(new PHUIObjectBoxView()) @@ -115,7 +115,7 @@ final class PhabricatorAuthListController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($list); - $title = pht('Auth Providers'); + $title = pht('Login and Registration Providers'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-key') @@ -128,10 +128,15 @@ final class PhabricatorAuthListController $list, )); - return $this->newPage() - ->setTitle($title) + $nav = $this->newNavigation() ->setCrumbs($crumbs) ->appendChild($view); + + $nav->selectFilter('login'); + + return $this->newPage() + ->setTitle($title) + ->appendChild($nav); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php index db9ec06799..c2d20dd1b3 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php @@ -1,32 +1,4 @@ setBaseURI(new PhutilURI($this->getApplicationURI())); - - if ($for_app) { - $nav->addLabel(pht('Create')); - $nav->addFilter('', - pht('Add Authentication Provider'), - $this->getApplicationURI('/config/new/')); - } - return $nav; - } - - public function buildApplicationMenu() { - return $this->buildSideNavView($for_app = true)->getMenu(); - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $can_create = $this->hasApplicationCapability( - AuthManageProvidersCapability::CAPABILITY); - - return $crumbs; - } - -} + extends PhabricatorAuthProviderController {} diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php new file mode 100644 index 0000000000..2fb4386ef4 --- /dev/null +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php @@ -0,0 +1,43 @@ +getViewer(); + + $nav = id(new AphrontSideNavFilterView()) + ->setBaseURI(new PhutilURI($this->getApplicationURI())) + ->setViewer($viewer); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setName(pht('Authentication')) + ->setType(PHUIListItemView::TYPE_LABEL)); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setKey('login') + ->setName(pht('Login and Registration')) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($this->getApplicationURI('/')) + ->setIcon('fa-key')); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setKey('mfa') + ->setName(pht('Multi-Factor')) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($this->getApplicationURI('mfa/')) + ->setIcon('fa-mobile')); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationMenu() { + return $this->newNavigation()->getMenu(); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php new file mode 100644 index 0000000000..53a8f10be3 --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php @@ -0,0 +1,11 @@ +addTextCrumb(pht('Multi-Factor'), $this->getApplicationURI('mfa/')); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php new file mode 100644 index 0000000000..5108eaaefd --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php @@ -0,0 +1,65 @@ +requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $engine = id(new PhabricatorAuthFactorProviderEditEngine()) + ->setController($this); + + $id = $request->getURIData('id'); + if (!$id) { + $factor_key = $request->getStr('providerFactorKey'); + + $map = PhabricatorAuthFactor::getAllFactors(); + $factor = idx($map, $factor_key); + if (!$factor) { + return $this->buildFactorSelectionResponse(); + } + + $engine + ->addContextParameter('providerFactorKey', $factor_key) + ->setProviderFactor($factor); + } + + return $engine->buildResponse(); + } + + private function buildFactorSelectionResponse() { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $cancel_uri = $this->getApplicationURI('mfa/'); + + $factors = PhabricatorAuthFactor::getAllFactors(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($factors as $factor_key => $factor) { + $factor_uri = id(new PhutilURI('/mfa/edit/')) + ->setQueryParam('providerFactorKey', $factor_key); + $factor_uri = $this->getApplicationURI($factor_uri); + + $item = id(new PHUIObjectItemView()) + ->setHeader($factor->getFactorName()) + ->setHref($factor_uri) + ->setClickable(true) + ->setImageIcon($factor->newIconView()) + ->addAttribute($factor->getFactorCreateHelp()); + + $menu->addItem($item); + } + + return $this->newDialog() + ->setTitle(pht('Choose Provider Type')) + ->appendChild($menu) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php new file mode 100644 index 0000000000..293728cf36 --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php @@ -0,0 +1,72 @@ +getViewer(); + + $can_manage = $this->hasApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->execute(); + + $list = new PHUIObjectItemListView(); + foreach ($providers as $provider) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($provider->getObjectName()) + ->setHeader($provider->getDisplayName()) + ->setHref($provider->getURI()); + + $list->addItem($item); + } + + $list->setNoDataString( + pht('You have not configured any multi-factor providers yet.')); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Multi-Factor')) + ->setBorder(true); + + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) + ->setHref($this->getApplicationURI('mfa/edit/')) + ->setIcon('fa-plus') + ->setDisabled(!$can_manage) + ->setWorkflow(true) + ->setText(pht('Add MFA Provider')); + + $list->setFlush(true); + $list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('MFA Providers')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + + $title = pht('MFA Providers'); + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-mobile') + ->addActionLink($button); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $list, + )); + + $nav = $this->newNavigation() + ->setCrumbs($crumbs) + ->appendChild($view); + + $nav->selectFilter('mfa'); + + return $this->newPage() + ->setTitle($title) + ->appendChild($nav); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php new file mode 100644 index 0000000000..67edf2f81b --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php @@ -0,0 +1,100 @@ +getViewer(); + + $this->requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $provider = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$provider) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($provider->getObjectName()) + ->setBorder(true); + + $header = $this->buildHeaderView($provider); + $properties = $this->buildPropertiesView($provider); + $curtain = $this->buildCurtain($provider); + + + $timeline = $this->buildTransactionTimeline( + $provider, + new PhabricatorAuthFactorProviderTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $timeline, + )) + ->addPropertySection(pht('Details'), $properties); + + return $this->newPage() + ->setTitle($provider->getDisplayName()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs( + array( + $provider->getPHID(), + )) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorAuthFactorProvider $provider) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($provider->getDisplayName()) + ->setPolicyObject($provider); + + return $view; + } + + private function buildPropertiesView( + PhabricatorAuthFactorProvider $provider) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $view->addProperty( + pht('Factor Type'), + $provider->getFactor()->getFactorName()); + + return $view; + } + + private function buildCurtain(PhabricatorAuthFactorProvider $provider) { + $viewer = $this->getViewer(); + $id = $provider->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $provider, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($provider); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit MFA Provider')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("mfa/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php new file mode 100644 index 0000000000..a0b5988589 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php @@ -0,0 +1,115 @@ +providerFactor = $factor; + return $this; + } + + public function getProviderFactor() { + return $this->providerFactor; + } + + protected function newEditableObject() { + $factor = $this->getProviderFactor(); + if ($factor) { + $provider = PhabricatorAuthFactorProvider::initializeNewProvider($factor); + } else { + $provider = new PhabricatorAuthFactorProvider(); + } + + return $provider; + } + + protected function newObjectQuery() { + return new PhabricatorAuthFactorProviderQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create MFA Provider'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create MFA Provider'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit MFA Provider'); + } + + protected function getObjectEditShortText($object) { + return $object->getObjectName(); + } + + protected function getObjectCreateShortText() { + return pht('Create MFA Provider'); + } + + protected function getObjectName() { + return pht('MFA Provider'); + } + + protected function getEditorURI() { + return '/auth/mfa/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/auth/mfa/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AuthManageProvidersCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + $factor_name = $object->getFactor()->getFactorName(); + + return array( + id(new PhabricatorStaticEditField()) + ->setKey('displayType') + ->setLabel(pht('Factor Type')) + ->setDescription(pht('Type of the MFA provider.')) + ->setValue($factor_name), + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setTransactionType( + PhabricatorAuthFactorProviderNameTransaction::TRANSACTIONTYPE) + ->setLabel(pht('Name')) + ->setDescription(pht('Display name for the MFA provider.')) + ->setValue($object->getName()) + ->setPlaceholder($factor_name), + ); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php new file mode 100644 index 0000000000..144f275391 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php @@ -0,0 +1,22 @@ +setIcon('fa-mobile'); + } + protected function newChallenge( PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer) { diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 33bb961691..3632ca5c45 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -12,6 +12,12 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return pht('Mobile Phone App (TOTP)'); } + public function getFactorCreateHelp() { + return pht( + 'Allow users to attach a mobile authenticator application (like '. + 'Google Authenticator) to their account.'); + } + public function getFactorDescription() { return pht( 'Attach a mobile authenticator application (like Authy '. diff --git a/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php b/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php new file mode 100644 index 0000000000..f0f9f572e8 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php @@ -0,0 +1,40 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $provider = $objects[$phid]; + + $handle->setURI($provider->getURI()); + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php new file mode 100644 index 0000000000..f4ce60773b --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php @@ -0,0 +1,67 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + public function newResultObject() { + return new PhabricatorAuthFactorProvider(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + return $where; + } + + protected function willFilterPage(array $providers) { + $map = PhabricatorAuthFactor::getAllFactors(); + foreach ($providers as $key => $provider) { + $factor_key = $provider->getProviderFactorKey(); + $factor = idx($map, $factor_key); + + if (!$factor) { + unset($providers[$key]); + continue; + } + + $provider->attachFactor($factor); + } + + return $providers; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php new file mode 100644 index 0000000000..5add1345c4 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php @@ -0,0 +1,10 @@ +setProviderFactorKey($factor->getFactorKey()) + ->attachFactor($factor) + ->setStatus(self::STATUS_ACTIVE); + } + + protected function getConfiguration() { + return array( + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'providerFactorKey' => 'text64', + 'name' => 'text255', + 'status' => 'text32', + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorAuthAuthFactorProviderPHIDType::TYPECONST; + } + + public function getURI() { + return '/auth/mfa/'.$this->getID().'/'; + } + + public function getObjectName() { + return pht('MFA Provider %d', $this->getID()); + } + + public function getAuthFactorProviderProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setAuthFactorProviderProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function attachFactor(PhabricatorAuthFactor $factor) { + $this->factor = $factor; + return $this; + } + + public function getFactor() { + return $this->assertAttached($this->factor); + } + + public function getDisplayName() { + $name = $this->getName(); + if (strlen($name)) { + return $name; + } + + return $this->getFactor()->getFactorName(); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorAuthFactorProviderEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorAuthFactorProviderTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + $extended = array(); + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + break; + case PhabricatorPolicyCapability::CAN_EDIT: + $extended[] = array( + new PhabricatorAuthApplication(), + AuthManageProvidersCapability::CAPABILITY, + ); + break; + } + + return $extended; + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php b/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php new file mode 100644 index 0000000000..0b7b7fc6a7 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php @@ -0,0 +1,18 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($old)) { + return pht( + '%s named this provider %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!strlen($new)) { + return pht( + '%s removed the name (%s) of this provider.', + $this->renderAuthor(), + $this->renderOldValue()); + } else { + return pht( + '%s renamed this provider from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Provider names can not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + } + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'name'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php new file mode 100644 index 0000000000..fe17eee545 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php @@ -0,0 +1,4 @@ + Date: Thu, 3 Jan 2019 05:08:19 -0800 Subject: [PATCH 06/35] Allow multiple mail receivers to react to an individual email Summary: Fixes T7477. Fixes T13066. Currently, inbound mail is processed by the first receiver that matches any "To:" address. "Cc" addresses are ignored. **To, CC, and Multiple Receivers** Some users would like to be able to "Cc" addresses like `bugs@` instead of having to "To" the address, which makes perfect sense. That's the driving use case behind T7477. Since users can To/Cc multiple "create object" or "update object" addresses, I also wanted to make the behavior more general. For example, if you email `bugs@` and also `paste@`, your mail might reasonably make both a Task and a Paste. Is this useful? I'm not sure. But it seems like it's pretty clearly the best match for user intent, and the least-surprising behavior we can have. There's also no good rule for picking which address "wins" when two or more match -- we ended up with "address order", which is pretty arbitrary since "To" and "Cc" are not really ordered fields. One part of this change is removing `phabricator.allow-email-users`. In practice, this option only controlled whether users were allowed to send mail to "Application Email" addresses with a configured default author, and it's unlikely that we'll expand it since I think the future of external/grey users is Nuance, not richer interaction with Maniphest/Differential/etc. Since this option only made "Default Author" work and "Default Author" is optional, we can simplify behavior by making the rule work like this: - If an address specifies a default author, it allows public email. - If an address does not, it doesn't. That's basically how it worked already, except that you could intentionally "break" the behavior by not configuring `phabricator.allow-email-users`. This is a backwards compatility change with possible security implications (it might allow email in that was previously blocked by configuration) that I'll call out in the changelog, but I suspect that no installs are really impacted and this new behavior is generally more intuitive. A somewhat related change here is that each receiver is allowed to react to each individual email address, instead of firing once. This allows you to configure `bugs-a@` and `bugs-b@` and CC them both and get two tasks. Useful? Maybe not, but seems like the best execution of intent. **Sender vs Author** Adjacently, T13066 described an improvement to error handling behavior here: we did not distinguish between "sender" (the user matching the email "From" address) and "actor" (the user we're actually acting as in the application). These are different when you're some internet rando and send to `bugs@`, which has a default author. Then the "sender" is `null` and the "author" is `@bugs-robot` or whatever (some user account you've configured). This refines "Sender" vs "Author". This is mostly a purity/correctness change, but it means that we won't send random email error messages to `@bugs-robot`. Since receivers are now allowed to process mail with no "sender" if they have some default "actor" they would rather use instead, it's not an error to send from an invalid address unless nothing processes the mail. **Other** This removes the "abundant receivers" error since this is no longer an error. This always sets "external user" mail recipients to be unverified. As far as I can tell, there's no pathway by which we send them email anyway (before or after this change), although it's possible I'm missing something somewhere. Test Plan: I did most of this with `bin/mail receive-test`. I rigged the workflow slightly for some of it since it doesn't support multiple addresses or explicit "CC" and adding either would be a bit tricky. These could also be tested with `scripts/mail/mail_handler.php`, but I don't currently have the MIME parser extension installed locally after a recent upgrade to Mojave and suspect T13232 makes it tricky to install. - Ran unit tests, which provide significant coverage of this flow. - Sent mail to multiple Maniphest application emails, got multiple tasks. - Sent mail to a Maniphest and a Paste application email, got a task and a paste. - Sent mail to a task. - Saw original email recorded on tasks. This is a behavior particular to tasks. - Sent mail to a paste. - Sent mail to a mock. - Sent mail to a Phame blog post. - Sent mail to a Legalpad document. - Sent mail to a Conpherence thread. - Sent mail to a poll. - This isn't every type of supported object but it's enough of them that I'm pretty confident I didn't break the whole flow. - Sent mail to an object I could not view (got an error). - As a non-user, sent mail to several "create an object..." addresses. - Addresses with a default user worked (e.g., created a task). - Addresses without a default user did not work. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13066, T7477 Differential Revision: https://secure.phabricator.com/D19952 --- .../mail/PhabricatorAuditMailReceiver.php | 2 +- .../PhabricatorCalendarEventMailReceiver.php | 2 +- .../PhabricatorExtraConfigSetupCheck.php | 4 + .../option/PhabricatorCoreConfigOptions.php | 8 - .../mail/ConpherenceThreadMailReceiver.php | 2 +- .../mail/PhabricatorCountdownMailReceiver.php | 2 +- .../mail/DifferentialCreateMailReceiver.php | 14 +- .../mail/DifferentialRevisionMailReceiver.php | 2 +- .../files/mail/FileCreateMailReceiver.php | 8 +- .../files/mail/FileMailReceiver.php | 2 +- .../legalpad/mail/LegalpadMailReceiver.php | 2 +- .../mail/ManiphestCreateMailReceiver.php | 13 +- .../mail/ManiphestTaskMailReceiver.php | 2 +- ...habricatorMetaMTAApplicationEmailPanel.php | 19 +- .../constants/MetaMTAReceivedMailStatus.php | 2 - ...catorMailManagementReceiveTestWorkflow.php | 18 +- .../query/PhabricatorMetaMTAActorQuery.php | 12 +- .../PhabricatorApplicationMailReceiver.php | 97 +++++++- .../receiver/PhabricatorMailReceiver.php | 169 ++------------ .../PhabricatorObjectMailReceiver.php | 112 ++++----- .../PhabricatorMetaMTAApplicationEmail.php | 3 + .../PhabricatorMetaMTAReceivedMail.php | 214 +++++++++++++----- ...PhabricatorMetaMTAReceivedMailTestCase.php | 8 +- .../paste/mail/PasteCreateMailReceiver.php | 13 +- .../paste/mail/PasteMailReceiver.php | 2 +- .../phame/mail/PhamePostMailReceiver.php | 2 +- .../pholio/mail/PholioMockMailReceiver.php | 2 +- .../ponder/mail/PonderAnswerMailReceiver.php | 2 +- .../mail/PonderQuestionCreateMailReceiver.php | 8 +- .../mail/PonderQuestionMailReceiver.php | 2 +- .../mail/PhabricatorSlowvoteMailReceiver.php | 2 +- 31 files changed, 408 insertions(+), 342 deletions(-) diff --git a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php index 9dc70e9d8a..9b55151747 100644 --- a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php +++ b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php @@ -12,7 +12,7 @@ final class PhabricatorAuditMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)preg_replace('/^COMMIT/', '', $pattern); + $id = (int)preg_replace('/^COMMIT/i', '', $pattern); return id(new DiffusionCommitQuery()) ->setViewer($viewer) diff --git a/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php b/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php index 8536907486..01a0367234 100644 --- a/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php +++ b/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php @@ -13,7 +13,7 @@ final class PhabricatorCalendarEventMailReceiver } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'E'); + $id = (int)substr($pattern, 1); return id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index e597c52897..12b9463166 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -394,6 +394,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'metamta.insecure-auth-with-reply-to' => pht( 'Authenticating users based on "Reply-To" is no longer supported.'), + + 'phabricator.allow-email-users' => pht( + 'Public email is now accepted if the associated address has a '. + 'default author, and rejected otherwise.'), ); return $ancient_config; diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index 08266217ea..48f6f24491 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -234,14 +234,6 @@ EOREMARKUP $this->newOption('phabricator.cache-namespace', 'string', 'phabricator') ->setLocked(true) ->setDescription(pht('Cache namespace.')), - $this->newOption('phabricator.allow-email-users', 'bool', false) - ->setBoolOptions( - array( - pht('Allow'), - pht('Disallow'), - )) - ->setDescription( - pht('Allow non-members to interact with tasks over email.')), $this->newOption('phabricator.silent', 'bool', false) ->setLocked(true) ->setBoolOptions( diff --git a/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php b/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php index e926d3cfc5..aeb4e28997 100644 --- a/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php +++ b/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php @@ -13,7 +13,7 @@ final class ConpherenceThreadMailReceiver } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'Z'); + $id = (int)substr($pattern, 1); return id(new ConpherenceThreadQuery()) ->setViewer($viewer) diff --git a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php index d0218de59b..9b448ef10f 100644 --- a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php +++ b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php @@ -13,7 +13,7 @@ final class PhabricatorCountdownMailReceiver } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)substr($pattern, 4); + $id = (int)substr($pattern, 1); return id(new PhabricatorCountdownQuery()) ->setViewer($viewer) diff --git a/src/applications/differential/mail/DifferentialCreateMailReceiver.php b/src/applications/differential/mail/DifferentialCreateMailReceiver.php index cfd7470099..0d4fd1a2be 100644 --- a/src/applications/differential/mail/DifferentialCreateMailReceiver.php +++ b/src/applications/differential/mail/DifferentialCreateMailReceiver.php @@ -9,14 +9,16 @@ final class DifferentialCreateMailReceiver protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { + PhutilEmailAddress $target) { + + $author = $this->getAuthor(); $attachments = $mail->getAttachments(); $files = array(); $errors = array(); if ($attachments) { $files = id(new PhabricatorFileQuery()) - ->setViewer($sender) + ->setViewer($author) ->withPHIDs($attachments) ->execute(); foreach ($files as $index => $file) { @@ -37,7 +39,7 @@ final class DifferentialCreateMailReceiver array( 'diff' => $file->loadFileData(), )); - $call->setUser($sender); + $call->setUser($author); try { $result = $call->execute(); $diffs[$file->getName()] = $result['uri']; @@ -56,7 +58,7 @@ final class DifferentialCreateMailReceiver array( 'diff' => $body, )); - $call->setUser($sender); + $call->setUser($author); try { $result = $call->execute(); $diffs[pht('Mail Body')] = $result['uri']; @@ -108,10 +110,10 @@ final class DifferentialCreateMailReceiver } id(new PhabricatorMetaMTAMail()) - ->addTos(array($sender->getPHID())) + ->addTos(array($author->getPHID())) ->setSubject($subject) ->setSubjectPrefix($subject_prefix) - ->setFrom($sender->getPHID()) + ->setFrom($author->getPHID()) ->setBody($body->render()) ->saveAndSend(); } diff --git a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php index 929ee72647..0fe8583a1d 100644 --- a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php +++ b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php @@ -13,7 +13,7 @@ final class DifferentialRevisionMailReceiver } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'D'); + $id = (int)substr($pattern, 1); return id(new DifferentialRevisionQuery()) ->setViewer($viewer) diff --git a/src/applications/files/mail/FileCreateMailReceiver.php b/src/applications/files/mail/FileCreateMailReceiver.php index 2cf946aea8..fe314f1f0a 100644 --- a/src/applications/files/mail/FileCreateMailReceiver.php +++ b/src/applications/files/mail/FileCreateMailReceiver.php @@ -9,7 +9,8 @@ final class FileCreateMailReceiver protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { + PhutilEmailAddress $target) { + $author = $this->getAuthor(); $attachment_phids = $mail->getAttachments(); if (empty($attachment_phids)) { @@ -21,6 +22,11 @@ final class FileCreateMailReceiver $first_phid = head($attachment_phids); $mail->setRelatedPHID($first_phid); + $sender = $this->getSender(); + if (!$sender) { + return; + } + $attachment_count = count($attachment_phids); if ($attachment_count > 1) { $subject = pht('You successfully uploaded %d files.', $attachment_count); diff --git a/src/applications/files/mail/FileMailReceiver.php b/src/applications/files/mail/FileMailReceiver.php index cdad22c5ce..f4074d9ba0 100644 --- a/src/applications/files/mail/FileMailReceiver.php +++ b/src/applications/files/mail/FileMailReceiver.php @@ -12,7 +12,7 @@ final class FileMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'F'); + $id = (int)substr($pattern, 1); return id(new PhabricatorFileQuery()) ->setViewer($viewer) diff --git a/src/applications/legalpad/mail/LegalpadMailReceiver.php b/src/applications/legalpad/mail/LegalpadMailReceiver.php index 5fe0dfd4e8..679812e4ad 100644 --- a/src/applications/legalpad/mail/LegalpadMailReceiver.php +++ b/src/applications/legalpad/mail/LegalpadMailReceiver.php @@ -12,7 +12,7 @@ final class LegalpadMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'L'); + $id = (int)substr($pattern, 1); return id(new LegalpadDocumentQuery()) ->setViewer($viewer) diff --git a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php index 64bdaa2ed5..22c09fdf60 100644 --- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php +++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php @@ -9,15 +9,20 @@ final class ManiphestCreateMailReceiver protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { + PhutilEmailAddress $target) { - $task = ManiphestTask::initializeNewTask($sender); - $task->setOriginalEmailSource($mail->getHeader('From')); + $author = $this->getAuthor(); + $task = ManiphestTask::initializeNewTask($author); + + $from_address = $mail->newFromAddress(); + if ($from_address) { + $task->setOriginalEmailSource((string)$from_address); + } $handler = new ManiphestReplyHandler(); $handler->setMailReceiver($task); - $handler->setActor($sender); + $handler->setActor($author); $handler->setExcludeMailRecipientPHIDs( $mail->loadAllRecipientPHIDs()); if ($this->getApplicationEmail()) { diff --git a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php index 220a888e8e..54ac72fd57 100644 --- a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php +++ b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php @@ -12,7 +12,7 @@ final class ManiphestTaskMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'T'); + $id = (int)substr($pattern, 1); return id(new ManiphestTaskQuery()) ->setViewer($viewer) diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php index afde9fee23..c13835e6f4 100644 --- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php +++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php @@ -261,8 +261,12 @@ final class PhabricatorMetaMTAApplicationEmailPanel ->setName($config_default) ->setLimit(1) ->setValue($v_default) - ->setCaption(pht( - 'Used if the "From:" address does not map to a known account.'))); + ->setCaption( + pht( + 'Used if the "From:" address does not map to a user account. '. + 'Setting a default author will allow anyone on the public '. + 'internet to create objects in Phabricator by sending email to '. + 'this address.'))); if ($is_new) { $title = pht('New Address'); @@ -369,9 +373,17 @@ final class PhabricatorMetaMTAApplicationEmailPanel $email_space = null; } + $default_author_phid = $email->getDefaultAuthorPHID(); + if (!$default_author_phid) { + $default_author = phutil_tag('em', array(), pht('None')); + } else { + $default_author = $viewer->renderHandle($default_author_phid); + } + $rows[] = array( $email_space, $email->getAddress(), + $default_author, $button_edit, $button_remove, ); @@ -383,11 +395,13 @@ final class PhabricatorMetaMTAApplicationEmailPanel array( pht('Space'), pht('Email'), + pht('Default'), pht('Edit'), pht('Delete'), )); $table->setColumnClasses( array( + '', '', 'wide', 'action', @@ -398,6 +412,7 @@ final class PhabricatorMetaMTAApplicationEmailPanel array( PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer), true, + true, $is_edit, $is_edit, )); diff --git a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php index 965bdbdbfe..2ec44b847a 100644 --- a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php +++ b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php @@ -6,7 +6,6 @@ final class MetaMTAReceivedMailStatus const STATUS_DUPLICATE = 'err:duplicate'; const STATUS_FROM_PHABRICATOR = 'err:self'; const STATUS_NO_RECEIVERS = 'err:no-receivers'; - const STATUS_ABUNDANT_RECEIVERS = 'err:multiple-receivers'; const STATUS_UNKNOWN_SENDER = 'err:unknown-sender'; const STATUS_DISABLED_SENDER = 'err:disabled-sender'; const STATUS_NO_PUBLIC_MAIL = 'err:no-public-mail'; @@ -23,7 +22,6 @@ final class MetaMTAReceivedMailStatus self::STATUS_DUPLICATE => pht('Duplicate Message'), self::STATUS_FROM_PHABRICATOR => pht('Phabricator Mail'), self::STATUS_NO_RECEIVERS => pht('No Receivers'), - self::STATUS_ABUNDANT_RECEIVERS => pht('Multiple Receivers'), self::STATUS_UNKNOWN_SENDER => pht('Unknown Sender'), self::STATUS_DISABLED_SENDER => pht('Disabled Sender'), self::STATUS_NO_PUBLIC_MAIL => pht('No Public Mail'), diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php index 46c444571b..5c17b11321 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php @@ -33,6 +33,7 @@ final class PhabricatorMailManagementReceiveTestWorkflow } public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); $console = PhutilConsole::getConsole(); $to = $args->getArg('to'); @@ -95,14 +96,23 @@ final class PhabricatorMailManagementReceiveTestWorkflow if (preg_match('/.+@.+/', $to)) { $header_content['to'] = $to; } else { + // We allow the user to use an object name instead of a real address // as a convenience. To build the mail, we build a similar message and // look for a receiver which will accept it. + + // In the general case, mail may be processed by multiple receivers, + // but mail to objects only ever has one receiver today. + $pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y'); + + $raw_target = $to.'+1+'.$pseudohash; + $target = new PhutilEmailAddress($raw_target.'@local.cli'); + $pseudomail = id(new PhabricatorMetaMTAReceivedMail()) ->setHeaders( array( - 'to' => $to.'+1+'.$pseudohash, + 'to' => $raw_target, )); $receivers = id(new PhutilClassMapQuery()) @@ -112,7 +122,11 @@ final class PhabricatorMailManagementReceiveTestWorkflow $receiver = null; foreach ($receivers as $possible_receiver) { - if (!$possible_receiver->canAcceptMail($pseudomail)) { + $possible_receiver = id(clone $possible_receiver) + ->setViewer($viewer) + ->setSender($user); + + if (!$possible_receiver->canAcceptMail($pseudomail, $target)) { continue; } $receiver = $possible_receiver; diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php index 18b8063ee1..269b9824a5 100644 --- a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php @@ -121,12 +121,12 @@ final class PhabricatorMetaMTAActorQuery extends PhabricatorQuery { $actor->setEmailAddress($xuser->getAccountID()); - // NOTE: This effectively drops all outbound mail to unrecognized - // addresses unless "phabricator.allow-email-users" is set. See T12237 - // for context. - $allow_key = 'phabricator.allow-email-users'; - $allow_value = PhabricatorEnv::getEnvConfig($allow_key); - $actor->setIsVerified((bool)$allow_value); + // Circa T7477, it appears that we never intentionally send email to + // external users (even when they email "bugs@" to create a task). + // Mark these users as unverified so mail to them is always dropped. + // See also T12237. In the future, we might change this behavior. + + $actor->setIsVerified(false); } } diff --git a/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php b/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php index 11646351f2..546f622e10 100644 --- a/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php @@ -3,32 +3,105 @@ abstract class PhabricatorApplicationMailReceiver extends PhabricatorMailReceiver { + private $applicationEmail; + private $emailList; + private $author; + abstract protected function newApplication(); + final protected function setApplicationEmail( + PhabricatorMetaMTAApplicationEmail $email) { + $this->applicationEmail = $email; + return $this; + } + + final protected function getApplicationEmail() { + return $this->applicationEmail; + } + + final protected function setAuthor(PhabricatorUser $author) { + $this->author = $author; + return $this; + } + + final protected function getAuthor() { + return $this->author; + } + final public function isEnabled() { return $this->newApplication()->isInstalled(); } - final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { - $application = $this->newApplication(); + final public function canAcceptMail( + PhabricatorMetaMTAReceivedMail $mail, + PhutilEmailAddress $target) { + $viewer = $this->getViewer(); + $sender = $this->getSender(); - $application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery()) - ->setViewer($viewer) - ->withApplicationPHIDs(array($application->getPHID())) - ->execute(); + foreach ($this->loadApplicationEmailList() as $application_email) { + $create_address = $application_email->newAddress(); - foreach ($mail->newTargetAddresses() as $address) { - foreach ($application_emails as $application_email) { - $create_address = $application_email->newAddress(); - if (PhabricatorMailUtil::matchAddresses($create_address, $address)) { - $this->setApplicationEmail($application_email); - return true; + if (!PhabricatorMailUtil::matchAddresses($create_address, $target)) { + continue; + } + + if ($sender) { + $author = $sender; + } else { + $author_phid = $application_email->getDefaultAuthorPHID(); + + // If this mail isn't from a recognized sender and the target address + // does not have a default author, we can't accept it, and it's an + // error because you tried to send it here. + + // You either need to be sending from a real address or be sending to + // an address which accepts mail from the public internet. + + if (!$author_phid) { + throw new PhabricatorMetaMTAReceivedMailProcessingException( + MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER, + pht( + 'You are sending from an unrecognized email address to '. + 'an address which does not support public email ("%s").', + (string)$target)); + } + + $author = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($author_phid)) + ->executeOne(); + if (!$author) { + throw new Exception( + pht( + 'Application email ("%s") has an invalid default author ("%s").', + (string)$create_address, + $author_phid)); } } + + $this + ->setApplicationEmail($application_email) + ->setAuthor($author); + + return true; } return false; } + private function loadApplicationEmailList() { + if ($this->emailList === null) { + $viewer = $this->getViewer(); + $application = $this->newApplication(); + + $this->emailList = id(new PhabricatorMetaMTAApplicationEmailQuery()) + ->setViewer($viewer) + ->withApplicationPHIDs(array($application->getPHID())) + ->execute(); + } + + return $this->emailList; + } + } diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php index 9f2e979815..738f8d9e26 100644 --- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php @@ -2,167 +2,40 @@ abstract class PhabricatorMailReceiver extends Phobject { - private $applicationEmail; + private $viewer; + private $sender; - public function setApplicationEmail( - PhabricatorMetaMTAApplicationEmail $email) { - $this->applicationEmail = $email; + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; return $this; } - public function getApplicationEmail() { - return $this->applicationEmail; + final public function getViewer() { + return $this->viewer; + } + + final public function setSender(PhabricatorUser $sender) { + $this->sender = $sender; + return $this; + } + + final public function getSender() { + return $this->sender; } abstract public function isEnabled(); - abstract public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail); + abstract public function canAcceptMail( + PhabricatorMetaMTAReceivedMail $mail, + PhutilEmailAddress $target); abstract protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender); + PhutilEmailAddress $target); 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 don't know who this user is, load or create an external user - // account for them if we're configured for it. - $email_key = 'phabricator.allow-email-users'; - $allow_email_users = PhabricatorEnv::getEnvConfig($email_key); - if ($allow_email_users) { - $from_obj = new PhutilEmailAddress($from); - $xuser = id(new PhabricatorExternalAccountQuery()) - ->setViewer($this->getViewer()) - ->withAccountTypes(array('email')) - ->withAccountDomains(array($from_obj->getDomainName(), 'self')) - ->withAccountIDs(array($from_obj->getAddress())) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->loadOneOrCreate(); - return $xuser->getPhabricatorUser(); - } else { - // NOTE: Currently, we'll always drop this mail (since it's headed to - // an unverified recipient). See T12237. These details are still useful - // because they'll appear in the mail logs and Mail web UI. - - $reasons[] = pht( - 'Phabricator is also not configured to allow unknown external users '. - 'to send mail to the system using just an email address.'); - $reasons[] = pht( - 'To interact with Phabricator, add this address ("%s") to your '. - 'account.', - $raw_from); - } - - if ($this->getApplicationEmail()) { - $application_email = $this->getApplicationEmail(); - $default_user_phid = $application_email->getConfigValue( - PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR); - - if ($default_user_phid) { - $user = id(new PhabricatorUser())->loadOneWhere( - 'phid = %s', - $default_user_phid); - if ($user) { - return $user; - } - - $reasons[] = pht( - 'Phabricator is misconfigured: the application email '. - '"%s" is set to user "%s", but that user does not exist.', - $application_email->getAddress(), - $default_user_phid); - } - } - - $reasons = implode("\n\n", $reasons); - - throw new PhabricatorMetaMTAReceivedMailProcessingException( - MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER, - $reasons); - } - - /** - * Reduce an email address to its canonical form. For example, an address - * like: - * - * "Abraham Lincoln" < ALincoln@example.com > - * - * ...will be reduced to: - * - * alincoln@example.com - * - * @param string Email address in noncanonical form. - * @return string Canonical email address. - */ - public static function getRawAddress($address) { - $address = id(new PhutilEmailAddress($address))->getAddress(); - return trim(phutil_utf8_strtolower($address)); + PhutilEmailAddress $target) { + $this->processReceivedMail($mail, $target); } } diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php index 9f92d33f21..16950c1577 100644 --- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php @@ -29,52 +29,23 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { final protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { + PhutilEmailAddress $target) { - $object = $this->loadObjectFromMail($mail, $sender); - $mail->setRelatedPHID($object->getPHID()); - - $this->processReceivedObjectMail($mail, $object, $sender); - - return $this; - } - - protected function processReceivedObjectMail( - PhabricatorMetaMTAReceivedMail $mail, - PhabricatorLiskDAO $object, - PhabricatorUser $sender) { - - $handler = $this->getTransactionReplyHandler(); - if ($handler) { - return $handler - ->setMailReceiver($object) - ->setActor($sender) - ->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs()) - ->processEmail($mail); + $parts = $this->matchObjectAddress($target); + if (!$parts) { + // We should only make it here if we matched already in "canAcceptMail()", + // so this is a surprise. + throw new Exception( + pht( + 'Failed to parse object address ("%s") during processing.', + (string)$target)); } - throw new PhutilMethodNotImplementedException(); - } - - protected function getTransactionReplyHandler() { - return null; - } - - public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) { - return $this->loadObject($pattern, $viewer); - } - - public function validateSender( - PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { - - parent::validateSender($mail, $sender); - - $parts = $this->matchObjectAddressInMail($mail); $pattern = $parts['pattern']; + $sender = $this->getSender(); try { - $object = $this->loadObjectFromMail($mail, $sender); + $object = $this->loadObject($pattern, $sender); } catch (PhabricatorPolicyException $policy_exception) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM, @@ -95,7 +66,6 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { } $sender_identifier = $parts['sender']; - if ($sender_identifier === 'public') { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { throw new PhabricatorMetaMTAReceivedMailProcessingException( @@ -136,30 +106,52 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { 'is correct.', $pattern)); } + + $mail->setRelatedPHID($object->getPHID()); + $this->processReceivedObjectMail($mail, $object, $sender); + + return $this; } + protected function processReceivedObjectMail( + PhabricatorMetaMTAReceivedMail $mail, + PhabricatorLiskDAO $object, + PhabricatorUser $sender) { - final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { - if ($this->matchObjectAddressInMail($mail)) { - return true; + $handler = $this->getTransactionReplyHandler(); + if ($handler) { + return $handler + ->setMailReceiver($object) + ->setActor($sender) + ->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs()) + ->processEmail($mail); } - return false; + throw new PhutilMethodNotImplementedException(); } - private function matchObjectAddressInMail( - PhabricatorMetaMTAReceivedMail $mail) { - - foreach ($mail->newTargetAddresses() as $address) { - $parts = $this->matchObjectAddress($address); - if ($parts) { - return $parts; - } - } - + protected function getTransactionReplyHandler() { return null; } + public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) { + return $this->loadObject($pattern, $viewer); + } + + final public function canAcceptMail( + PhabricatorMetaMTAReceivedMail $mail, + PhutilEmailAddress $target) { + + // If we don't have a valid sender user account, we can never accept + // mail to any object. + $sender = $this->getSender(); + if (!$sender) { + return false; + } + + return (bool)$this->matchObjectAddress($target); + } + private function matchObjectAddress(PhutilEmailAddress $address) { $address = PhabricatorMailUtil::normalizeAddress($address); $local = $address->getLocalPart(); @@ -188,16 +180,6 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { return $regexp; } - private function loadObjectFromMail( - PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { - $parts = $this->matchObjectAddressInMail($mail); - - return $this->loadObject( - phutil_utf8_strtoupper($parts['pattern']), - $sender); - } - public static function computeMailHash($mail_key, $phid) { $hash = PhabricatorHash::digestWithNamedKey( $mail_key.$phid, diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php index f9698c3928..f5673bed9d 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php @@ -67,6 +67,9 @@ final class PhabricatorMetaMTAApplicationEmail return idx($this->configData, $key, $default); } + public function getDefaultAuthorPHID() { + return $this->getConfigValue(self::CONFIG_DEFAULT_AUTHOR); + } public function getInUseMessage() { $applications = PhabricatorApplication::getAllApplications(); diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 95f6048c01..0605c99b89 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -125,6 +125,7 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { } public function processReceivedMail() { + $viewer = $this->getViewer(); $sender = null; try { @@ -132,26 +133,94 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { $this->dropMailAlreadyReceived(); $this->dropEmptyMail(); - $receiver = $this->loadReceiver(); - $sender = $receiver->loadSender($this); - $receiver->validateSender($this, $sender); + $sender = $this->loadSender(); + if ($sender) { + $this->setAuthorPHID($sender->getPHID()); - $this->setAuthorPHID($sender->getPHID()); + // If we've identified the sender, mark them as the author of any + // attached files. We do this before we validate them (below), since + // they still authored these files even if their account is not allowed + // to interact via email. - // Now that we've identified the sender, mark them as the author of - // any attached files. - $attachments = $this->getAttachments(); - if ($attachments) { - $files = id(new PhabricatorFileQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs($attachments) - ->execute(); - foreach ($files as $file) { - $file->setAuthorPHID($sender->getPHID())->save(); + $attachments = $this->getAttachments(); + if ($attachments) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($attachments) + ->execute(); + foreach ($files as $file) { + $file->setAuthorPHID($sender->getPHID())->save(); + } + } + + $this->validateSender($sender); + } + + $receivers = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorMailReceiver') + ->setFilterMethod('isEnabled') + ->execute(); + + $any_accepted = false; + $receiver_exception = null; + + $targets = $this->newTargetAddresses(); + foreach ($receivers as $receiver) { + $receiver = id(clone $receiver) + ->setViewer($viewer); + + if ($sender) { + $receiver->setSender($sender); + } + + foreach ($targets as $target) { + try { + if (!$receiver->canAcceptMail($this, $target)) { + continue; + } + + $any_accepted = true; + + $receiver->receiveMail($this, $target); + } catch (Exception $ex) { + // If receivers raise exceptions, we'll keep the first one in hope + // that it points at a root cause. + if (!$receiver_exception) { + $receiver_exception = $ex; + } + } } } - $receiver->receiveMail($this, $sender); + if ($receiver_exception) { + throw $receiver_exception; + } + + if (!$any_accepted) { + if (!$sender) { + // NOTE: Currently, we'll always drop this mail (since it's headed to + // an unverified recipient). See T12237. These details are still + // useful because they'll appear in the mail logs and Mail web UI. + + throw new PhabricatorMetaMTAReceivedMailProcessingException( + MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER, + pht( + 'This email was sent from an email address ("%s") that is not '. + 'associated with a Phabricator account. To interact with '. + 'Phabricator via email, add this address to your account.', + (string)$this->newFromAddress())); + } else { + throw new PhabricatorMetaMTAReceivedMailProcessingException( + MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS, + pht( + 'Phabricator can not process this mail because no application '. + 'knows how to handle it. Check that the address you sent it to '. + 'is correct.'. + "\n\n". + '(No concrete, enabled subclass of PhabricatorMailReceiver can '. + 'accept this mail.)')); + } + } } catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) { switch ($ex->getStatusCode()) { case MetaMTAReceivedMailStatus::STATUS_DUPLICATE: @@ -311,51 +380,6 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { 'text, and signatures are discarded and ignored.')); } - /** - * Load a concrete instance of the @{class:PhabricatorMailReceiver} which - * accepts this mail, if one exists. - */ - private function loadReceiver() { - $receivers = id(new PhutilClassMapQuery()) - ->setAncestorClass('PhabricatorMailReceiver') - ->setFilterMethod('isEnabled') - ->execute(); - - $accept = array(); - foreach ($receivers as $key => $receiver) { - if ($receiver->canAcceptMail($this)) { - $accept[$key] = $receiver; - } - } - - if (!$accept) { - throw new PhabricatorMetaMTAReceivedMailProcessingException( - MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS, - pht( - 'Phabricator can not process this mail because no application '. - 'knows how to handle it. Check that the address you sent it to is '. - 'correct.'. - "\n\n". - '(No concrete, enabled subclass of PhabricatorMailReceiver can '. - 'accept this mail.)')); - } - - if (count($accept) > 1) { - $names = implode(', ', array_keys($accept)); - throw new PhabricatorMetaMTAReceivedMailProcessingException( - MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS, - pht( - 'Phabricator is not able to process this mail because more than '. - 'one application is willing to accept it, creating ambiguity. '. - 'Mail needs to be accepted by exactly one receiving application.'. - "\n\n". - 'Accepting receivers: %s.', - $names)); - } - - return head($accept); - } - private function sendExceptionMail( Exception $ex, PhabricatorUser $viewer = null) { @@ -434,4 +458,74 @@ EOBODY )); } + public function newFromAddress() { + $raw_from = $this->getHeader('From'); + + if (strlen($raw_from)) { + return new PhutilEmailAddress($raw_from); + } + + return null; + } + + private function getViewer() { + return PhabricatorUser::getOmnipotentUser(); + } + + /** + * Identify the sender's user account for a piece of received mail. + * + * Note that this method does not validate that the sender is who they say + * they are, just that they've presented some credential which corresponds + * to a recognizable user. + */ + private function loadSender() { + $viewer = $this->getViewer(); + + // Try to identify the user based on their "From" address. + $from_address = $this->newFromAddress(); + if ($from_address) { + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withEmails(array($from_address->getAddress())) + ->executeOne(); + if ($user) { + return $user; + } + } + + return null; + } + + private function validateSender(PhabricatorUser $sender) { + $failure_reason = null; + if ($sender->getIsDisabled()) { + $failure_reason = pht( + 'Your account ("%s") is disabled, so you can not interact with '. + 'Phabricator over email.', + $sender->getUsername()); + } else if ($sender->getIsStandardUser()) { + if (!$sender->getIsApproved()) { + $failure_reason = pht( + 'Your account ("%s") has not been approved yet. You can not '. + 'interact with Phabricator over email until your account is '. + 'approved.', + $sender->getUsername()); + } else if (PhabricatorUserEmail::isEmailVerificationRequired() && + !$sender->getIsEmailVerified()) { + $failure_reason = pht( + 'You have not verified the email address for your account ("%s"). '. + 'You must verify your email address before you can interact '. + 'with Phabricator over email.', + $sender->getUsername()); + } + } + + if ($failure_reason) { + throw new PhabricatorMetaMTAReceivedMailProcessingException( + MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER, + $failure_reason); + } + } + } diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php index 31a31b5ff2..2630a50feb 100644 --- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php +++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php @@ -48,11 +48,15 @@ final class PhabricatorMetaMTAReceivedMailTestCase extends PhabricatorTestCase { } public function testDropUnreceivableMail() { + $user = $this->generateNewTestUser() + ->save(); + $mail = new PhabricatorMetaMTAReceivedMail(); $mail->setHeaders( array( 'Message-ID' => 'test@example.com', 'To' => 'does+not+exist@example.com', + 'From' => $user->loadPrimaryEmail()->getAddress(), )); $mail->setBodies( array( @@ -70,10 +74,6 @@ final class PhabricatorMetaMTAReceivedMailTestCase extends PhabricatorTestCase { public function testDropUnknownSenderMail() { $this->setManiphestCreateEmail(); - $env = PhabricatorEnv::beginScopedEnv(); - $env->overrideEnvConfig('phabricator.allow-email-users', false); - $env->overrideEnvConfig('metamta.maniphest.default-public-author', null); - $mail = new PhabricatorMetaMTAReceivedMail(); $mail->setHeaders( array( diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php index 992d1e60b5..844c5f1acc 100644 --- a/src/applications/paste/mail/PasteCreateMailReceiver.php +++ b/src/applications/paste/mail/PasteCreateMailReceiver.php @@ -9,7 +9,8 @@ final class PasteCreateMailReceiver protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { + PhutilEmailAddress $target) { + $author = $this->getAuthor(); $title = $mail->getSubject(); if (!$title) { @@ -26,18 +27,23 @@ final class PasteCreateMailReceiver ->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title); - $paste = PhabricatorPaste::initializeNewPaste($sender); + $paste = PhabricatorPaste::initializeNewPaste($author); $content_source = $mail->newContentSource(); $editor = id(new PhabricatorPasteEditor()) - ->setActor($sender) + ->setActor($author) ->setContentSource($content_source) ->setContinueOnNoEffect(true); $xactions = $editor->applyTransactions($paste, $xactions); $mail->setRelatedPHID($paste->getPHID()); + $sender = $this->getSender(); + if (!$sender) { + return; + } + $subject_prefix = PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); $subject = pht('You successfully created a paste.'); @@ -56,5 +62,4 @@ final class PasteCreateMailReceiver ->saveAndSend(); } - } diff --git a/src/applications/paste/mail/PasteMailReceiver.php b/src/applications/paste/mail/PasteMailReceiver.php index cb5a6e8982..f0b2f4674c 100644 --- a/src/applications/paste/mail/PasteMailReceiver.php +++ b/src/applications/paste/mail/PasteMailReceiver.php @@ -12,7 +12,7 @@ final class PasteMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'P'); + $id = (int)substr($pattern, 1); return id(new PhabricatorPasteQuery()) ->setViewer($viewer) diff --git a/src/applications/phame/mail/PhamePostMailReceiver.php b/src/applications/phame/mail/PhamePostMailReceiver.php index 3655e73906..3e2f493d3a 100644 --- a/src/applications/phame/mail/PhamePostMailReceiver.php +++ b/src/applications/phame/mail/PhamePostMailReceiver.php @@ -13,7 +13,7 @@ final class PhamePostMailReceiver } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)substr($pattern, 4); + $id = (int)substr($pattern, 1); return id(new PhamePostQuery()) ->setViewer($viewer) diff --git a/src/applications/pholio/mail/PholioMockMailReceiver.php b/src/applications/pholio/mail/PholioMockMailReceiver.php index 09c0eb3051..13fdc559ec 100644 --- a/src/applications/pholio/mail/PholioMockMailReceiver.php +++ b/src/applications/pholio/mail/PholioMockMailReceiver.php @@ -12,7 +12,7 @@ final class PholioMockMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'M'); + $id = (int)substr($pattern, 1); return id(new PholioMockQuery()) ->setViewer($viewer) diff --git a/src/applications/ponder/mail/PonderAnswerMailReceiver.php b/src/applications/ponder/mail/PonderAnswerMailReceiver.php index d7269ac861..dfa252c4a1 100644 --- a/src/applications/ponder/mail/PonderAnswerMailReceiver.php +++ b/src/applications/ponder/mail/PonderAnswerMailReceiver.php @@ -12,7 +12,7 @@ final class PonderAnswerMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'ANSR'); + $id = (int)substr($pattern, 4); return id(new PonderAnswerQuery()) ->setViewer($viewer) diff --git a/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php b/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php index 1eb3269798..32669855ea 100644 --- a/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php +++ b/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php @@ -9,7 +9,8 @@ final class PonderQuestionCreateMailReceiver protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, - PhabricatorUser $sender) { + PhutilEmailAddress $target) { + $author = $this->getAuthor(); $title = $mail->getSubject(); if (!strlen($title)) { @@ -26,18 +27,17 @@ final class PonderQuestionCreateMailReceiver ->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT) ->setNewValue($mail->getCleanTextBody()); - $question = PonderQuestion::initializeNewQuestion($sender); + $question = PonderQuestion::initializeNewQuestion($author); $content_source = $mail->newContentSource(); $editor = id(new PonderQuestionEditor()) - ->setActor($sender) + ->setActor($author) ->setContentSource($content_source) ->setContinueOnNoEffect(true); $xactions = $editor->applyTransactions($question, $xactions); $mail->setRelatedPHID($question->getPHID()); - } diff --git a/src/applications/ponder/mail/PonderQuestionMailReceiver.php b/src/applications/ponder/mail/PonderQuestionMailReceiver.php index 6388837af3..e68d6c0ecb 100644 --- a/src/applications/ponder/mail/PonderQuestionMailReceiver.php +++ b/src/applications/ponder/mail/PonderQuestionMailReceiver.php @@ -12,7 +12,7 @@ final class PonderQuestionMailReceiver extends PhabricatorObjectMailReceiver { } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'Q'); + $id = (int)substr($pattern, 1); return id(new PonderQuestionQuery()) ->setViewer($viewer) diff --git a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php index 78e608231f..7b7459d4c3 100644 --- a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php +++ b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php @@ -13,7 +13,7 @@ final class PhabricatorSlowvoteMailReceiver } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)substr($pattern, 4); + $id = (int)substr($pattern, 1); return id(new PhabricatorSlowvoteQuery()) ->setViewer($viewer) From a37b28ef79cbb5dab37a40f14e69b4bbd0e4eb60 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 3 Jan 2019 15:45:58 -0800 Subject: [PATCH 07/35] Prevent inbound processing of the "void/placeholder" address and other reserved addresses Summary: Depends on D19952. Ref T13222. Never process mail targets if they match: - The "default" address which we send mail "From". - The "void" address which we use as a placholder "To" when we only have "CC" addresses. - Any address from a list of reserved/administrative names. The first two prevent loops. The third one prevents abuse. There's a reasonably well-annotated list of reservations and reasons here: https://webmasters.stackexchange.com/questions/104811/is-there-any-list-of-email-addresses-reserved-because-of-security-concerns-for-a Stuff like `support@` seems fine; stuff like `ssladmin@` might let you get SSL certs issued for a domain you don't control. Also, forbid users from creating application emails with these reserved addresses. Finally, build the default and void addresses somewhat more cleverly. Test Plan: Added unit tests, tried to configured reserved addresses, hit the default/void cases manually with `bin/mail receive-test`. Reviewers: amckinley Reviewed By: amckinley Subscribers: olexiy.myronenko Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19953 --- .../PhabricatorMetaMTAConfigOptions.php | 5 +- ...abricatorMetaMTAApplicationEmailEditor.php | 10 ++++ .../PhabricatorMailReceiverTestCase.php | 26 +++++++++++ .../storage/PhabricatorMetaMTAMail.php | 21 +++++++-- .../PhabricatorMetaMTAReceivedMail.php | 13 +++++- .../metamta/util/PhabricatorMailUtil.php | 46 +++++++++++++++++++ 6 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index fb81401681..ce24d48ead 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -191,10 +191,7 @@ EODOC $this->newOption('cluster.mailers', 'cluster.mailers', array()) ->setHidden(true) ->setDescription($mailers_description), - $this->newOption( - 'metamta.default-address', - 'string', - 'noreply@phabricator.example.com') + $this->newOption('metamta.default-address', 'string', null) ->setDescription(pht('Default "From" address.')), $this->newOption( 'metamta.one-mail-per-recipient', diff --git a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php index 2cbd164a87..8df02793f7 100644 --- a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php +++ b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php @@ -104,6 +104,16 @@ final class PhabricatorMetaMTAApplicationEmailEditor pht('Invalid'), pht('Email address is not formatted properly.')); } + + $address = new PhutilEmailAddress($email); + if (PhabricatorMailUtil::isReservedAddress($address)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Reserved'), + pht( + 'This email address is reserved. Choose a different '. + 'address.')); + } } $missing = $this->validateIsEmptyTextField( diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php index 85389c3256..fb38601717 100644 --- a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php +++ b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php @@ -41,4 +41,30 @@ final class PhabricatorMailReceiverTestCase extends PhabricatorTestCase { } } + public function testReservedAddresses() { + $default_address = id(new PhabricatorMetaMTAMail()) + ->newDefaultEmailAddress(); + + $void_address = id(new PhabricatorMetaMTAMail()) + ->newVoidEmailAddress(); + + $map = array( + 'alincoln@example.com' => false, + 'sysadmin@example.com' => true, + 'hostmaster@example.com' => true, + '"Walter Ebmaster" ' => true, + (string)$default_address => true, + (string)$void_address => true, + ); + + foreach ($map as $raw_address => $expect) { + $address = new PhutilEmailAddress($raw_address); + + $this->assertEqual( + $expect, + PhabricatorMailUtil::isReservedAddress($address), + pht('Reserved: %s', $raw_address)); + } + } + } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 15c8a0945a..1117c4273d 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -713,7 +713,7 @@ final class PhabricatorMetaMTAMail $actors = $this->loadAllActors(); $deliverable_actors = $this->filterDeliverableActors($actors); - $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); + $default_from = (string)$this->newDefaultEmailAddress(); if (empty($params['from'])) { $mailer->setFrom($default_from); } @@ -1463,18 +1463,33 @@ final class PhabricatorMetaMTAMail } private function newMailDomain() { + $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); + if (strlen($domain)) { + return $domain; + } + $install_uri = PhabricatorEnv::getURI('/'); $install_uri = new PhutilURI($install_uri); return $install_uri->getDomain(); } - public function newVoidEmailAddress() { + public function newDefaultEmailAddress() { + $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address'); + if (strlen($raw_address)) { + return new PhutilEmailAddress($raw_address); + } + $domain = $this->newMailDomain(); - $address = "void-recipient@{$domain}"; + $address = "noreply@{$domain}"; + return new PhutilEmailAddress($address); } + public function newVoidEmailAddress() { + return $this->newDefaultEmailAddress(); + } + /* -( Routing )------------------------------------------------------------ */ diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 0605c99b89..714e3d3c35 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -161,10 +161,19 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { ->setFilterMethod('isEnabled') ->execute(); + $targets = $this->newTargetAddresses(); + foreach ($targets as $key => $target) { + // Never accept any reserved address as a mail target. This prevents + // security issues around "hostmaster@" and bad behavior with + // "noreply@". + if (PhabricatorMailUtil::isReservedAddress($target)) { + unset($targets[$key]); + continue; + } + } + $any_accepted = false; $receiver_exception = null; - - $targets = $this->newTargetAddresses(); foreach ($receivers as $receiver) { $receiver = id(clone $receiver) ->setViewer($viewer); diff --git a/src/applications/metamta/util/PhabricatorMailUtil.php b/src/applications/metamta/util/PhabricatorMailUtil.php index 60eb89dea2..17539a0add 100644 --- a/src/applications/metamta/util/PhabricatorMailUtil.php +++ b/src/applications/metamta/util/PhabricatorMailUtil.php @@ -62,4 +62,50 @@ final class PhabricatorMailUtil return ($u->getAddress() === $v->getAddress()); } + public static function isReservedAddress(PhutilEmailAddress $address) { + $address = self::normalizeAddress($address); + $local = $address->getLocalPart(); + + $reserved = array( + 'admin', + 'administrator', + 'hostmaster', + 'list', + 'list-request', + 'majordomo', + 'postmaster', + 'root', + 'ssl-admin', + 'ssladmin', + 'ssladministrator', + 'sslwebmaster', + 'sysadmin', + 'uucp', + 'webmaster', + + 'noreply', + 'no-reply', + ); + + $reserved = array_fuse($reserved); + + if (isset($reserved[$local])) { + return true; + } + + $default_address = id(new PhabricatorMetaMTAMail()) + ->newDefaultEmailAddress(); + if (self::matchAddresses($address, $default_address)) { + return true; + } + + $void_address = id(new PhabricatorMetaMTAMail()) + ->newVoidEmailAddress(); + if (self::matchAddresses($address, $void_address)) { + return true; + } + + return false; + } + } From b5797ce60a35e1b5d395598dec5dc22d57288120 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 4 Jan 2019 05:45:38 -0800 Subject: [PATCH 08/35] Refactor mail to produce an intermediate "bag of strings" object in preparation for SMS Summary: Depends on D19954. Ref T920. This is a step toward a world where "Mailers" are generic and may send messages over a broader array of channels (email, SMS, postal mail). There are a few major parts here: - Instead of calling `$mailer->setSubject()`, `$mailer->setFrom()`, etc., build in intermediate `$message` object first, then pass that to the mailer. - This breaks every mailer! This change on its own does not fix them. I plan to fix them in a series of "update mailer X", "update mailer Y" followups. - This generally makes the API easier to change in the far future, and in the near future supports mailers accepting different types of `$message` objects with the same API. - Pull the "build an email" stuff out into a `PhabricatorMailEmailEngine`. `MetaMTAMail` is already a huge object without also doing this translation step. This is just a separation/simplification change, but also tries to fight against `MetaMTAMail` getting 5K lines of email/sms/whatsapp/postal-mail code. - Try to rewrite the "build an email" stuff to be a bit more straightforward while making it generate objects. Prior to this change, it had this weird flow: ```lang=php foreach ($properties as $key => $prop) { switch ($key) { case 'xyz': // ... } } ``` This is just inherently somewhat hard to puzzle out, and it means that processing order depends on internal property order, which is quite surprising. Test Plan: This breaks everything on its own; adapters must be updated to use the new API. See followups. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D19955 --- src/__phutil_library_map__.php | 4 + .../PhabricatorMetaMTAMailViewController.php | 3 - .../engine/PhabricatorMailEmailEngine.php | 649 ++++++++++++++++++ .../engine/PhabricatorMailMessageEngine.php | 55 ++ ...atorMailManagementShowOutboundWorkflow.php | 4 - .../storage/PhabricatorMetaMTAMail.php | 642 +++-------------- 6 files changed, 813 insertions(+), 544 deletions(-) create mode 100644 src/applications/metamta/engine/PhabricatorMailEmailEngine.php create mode 100644 src/applications/metamta/engine/PhabricatorMailMessageEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e30f3258d3..2b620b11d8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3389,6 +3389,7 @@ phutil_register_library_map(array( 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', 'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php', 'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php', + 'PhabricatorMailEmailEngine' => 'applications/metamta/engine/PhabricatorMailEmailEngine.php', 'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php', 'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php', 'PhabricatorMailEmailMessage' => 'applications/metamta/message/PhabricatorMailEmailMessage.php', @@ -3414,6 +3415,7 @@ phutil_register_library_map(array( 'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php', 'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php', 'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php', + 'PhabricatorMailMessageEngine' => 'applications/metamta/engine/PhabricatorMailMessageEngine.php', 'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php', 'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php', 'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php', @@ -9221,6 +9223,7 @@ phutil_register_library_map(array( 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailAttachment' => 'Phobject', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', + 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', 'PhabricatorMailEmailHeraldField' => 'HeraldField', 'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorMailEmailMessage' => 'PhabricatorMailExternalMessage', @@ -9246,6 +9249,7 @@ phutil_register_library_map(array( 'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorMailMessageEngine' => 'Phobject', 'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter', 'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction', diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php index 9b33397831..b831a9c9d6 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php @@ -187,9 +187,6 @@ final class PhabricatorMetaMTAMailViewController ->setStacked(true); $headers = $mail->getDeliveredHeaders(); - if ($headers === null) { - $headers = $mail->generateHeaders(); - } // Sort headers by name. $headers = isort($headers, 0); diff --git a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php new file mode 100644 index 0000000000..51bc1cad07 --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php @@ -0,0 +1,649 @@ +getMailer(); + $mail = $this->getMail(); + + $message = new PhabricatorMailEmailMessage(); + + $from_address = $this->newFromEmailAddress(); + $message->setFromAddress($from_address); + + $reply_address = $this->newReplyToEmailAddress(); + if ($reply_address) { + $message->setReplyToAddress($reply_address); + } + + $to_addresses = $this->newToEmailAddresses(); + $cc_addresses = $this->newCCEmailAddresses(); + + if (!$to_addresses && !$cc_addresses) { + $mail->setMessage( + pht( + 'Message has no valid recipients: all To/CC are disabled, '. + 'invalid, or configured not to receive this mail.')); + return null; + } + + // If this email describes a mail processing error, we rate limit outbound + // messages to each individual address. This prevents messes where + // something is stuck in a loop or dumps a ton of messages on us suddenly. + if ($mail->getIsErrorEmail()) { + $all_recipients = array(); + foreach ($to_addresses as $to_address) { + $all_recipients[] = $to_address->getAddress(); + } + foreach ($cc_addresses as $cc_address) { + $all_recipients[] = $cc_address->getAddress(); + } + if ($this->shouldRateLimitMail($all_recipients)) { + $mail->setMessage( + pht( + 'This is an error email, but one or more recipients have '. + 'exceeded the error email rate limit. Declining to deliver '. + 'message.')); + return null; + } + } + + // Some mailers require a valid "To:" in order to deliver mail. If we + // don't have any "To:", try to fill it in with a placeholder "To:". + // If that also fails, move the "Cc:" line to "To:". + if (!$to_addresses) { + $void_address = $this->newVoidEmailAddress(); + $cc_addresses = $to_addresses; + $to_addresses = array($void_address); + } + + $to_addresses = $this->getUniqueEmailAddresses($to_addresses); + $cc_addresses = $this->getUniqueEmailAddresses( + $cc_addresses, + $to_addresses); + + $message->setToAddresses($to_addresses); + $message->setCCAddresses($cc_addresses); + + $attachments = $this->newEmailAttachments(); + $message->setAttachments($attachments); + + $subject = $this->newEmailSubject(); + $message->setSubject($subject); + + $headers = $this->newEmailHeaders(); + foreach ($this->newEmailThreadingHeaders($mailer) as $threading_header) { + $headers[] = $threading_header; + } + + $stamps = $mail->getMailStamps(); + if ($stamps) { + $headers[] = $this->newEmailHeader( + 'X-Phabricator-Stamps', + implode(' ', $stamps)); + } + + $must_encrypt = $mail->getMustEncrypt(); + + $raw_body = $mail->getBody(); + $body = $raw_body; + if ($must_encrypt) { + $parts = array(); + + $encrypt_uri = $this->getMustEncryptURI(); + if (!strlen($encrypt_uri)) { + $encrypt_phid = $this->getRelatedPHID(); + if ($encrypt_phid) { + $encrypt_uri = urisprintf( + '/object/%s/', + $encrypt_phid); + } + } + + if (strlen($encrypt_uri)) { + $parts[] = pht( + 'This secure message is notifying you of a change to this object:'); + $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri); + } + + $parts[] = pht( + 'The content for this message can only be transmitted over a '. + 'secure channel. To view the message content, follow this '. + 'link:'); + + $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); + + $body = implode("\n\n", $parts); + } else { + $body = $raw_body; + } + + $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); + if (strlen($body) > $body_limit) { + $body = id(new PhutilUTF8StringTruncator()) + ->setMaximumBytes($body_limit) + ->truncateString($body); + $body .= "\n"; + $body .= pht('(This email was truncated at %d bytes.)', $body_limit); + } + $message->setTextBody($body); + $body_limit -= strlen($body); + + // If we sent a different message body than we were asked to, record + // what we actually sent to make debugging and diagnostics easier. + if ($body !== $raw_body) { + $mail->setDeliveredBody($body); + } + + if ($must_encrypt) { + $send_html = false; + } else { + $send_html = $this->shouldSendHTML(); + } + + if ($send_html) { + $html_body = $mail->getHTMLBody(); + if (strlen($html_body)) { + // NOTE: We just drop the entire HTML body if it won't fit. Safely + // truncating HTML is hard, and we already have the text body to fall + // back to. + if (strlen($html_body) <= $body_limit) { + $message->setHTMLBody($html_body); + $body_limit -= strlen($html_body); + } + } + } + + // Pass the headers to the mailer, then save the state so we can show + // them in the web UI. If the mail must be encrypted, we remove headers + // which are not on a strict whitelist to avoid disclosing information. + $filtered_headers = $this->filterHeaders($headers, $must_encrypt); + $message->setHeaders($filtered_headers); + + $mail->setUnfilteredHeaders($headers); + $mail->setDeliveredHeaders($headers); + + if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { + $mail->setMessage( + pht( + 'Phabricator is running in silent mode. See `%s` '. + 'in the configuration to change this setting.', + 'phabricator.silent')); + + return null; + } + + return $message; + } + +/* -( Message Components )------------------------------------------------- */ + + private function newFromEmailAddress() { + $from_address = $this->newDefaultEmailAddress(); + $mail = $this->getMail(); + + // If the mail content must be encrypted, always disguise the sender. + $must_encrypt = $mail->getMustEncrypt(); + if ($must_encrypt) { + return $from_address; + } + + // If we have a raw "From" address, use that. + $raw_from = $mail->getRawFrom(); + if ($raw_from) { + list($from_email, $from_name) = $raw_from; + return $this->newEmailAddress($from_email, $from_name); + } + + // Otherwise, use as much of the information for any sending entity as + // we can. + $from_phid = $mail->getFrom(); + + $actor = $this->getActor($from_phid); + if ($actor) { + $actor_email = $actor->getEmailAddress(); + $actor_name = $actor->getName(); + } else { + $actor_email = null; + $actor_name = null; + } + + $send_as_user = PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); + if ($send_as_user) { + if ($actor_email !== null) { + $from_address->setAddress($actor_email); + } + } + + if ($actor_name !== null) { + $from_address->setDisplayName($actor_name); + } + + return $from_address; + } + + private function newReplyToEmailAddress() { + $mail = $this->getMail(); + + $reply_raw = $mail->getReplyTo(); + if (!strlen($reply_raw)) { + return null; + } + + $reply_address = new PhutilEmailAddress($reply_raw); + + // If we have a sending object, change the display name. + $from_phid = $mail->getFrom(); + $actor = $this->getActor($from_phid); + if ($actor) { + $reply_address->setDisplayName($actor->getName()); + } + + // If we don't have a display name, fill in a default. + if (!strlen($reply_address->getDisplayName())) { + $reply_address->setDisplayName(pht('Phabricator')); + } + + return $reply_address; + } + + private function newToEmailAddresses() { + $mail = $this->getMail(); + + $phids = $mail->getToPHIDs(); + $addresses = $this->newEmailAddressesFromActorPHIDs($phids); + + foreach ($mail->getRawToAddresses() as $raw_address) { + $addresses[] = new PhutilEmailAddress($raw_address); + } + + return $addresses; + } + + private function newCCEmailAddresses() { + $mail = $this->getMail(); + $phids = $mail->getCcPHIDs(); + return $this->newEmailAddressesFromActorPHIDs($phids); + } + + private function newEmailAddressesFromActorPHIDs(array $phids) { + $mail = $this->getMail(); + $phids = $mail->expandRecipients($phids); + + $addresses = array(); + foreach ($phids as $phid) { + $actor = $this->getActor($phid); + if (!$actor) { + continue; + } + + if (!$actor->isDeliverable()) { + continue; + } + + $addresses[] = new PhutilEmailAddress($actor->getEmailAddress()); + } + + return $addresses; + } + + private function newEmailSubject() { + $mail = $this->getMail(); + + $is_threaded = (bool)$mail->getThreadID(); + $must_encrypt = $mail->getMustEncrypt(); + + $subject = array(); + + if ($is_threaded) { + if ($this->shouldAddRePrefix()) { + $subject[] = 'Re:'; + } + } + + $subject[] = trim($mail->getSubjectPrefix()); + + // If mail content must be encrypted, we replace the subject with + // a generic one. + if ($must_encrypt) { + $encrypt_subject = $mail->getMustEncryptSubject(); + if (!strlen($encrypt_subject)) { + $encrypt_subject = pht('Object Updated'); + } + $subject[] = $encrypt_subject; + } else { + $vary_prefix = $mail->getVarySubjectPrefix(); + if (strlen($vary_prefix)) { + if ($this->shouldVarySubject()) { + $subject[] = $vary_prefix; + } + } + + $subject[] = $mail->getSubject(); + } + + foreach ($subject as $key => $part) { + if (!strlen($part)) { + unset($subject[$key]); + } + } + + $subject = implode(' ', $subject); + return $subject; + } + + private function newEmailHeaders() { + $mail = $this->getMail(); + + $headers = array(); + + $headers[] = $this->newEmailHeader( + 'X-Phabricator-Sent-This-Message', + 'Yes'); + $headers[] = $this->newEmailHeader( + 'X-Mail-Transport-Agent', + 'MetaMTA'); + + // Some clients respect this to suppress OOF and other auto-responses. + $headers[] = $this->newEmailHeader( + 'X-Auto-Response-Suppress', + 'All'); + + $mailtags = $mail->getMailTags(); + if ($mailtags) { + $tag_header = array(); + foreach ($mailtags as $mailtag) { + $tag_header[] = '<'.$mailtag.'>'; + } + $tag_header = implode(', ', $tag_header); + $headers[] = $this->newEmailHeader( + 'X-Phabricator-Mail-Tags', + $tag_header); + } + + $value = $mail->getHeaders(); + foreach ($value as $pair) { + list($header_key, $header_value) = $pair; + + // NOTE: If we have \n in a header, SES rejects the email. + $header_value = str_replace("\n", ' ', $header_value); + $headers[] = $this->newEmailHeader($header_key, $header_value); + } + + $is_bulk = $mail->getIsBulk(); + if ($is_bulk) { + $headers[] = $this->newEmailHeader('Precedence', 'bulk'); + } + + if ($mail->getMustEncrypt()) { + $headers[] = $this->newEmailHeader('X-Phabricator-Must-Encrypt', 'Yes'); + } + + $related_phid = $mail->getRelatedPHID(); + if ($related_phid) { + $headers[] = $this->newEmailHeader('Thread-Topic', $related_phid); + } + + $headers[] = $this->newEmailHeader( + 'X-Phabricator-Mail-ID', + $mail->getID()); + + $unique = Filesystem::readRandomCharacters(16); + $headers[] = $this->newEmailHeader( + 'X-Phabricator-Send-Attempt', + $unique); + + return $headers; + } + + private function newEmailThreadingHeaders() { + $mailer = $this->getMailer(); + $mail = $this->getMail(); + + $headers = array(); + + $thread_id = $mail->getThreadID(); + if (!strlen($thread_id)) { + return $headers; + } + + $is_first = $mail->getIsFirstMessage(); + + // NOTE: Gmail freaks out about In-Reply-To and References which aren't in + // the form ""; this is also required by RFC 2822, + // although some clients are more liberal in what they accept. + $domain = $this->newMailDomain(); + $thread_id = '<'.$thread_id.'@'.$domain.'>'; + + if ($is_first && $mailer->supportsMessageIDHeader()) { + $headers[] = $this->newEmailHeader('Message-ID', $thread_id); + } else { + $in_reply_to = $thread_id; + $references = array($thread_id); + $parent_id = $mail->getParentMessageID(); + if ($parent_id) { + $in_reply_to = $parent_id; + // By RFC 2822, the most immediate parent should appear last + // in the "References" header, so this order is intentional. + $references[] = $parent_id; + } + $references = implode(' ', $references); + $headers[] = $this->newEmailHeader('In-Reply-To', $in_reply_to); + $headers[] = $this->newEmailHeader('References', $references); + } + $thread_index = $this->generateThreadIndex($thread_id, $is_first); + $headers[] = $this->newEmailHeader('Thread-Index', $thread_index); + + return $headers; + } + + private function newEmailAttachments() { + $mail = $this->getMail(); + + // If the mail content must be encrypted, don't add attachments. + $must_encrypt = $mail->getMustEncrypt(); + if ($must_encrypt) { + return array(); + } + + return $mail->getAttachments(); + } + +/* -( Preferences )-------------------------------------------------------- */ + + private function shouldAddRePrefix() { + $preferences = $this->getPreferences(); + + $value = $preferences->getSettingValue( + PhabricatorEmailRePrefixSetting::SETTINGKEY); + + return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX); + } + + private function shouldVarySubject() { + $preferences = $this->getPreferences(); + + $value = $preferences->getSettingValue( + PhabricatorEmailVarySubjectsSetting::SETTINGKEY); + + return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS); + } + + private function shouldSendHTML() { + $preferences = $this->getPreferences(); + + $value = $preferences->getSettingValue( + PhabricatorEmailFormatSetting::SETTINGKEY); + + return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL); + } + + +/* -( Utilities )---------------------------------------------------------- */ + + private function newEmailHeader($name, $value) { + return id(new PhabricatorMailHeader()) + ->setName($name) + ->setValue($value); + } + + private function newEmailAddress($address, $name = null) { + $object = id(new PhutilEmailAddress()) + ->setAddress($address); + + if (strlen($name)) { + $object->setDisplayName($name); + } + + return $object; + } + + public function newDefaultEmailAddress() { + $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address'); + + if (!strlen($raw_address)) { + $domain = $this->newMailDomain(); + $raw_address = "noreply@{$domain}"; + } + + $address = new PhutilEmailAddress($raw_address); + + if (!strlen($address->getDisplayName())) { + $address->setDisplayName(pht('Phabricator')); + } + + return $address; + } + + public function newVoidEmailAddress() { + return $this->newDefaultEmailAddress(); + } + + private function newMailDomain() { + $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); + if (strlen($domain)) { + return $domain; + } + + $install_uri = PhabricatorEnv::getURI('/'); + $install_uri = new PhutilURI($install_uri); + + return $install_uri->getDomain(); + } + + private function filterHeaders(array $headers, $must_encrypt) { + assert_instances_of($headers, 'PhabricatorMailHeader'); + + if (!$must_encrypt) { + return $headers; + } + + $whitelist = array( + 'In-Reply-To', + 'Message-ID', + 'Precedence', + 'References', + 'Thread-Index', + 'Thread-Topic', + + 'X-Mail-Transport-Agent', + 'X-Auto-Response-Suppress', + + 'X-Phabricator-Sent-This-Message', + 'X-Phabricator-Must-Encrypt', + 'X-Phabricator-Mail-ID', + 'X-Phabricator-Send-Attempt', + ); + + // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". + // This header contains a significant amount of meaningful information + // about the object. + + $whitelist_map = array(); + foreach ($whitelist as $term) { + $whitelist_map[phutil_utf8_strtolower($term)] = true; + } + + foreach ($headers as $key => $header) { + $name = $header->getName(); + $name = phutil_utf8_strtolower($name); + + if (!isset($whitelist_map[$name])) { + unset($headers[$key]); + } + } + + return $headers; + } + + private function getUniqueEmailAddresses( + array $addresses, + array $exclude = array()) { + assert_instances_of($addresses, 'PhutilEmailAddress'); + assert_instances_of($exclude, 'PhutilEmailAddress'); + + $seen = array(); + + foreach ($exclude as $address) { + $seen[$address->getAddress()] = true; + } + + foreach ($addresses as $key => $address) { + $raw_address = $address->getAddress(); + + if (isset($seen[$raw_address])) { + unset($addresses[$key]); + continue; + } + + $seen[$raw_address] = true; + } + + return array_values($addresses); + } + + private function generateThreadIndex($seed, $is_first_mail) { + // When threading, Outlook ignores the 'References' and 'In-Reply-To' + // headers that most clients use. Instead, it uses a custom 'Thread-Index' + // header. The format of this header is something like this (from + // camel-exchange-folder.c in Evolution Exchange): + + /* A new post to a folder gets a 27-byte-long thread index. (The value + * is apparently unique but meaningless.) Each reply to a post gets a + * 32-byte-long thread index whose first 27 bytes are the same as the + * parent's thread index. Each reply to any of those gets a + * 37-byte-long thread index, etc. The Thread-Index header contains a + * base64 representation of this value. + */ + + // The specific implementation uses a 27-byte header for the first email + // a recipient receives, and a random 5-byte suffix (32 bytes total) + // thereafter. This means that all the replies are (incorrectly) siblings, + // but it would be very difficult to keep track of the entire tree and this + // gets us reasonable client behavior. + + $base = substr(md5($seed), 0, 27); + if (!$is_first_mail) { + // Not totally sure, but it seems like outlook orders replies by + // thread-index rather than timestamp, so to get these to show up in the + // right order we use the time as the last 4 bytes. + $base .= ' '.pack('N', time()); + } + + return base64_encode($base); + } + + private function shouldRateLimitMail(array $all_recipients) { + try { + PhabricatorSystemActionEngine::willTakeAction( + $all_recipients, + new PhabricatorMetaMTAErrorMailAction(), + 1); + return false; + } catch (PhabricatorSystemActionRateLimitException $ex) { + return true; + } + } + +} diff --git a/src/applications/metamta/engine/PhabricatorMailMessageEngine.php b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php new file mode 100644 index 0000000000..752096b146 --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php @@ -0,0 +1,55 @@ +mailer = $mailer; + return $this; + } + + final public function getMailer() { + return $this->mailer; + } + + final public function setMail(PhabricatorMetaMTAMail $mail) { + $this->mail = $mail; + return $this; + } + + final public function getMail() { + return $this->mail; + } + + final public function setActors(array $actors) { + assert_instances_of($actors, 'PhabricatorMetaMTAActor'); + $this->actors = $actors; + return $this; + } + + final public function getActors() { + return $this->actors; + } + + final public function getActor($phid) { + return idx($this->actors, $phid); + } + + final public function setPreferences( + PhabricatorUserPreferences $preferences) { + $this->preferences = $preferences; + return $this; + } + + final public function getPreferences() { + return $this->preferences; + } + +} diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php index 0fc7dd14b9..d462314342 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -116,10 +116,6 @@ final class PhabricatorMailManagementShowOutboundWorkflow $headers = $message->getDeliveredHeaders(); $unfiltered = $message->getUnfilteredHeaders(); - if (!$unfiltered) { - $headers = $message->generateHeaders(); - $unfiltered = $headers; - } $header_map = array(); foreach ($headers as $header) { diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 1117c4273d..078fe00a7a 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -191,13 +191,17 @@ final class PhabricatorMetaMTAMail return $this; } + public function getHeaders() { + return $this->getParam('headers', array()); + } + public function addAttachment(PhabricatorMailAttachment $attachment) { $this->parameters['attachments'][] = $attachment->toDictionary(); return $this; } public function getAttachments() { - $dicts = $this->getParam('attachments'); + $dicts = $this->getParam('attachments', array()); $result = array(); foreach ($dicts as $dict) { @@ -256,11 +260,19 @@ final class PhabricatorMetaMTAMail return $this; } + public function getRawFrom() { + return $this->getParam('raw-from'); + } + public function setReplyTo($reply_to) { $this->setParam('reply-to', $reply_to); return $this; } + public function getReplyTo() { + return $this->getParam('reply-to'); + } + public function setSubject($subject) { $this->setParam('subject', $subject); return $this; @@ -271,11 +283,19 @@ final class PhabricatorMetaMTAMail return $this; } + public function getSubjectPrefix() { + return $this->getParam('subject-prefix'); + } + public function setVarySubjectPrefix($prefix) { $this->setParam('vary-subject-prefix', $prefix); return $this; } + public function getVarySubjectPrefix() { + return $this->getParam('vary-subject-prefix'); + } + public function setBody($body) { $this->setParam('body', $body); return $this; @@ -413,6 +433,10 @@ final class PhabricatorMetaMTAMail return $this; } + public function getIsBulk() { + return $this->getParam('is-bulk'); + } + /** * Use this method to set an ID used for message threading. MetaMTA will * set appropriate headers (Message-ID, In-Reply-To, References and @@ -429,6 +453,14 @@ final class PhabricatorMetaMTAMail return $this; } + public function getThreadID() { + return $this->getParam('thread-id'); + } + + public function getIsFirstMessage() { + return (bool)$this->getParam('is-first-message'); + } + /** * Save a newly created mail to the database. The mail will eventually be * delivered by the MetaMTA daemon. @@ -597,10 +629,6 @@ final class PhabricatorMetaMTAMail } } - foreach ($sorted as $mailer) { - $mailer->prepareForSend(); - } - return $sorted; } @@ -627,36 +655,51 @@ final class PhabricatorMetaMTAMail ->save(); } - $exceptions = array(); - foreach ($mailers as $template_mailer) { - $mailer = null; + $actors = $this->loadAllActors(); + // If we're sending one mail to everyone, some recipients will be in + // "Cc" rather than "To". We'll move them to "To" later (or supply a + // dummy "To") but need to look for the recipient in either the + // "To" or "Cc" fields here. + $target_phid = head($this->getToPHIDs()); + if (!$target_phid) { + $target_phid = head($this->getCcPHIDs()); + } + $preferences = $this->loadPreferences($target_phid); + + // Attach any files we're about to send to this message, so the recipients + // can view them. + $viewer = PhabricatorUser::getOmnipotentUser(); + $files = $this->loadAttachedFiles($viewer); + foreach ($files as $file) { + $file->attachToObject($this->getPHID()); + } + + $exceptions = array(); + foreach ($mailers as $mailer) { try { - $mailer = $this->buildMailer($template_mailer); + $message = id(new PhabricatorMailEmailEngine()) + ->setMailer($mailer) + ->setMail($this) + ->setActors($actors) + ->setPreferences($preferences) + ->newMessage($mailer); } catch (Exception $ex) { $exceptions[] = $ex; continue; } - if (!$mailer) { - // If we don't get a mailer back, that means the mail doesn't - // actually need to be sent (for example, because recipients have - // declined to receive the mail). Void it and return. + if (!$message) { + // If we don't get a message back, that means the mail doesn't actually + // need to be sent (for example, because recipients have declined to + // receive the mail). Void it and return. return $this ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID) ->save(); } try { - $ok = $mailer->send(); - if (!$ok) { - // TODO: At some point, we should clean this up and make all mailers - // throw. - throw new Exception( - pht( - 'Mail adapter encountered an unexpected, unspecified '. - 'failure.')); - } + $mailer->sendMessage($message); } catch (PhabricatorMetaMTAPermanentFailureException $ex) { // If any mailer raises a permanent failure, stop trying to send the // mail with other mailers. @@ -677,6 +720,19 @@ final class PhabricatorMetaMTAMail $this->setParam('mailer.key', $mailer_key); } + // Now that we sent the message, store the final deliverability outcomes + // and reasoning so we can explain why things happened the way they did. + $actor_list = array(); + foreach ($actors as $actor) { + $actor_list[$actor->getPHID()] = array( + 'deliverable' => $actor->isDeliverable(), + 'reasons' => $actor->getDeliverabilityReasons(), + ); + } + $this->setParam('actors.sent', $actor_list); + $this->setParam('routing.sent', $this->getParam('routing')); + $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); + return $this ->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT) ->save(); @@ -705,368 +761,6 @@ final class PhabricatorMetaMTAMail $exceptions); } - private function buildMailer(PhabricatorMailImplementationAdapter $mailer) { - $headers = $this->generateHeaders(); - - $params = $this->parameters; - - $actors = $this->loadAllActors(); - $deliverable_actors = $this->filterDeliverableActors($actors); - - $default_from = (string)$this->newDefaultEmailAddress(); - if (empty($params['from'])) { - $mailer->setFrom($default_from); - } - - $is_first = idx($params, 'is-first-message'); - unset($params['is-first-message']); - - $is_threaded = (bool)idx($params, 'thread-id'); - $must_encrypt = $this->getMustEncrypt(); - - $reply_to_name = idx($params, 'reply-to-name', ''); - unset($params['reply-to-name']); - - $add_cc = array(); - $add_to = array(); - - // If we're sending one mail to everyone, some recipients will be in - // "Cc" rather than "To". We'll move them to "To" later (or supply a - // dummy "To") but need to look for the recipient in either the - // "To" or "Cc" fields here. - $target_phid = head(idx($params, 'to', array())); - if (!$target_phid) { - $target_phid = head(idx($params, 'cc', array())); - } - - $preferences = $this->loadPreferences($target_phid); - - foreach ($params as $key => $value) { - switch ($key) { - case 'raw-from': - list($from_email, $from_name) = $value; - $mailer->setFrom($from_email, $from_name); - break; - case 'from': - // If the mail content must be encrypted, disguise the sender. - if ($must_encrypt) { - $mailer->setFrom($default_from, pht('Phabricator')); - break; - } - - $from = $value; - $actor_email = null; - $actor_name = null; - $actor = idx($actors, $from); - if ($actor) { - $actor_email = $actor->getEmailAddress(); - $actor_name = $actor->getName(); - } - $can_send_as_user = $actor_email && - PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); - - if ($can_send_as_user) { - $mailer->setFrom($actor_email, $actor_name); - } else { - $from_email = coalesce($actor_email, $default_from); - $from_name = coalesce($actor_name, pht('Phabricator')); - - if (empty($params['reply-to'])) { - $params['reply-to'] = $from_email; - $params['reply-to-name'] = $from_name; - } - - $mailer->setFrom($default_from, $from_name); - } - break; - case 'reply-to': - $mailer->addReplyTo($value, $reply_to_name); - break; - case 'to': - $to_phids = $this->expandRecipients($value); - $to_actors = array_select_keys($deliverable_actors, $to_phids); - $add_to = array_merge( - $add_to, - mpull($to_actors, 'getEmailAddress')); - break; - case 'raw-to': - $add_to = array_merge($add_to, $value); - break; - case 'cc': - $cc_phids = $this->expandRecipients($value); - $cc_actors = array_select_keys($deliverable_actors, $cc_phids); - $add_cc = array_merge( - $add_cc, - mpull($cc_actors, 'getEmailAddress')); - break; - case 'attachments': - $attached_viewer = PhabricatorUser::getOmnipotentUser(); - $files = $this->loadAttachedFiles($attached_viewer); - foreach ($files as $file) { - $file->attachToObject($this->getPHID()); - } - - // If the mail content must be encrypted, don't add attachments. - if ($must_encrypt) { - break; - } - - $value = $this->getAttachments(); - foreach ($value as $attachment) { - $mailer->addAttachment( - $attachment->getData(), - $attachment->getFilename(), - $attachment->getMimeType()); - } - break; - case 'subject': - $subject = array(); - - if ($is_threaded) { - if ($this->shouldAddRePrefix($preferences)) { - $subject[] = 'Re:'; - } - } - - $subject[] = trim(idx($params, 'subject-prefix')); - - // If mail content must be encrypted, we replace the subject with - // a generic one. - if ($must_encrypt) { - $encrypt_subject = $this->getMustEncryptSubject(); - if (!strlen($encrypt_subject)) { - $encrypt_subject = pht('Object Updated'); - } - $subject[] = $encrypt_subject; - } else { - $vary_prefix = idx($params, 'vary-subject-prefix'); - if ($vary_prefix != '') { - if ($this->shouldVarySubject($preferences)) { - $subject[] = $vary_prefix; - } - } - - $subject[] = $value; - } - - $mailer->setSubject(implode(' ', array_filter($subject))); - break; - case 'thread-id': - - // NOTE: Gmail freaks out about In-Reply-To and References which - // aren't in the form ""; this is also required - // by RFC 2822, although some clients are more liberal in what they - // accept. - $domain = $this->newMailDomain(); - $value = '<'.$value.'@'.$domain.'>'; - - if ($is_first && $mailer->supportsMessageIDHeader()) { - $headers[] = array('Message-ID', $value); - } else { - $in_reply_to = $value; - $references = array($value); - $parent_id = $this->getParentMessageID(); - if ($parent_id) { - $in_reply_to = $parent_id; - // By RFC 2822, the most immediate parent should appear last - // in the "References" header, so this order is intentional. - $references[] = $parent_id; - } - $references = implode(' ', $references); - $headers[] = array('In-Reply-To', $in_reply_to); - $headers[] = array('References', $references); - } - $thread_index = $this->generateThreadIndex($value, $is_first); - $headers[] = array('Thread-Index', $thread_index); - break; - default: - // Other parameters are handled elsewhere or are not relevant to - // constructing the message. - break; - } - } - - $stamps = $this->getMailStamps(); - if ($stamps) { - $headers[] = array('X-Phabricator-Stamps', implode(' ', $stamps)); - } - - $raw_body = idx($params, 'body', ''); - $body = $raw_body; - if ($must_encrypt) { - $parts = array(); - - $encrypt_uri = $this->getMustEncryptURI(); - if (!strlen($encrypt_uri)) { - $encrypt_phid = $this->getRelatedPHID(); - if ($encrypt_phid) { - $encrypt_uri = urisprintf( - '/object/%s/', - $encrypt_phid); - } - } - - if (strlen($encrypt_uri)) { - $parts[] = pht( - 'This secure message is notifying you of a change to this object:'); - $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri); - } - - $parts[] = pht( - 'The content for this message can only be transmitted over a '. - 'secure channel. To view the message content, follow this '. - 'link:'); - - $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); - - $body = implode("\n\n", $parts); - } else { - $body = $raw_body; - } - - $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); - if (strlen($body) > $body_limit) { - $body = id(new PhutilUTF8StringTruncator()) - ->setMaximumBytes($body_limit) - ->truncateString($body); - $body .= "\n"; - $body .= pht('(This email was truncated at %d bytes.)', $body_limit); - } - $mailer->setBody($body); - $body_limit -= strlen($body); - - // If we sent a different message body than we were asked to, record - // what we actually sent to make debugging and diagnostics easier. - if ($body !== $raw_body) { - $this->setParam('body.sent', $body); - } - - if ($must_encrypt) { - $send_html = false; - } else { - $send_html = $this->shouldSendHTML($preferences); - } - - if ($send_html) { - $html_body = idx($params, 'html-body'); - if (strlen($html_body)) { - // NOTE: We just drop the entire HTML body if it won't fit. Safely - // truncating HTML is hard, and we already have the text body to fall - // back to. - if (strlen($html_body) <= $body_limit) { - $mailer->setHTMLBody($html_body); - $body_limit -= strlen($html_body); - } - } - } - - // Pass the headers to the mailer, then save the state so we can show - // them in the web UI. If the mail must be encrypted, we remove headers - // which are not on a strict whitelist to avoid disclosing information. - $filtered_headers = $this->filterHeaders($headers, $must_encrypt); - foreach ($filtered_headers as $header) { - list($header_key, $header_value) = $header; - $mailer->addHeader($header_key, $header_value); - } - $this->setParam('headers.unfiltered', $headers); - $this->setParam('headers.sent', $filtered_headers); - - // Save the final deliverability outcomes and reasoning so we can - // explain why things happened the way they did. - $actor_list = array(); - foreach ($actors as $actor) { - $actor_list[$actor->getPHID()] = array( - 'deliverable' => $actor->isDeliverable(), - 'reasons' => $actor->getDeliverabilityReasons(), - ); - } - $this->setParam('actors.sent', $actor_list); - - $this->setParam('routing.sent', $this->getParam('routing')); - $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); - - if (!$add_to && !$add_cc) { - $this->setMessage( - pht( - 'Message has no valid recipients: all To/Cc are disabled, '. - 'invalid, or configured not to receive this mail.')); - - return null; - } - - if ($this->getIsErrorEmail()) { - $all_recipients = array_merge($add_to, $add_cc); - if ($this->shouldRateLimitMail($all_recipients)) { - $this->setMessage( - pht( - 'This is an error email, but one or more recipients have '. - 'exceeded the error email rate limit. Declining to deliver '. - 'message.')); - - return null; - } - } - - if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { - $this->setMessage( - pht( - 'Phabricator is running in silent mode. See `%s` '. - 'in the configuration to change this setting.', - 'phabricator.silent')); - - return null; - } - - // Some mailers require a valid "To:" in order to deliver mail. If we don't - // have any "To:", fill it in with a placeholder "To:". This allows client - // rules based on whether the recipient is in "To:" or "CC:" to continue - // behaving in the same way. - if (!$add_to) { - $void_recipient = $this->newVoidEmailAddress(); - $add_to = array($void_recipient->getAddress()); - } - - $add_to = array_unique($add_to); - $add_cc = array_diff(array_unique($add_cc), $add_to); - - $mailer->addTos($add_to); - if ($add_cc) { - $mailer->addCCs($add_cc); - } - - return $mailer; - } - - private function generateThreadIndex($seed, $is_first_mail) { - // When threading, Outlook ignores the 'References' and 'In-Reply-To' - // headers that most clients use. Instead, it uses a custom 'Thread-Index' - // header. The format of this header is something like this (from - // camel-exchange-folder.c in Evolution Exchange): - - /* A new post to a folder gets a 27-byte-long thread index. (The value - * is apparently unique but meaningless.) Each reply to a post gets a - * 32-byte-long thread index whose first 27 bytes are the same as the - * parent's thread index. Each reply to any of those gets a - * 37-byte-long thread index, etc. The Thread-Index header contains a - * base64 representation of this value. - */ - - // The specific implementation uses a 27-byte header for the first email - // a recipient receives, and a random 5-byte suffix (32 bytes total) - // thereafter. This means that all the replies are (incorrectly) siblings, - // but it would be very difficult to keep track of the entire tree and this - // gets us reasonable client behavior. - - $base = substr(md5($seed), 0, 27); - if (!$is_first_mail) { - // Not totally sure, but it seems like outlook orders replies by - // thread-index rather than timestamp, so to get these to show up in the - // right order we use the time as the last 4 bytes. - $base .= ' '.pack('N', time()); - } - - return base64_encode($base); - } public static function shouldMailEachRecipient() { return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); @@ -1120,7 +814,7 @@ final class PhabricatorMetaMTAMail * recipients. * @return list Deaggregated list of mailable recipients. */ - private function expandRecipients(array $phids) { + public function expandRecipients(array $phids) { if ($this->recipientExpansionMap === null) { $all_phids = $this->getAllActorPHIDs(); $this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery()) @@ -1320,72 +1014,15 @@ final class PhabricatorMetaMTAMail return $actors; } - private function shouldRateLimitMail(array $all_recipients) { - try { - PhabricatorSystemActionEngine::willTakeAction( - $all_recipients, - new PhabricatorMetaMTAErrorMailAction(), - 1); - return false; - } catch (PhabricatorSystemActionRateLimitException $ex) { - return true; - } - } - - public function generateHeaders() { - $headers = array(); - - $headers[] = array('X-Phabricator-Sent-This-Message', 'Yes'); - $headers[] = array('X-Mail-Transport-Agent', 'MetaMTA'); - - // Some clients respect this to suppress OOF and other auto-responses. - $headers[] = array('X-Auto-Response-Suppress', 'All'); - - $mailtags = $this->getParam('mailtags'); - if ($mailtags) { - $tag_header = array(); - foreach ($mailtags as $mailtag) { - $tag_header[] = '<'.$mailtag.'>'; - } - $tag_header = implode(', ', $tag_header); - $headers[] = array('X-Phabricator-Mail-Tags', $tag_header); - } - - $value = $this->getParam('headers', array()); - foreach ($value as $pair) { - list($header_key, $header_value) = $pair; - - // NOTE: If we have \n in a header, SES rejects the email. - $header_value = str_replace("\n", ' ', $header_value); - $headers[] = array($header_key, $header_value); - } - - $is_bulk = $this->getParam('is-bulk'); - if ($is_bulk) { - $headers[] = array('Precedence', 'bulk'); - } - - if ($this->getMustEncrypt()) { - $headers[] = array('X-Phabricator-Must-Encrypt', 'Yes'); - } - - $related_phid = $this->getRelatedPHID(); - if ($related_phid) { - $headers[] = array('Thread-Topic', $related_phid); - } - - $headers[] = array('X-Phabricator-Mail-ID', $this->getID()); - - $unique = Filesystem::readRandomCharacters(16); - $headers[] = array('X-Phabricator-Send-Attempt', $unique); - - return $headers; - } - public function getDeliveredHeaders() { return $this->getParam('headers.sent'); } + public function setDeliveredHeaders(array $headers) { + $headers = $this->flattenHeaders($headers); + return $this->setParam('headers.sent', $headers); + } + public function getUnfilteredHeaders() { $unfiltered = $this->getParam('headers.unfiltered'); @@ -1399,6 +1036,25 @@ final class PhabricatorMetaMTAMail return $unfiltered; } + public function setUnfilteredHeaders(array $headers) { + $headers = $this->flattenHeaders($headers); + return $this->setParam('headers.unfiltered', $headers); + } + + private function flattenHeaders(array $headers) { + assert_instances_of($headers, 'PhabricatorMailHeader'); + + $list = array(); + foreach ($list as $header) { + $list[] = array( + $header->getName(), + $header->getValue(), + ); + } + + return $list; + } + public function getDeliveredActors() { return $this->getParam('actors.sent'); } @@ -1415,81 +1071,14 @@ final class PhabricatorMetaMTAMail return $this->getParam('body.sent'); } - private function filterHeaders(array $headers, $must_encrypt) { - if (!$must_encrypt) { - return $headers; - } - - $whitelist = array( - 'In-Reply-To', - 'Message-ID', - 'Precedence', - 'References', - 'Thread-Index', - 'Thread-Topic', - - 'X-Mail-Transport-Agent', - 'X-Auto-Response-Suppress', - - 'X-Phabricator-Sent-This-Message', - 'X-Phabricator-Must-Encrypt', - 'X-Phabricator-Mail-ID', - 'X-Phabricator-Send-Attempt', - ); - - // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". - // This header contains a significant amount of meaningful information - // about the object. - - $whitelist_map = array(); - foreach ($whitelist as $term) { - $whitelist_map[phutil_utf8_strtolower($term)] = true; - } - - foreach ($headers as $key => $header) { - list($name, $value) = $header; - $name = phutil_utf8_strtolower($name); - - if (!isset($whitelist_map[$name])) { - unset($headers[$key]); - } - } - - return $headers; + public function setDeliveredBody($body) { + return $this->setParam('body.sent', $body); } public function getURI() { return '/mail/detail/'.$this->getID().'/'; } - private function newMailDomain() { - $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); - if (strlen($domain)) { - return $domain; - } - - $install_uri = PhabricatorEnv::getURI('/'); - $install_uri = new PhutilURI($install_uri); - - return $install_uri->getDomain(); - } - - public function newDefaultEmailAddress() { - $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address'); - if (strlen($raw_address)) { - return new PhutilEmailAddress($raw_address); - } - - $domain = $this->newMailDomain(); - $address = "noreply@{$domain}"; - - return new PhutilEmailAddress($address); - } - - public function newVoidEmailAddress() { - return $this->newDefaultEmailAddress(); - } - /* -( Routing )------------------------------------------------------------ */ @@ -1578,27 +1167,6 @@ final class PhabricatorMetaMTAMail return PhabricatorUserPreferences::loadGlobalPreferences($viewer); } - private function shouldAddRePrefix(PhabricatorUserPreferences $preferences) { - $value = $preferences->getSettingValue( - PhabricatorEmailRePrefixSetting::SETTINGKEY); - - return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX); - } - - private function shouldVarySubject(PhabricatorUserPreferences $preferences) { - $value = $preferences->getSettingValue( - PhabricatorEmailVarySubjectsSetting::SETTINGKEY); - - return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS); - } - - private function shouldSendHTML(PhabricatorUserPreferences $preferences) { - $value = $preferences->getSettingValue( - PhabricatorEmailFormatSetting::SETTINGKEY); - - return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL); - } - public function shouldRenderMailStampsInBody($viewer) { $preferences = $this->loadPreferences($viewer->getPHID()); $value = $preferences->getSettingValue( From a8657e6ab6fd3feb3b880329f46df2a45b67dc82 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 4 Jan 2019 05:40:26 -0800 Subject: [PATCH 09/35] Update Postmark adapter for multiple mail media Summary: Depends on D19955. Ref T920. Ref T5969. Update Postmark to accept new Message objects. Also: - Update the inbound whitelist. - Add a little support for `media` configuration. - Add a service call timeout (see T5969). - Drop the needless word "Implementation" from the Adapter class tree. I could call these "Mailers" instead of "Adapters", but then we get "PhabricatorMailMailer" which feels questionable. Test Plan: Used `bin/mail send-test` to send mail via Postmark with various options (mulitple recipients, text vs html, attachments). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5969, T920 Differential Revision: https://secure.phabricator.com/D19956 --- src/__phutil_library_map__.php | 32 ++--- ...Adapter.php => PhabricatorMailAdapter.php} | 99 ++++++++------ ...hp => PhabricatorMailAmazonSESAdapter.php} | 4 +- ...catorMailImplementationPostmarkAdapter.php | 120 ---------------- ....php => PhabricatorMailMailgunAdapter.php} | 4 +- ...hp => PhabricatorMailPHPMailerAdapter.php} | 4 +- ...> PhabricatorMailPHPMailerLiteAdapter.php} | 6 +- .../PhabricatorMailPostmarkAdapter.php | 128 ++++++++++++++++++ ...php => PhabricatorMailSendGridAdapter.php} | 4 +- ...ter.php => PhabricatorMailTestAdapter.php} | 4 +- ...ricatorMetaMTAMailgunReceiveController.php | 2 +- ...icatorMetaMTAPostmarkReceiveController.php | 2 +- ...icatorMetaMTASendGridReceiveController.php | 2 +- .../engine/PhabricatorMailMessageEngine.php | 3 +- .../storage/PhabricatorMetaMTAMail.php | 26 +++- .../PhabricatorMetaMTAMailTestCase.php | 20 +-- .../configuring_outbound_email.diviner | 18 ++- .../PhabricatorClusterMailersConfigType.php | 2 +- 18 files changed, 272 insertions(+), 208 deletions(-) rename src/applications/metamta/adapter/{PhabricatorMailImplementationAdapter.php => PhabricatorMailAdapter.php} (50%) rename src/applications/metamta/adapter/{PhabricatorMailImplementationAmazonSESAdapter.php => PhabricatorMailAmazonSESAdapter.php} (91%) delete mode 100644 src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php rename src/applications/metamta/adapter/{PhabricatorMailImplementationMailgunAdapter.php => PhabricatorMailMailgunAdapter.php} (97%) rename src/applications/metamta/adapter/{PhabricatorMailImplementationPHPMailerAdapter.php => PhabricatorMailPHPMailerAdapter.php} (97%) rename src/applications/metamta/adapter/{PhabricatorMailImplementationPHPMailerLiteAdapter.php => PhabricatorMailPHPMailerLiteAdapter.php} (96%) create mode 100644 src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php rename src/applications/metamta/adapter/{PhabricatorMailImplementationSendGridAdapter.php => PhabricatorMailSendGridAdapter.php} (97%) rename src/applications/metamta/adapter/{PhabricatorMailImplementationTestAdapter.php => PhabricatorMailTestAdapter.php} (96%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2b620b11d8..fb6cfed1fa 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3387,6 +3387,8 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php', 'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php', 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', + 'PhabricatorMailAdapter' => 'applications/metamta/adapter/PhabricatorMailAdapter.php', + 'PhabricatorMailAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php', 'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php', 'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php', 'PhabricatorMailEmailEngine' => 'applications/metamta/engine/PhabricatorMailEmailEngine.php', @@ -3397,14 +3399,7 @@ phutil_register_library_map(array( 'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php', 'PhabricatorMailExternalMessage' => 'applications/metamta/message/PhabricatorMailExternalMessage.php', 'PhabricatorMailHeader' => 'applications/metamta/message/PhabricatorMailHeader.php', - 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', - 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', - 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', - 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php', - 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', - 'PhabricatorMailImplementationPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php', - 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', - 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php', + 'PhabricatorMailMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailMailgunAdapter.php', 'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php', 'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php', 'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php', @@ -3422,14 +3417,19 @@ phutil_register_library_map(array( 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php', 'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php', + 'PhabricatorMailPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailPHPMailerAdapter.php', + 'PhabricatorMailPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailPHPMailerLiteAdapter.php', + 'PhabricatorMailPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php', 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', + 'PhabricatorMailSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailSendGridAdapter.php', 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', + 'PhabricatorMailTestAdapter' => 'applications/metamta/adapter/PhabricatorMailTestAdapter.php', 'PhabricatorMailUtil' => 'applications/metamta/util/PhabricatorMailUtil.php', 'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php', 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', @@ -9221,6 +9221,8 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', + 'PhabricatorMailAdapter' => 'Phobject', + 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailPHPMailerLiteAdapter', 'PhabricatorMailAttachment' => 'Phobject', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', @@ -9231,14 +9233,7 @@ phutil_register_library_map(array( 'PhabricatorMailEngineExtension' => 'Phobject', 'PhabricatorMailExternalMessage' => 'Phobject', 'PhabricatorMailHeader' => 'Phobject', - 'PhabricatorMailImplementationAdapter' => 'Phobject', - 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', - 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', - 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter', - 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', - 'PhabricatorMailImplementationPostmarkAdapter' => 'PhabricatorMailImplementationAdapter', - 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', - 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', + 'PhabricatorMailMailgunAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow', @@ -9256,14 +9251,19 @@ phutil_register_library_map(array( 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundStatus' => 'Phobject', + 'PhabricatorMailPHPMailerAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailPHPMailerLiteAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailPostmarkAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorMailReceiver' => 'Phobject', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', + 'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', + 'PhabricatorMailTestAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailUtil' => 'Phobject', 'PhabricatorMainMenuBarExtension' => 'Phobject', 'PhabricatorMainMenuSearchView' => 'AphrontView', diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php similarity index 50% rename from src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailAdapter.php index a8dba2335b..b3833e779e 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php @@ -1,13 +1,16 @@ getPhobjectClassConstant('ADAPTERTYPE'); @@ -20,37 +23,67 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { ->execute(); } + /* abstract */ public function getSupportedMessageTypes() { + throw new PhutilMethodNotImplementedException(); + } - abstract public function setFrom($email, $name = ''); - abstract public function addReplyTo($email, $name = ''); - abstract public function addTos(array $emails); - abstract public function addCCs(array $emails); - abstract public function addAttachment($data, $filename, $mimetype); - abstract public function addHeader($header_name, $header_value); - abstract public function setBody($plaintext_body); - abstract public function setHTMLBody($html_body); - abstract public function setSubject($subject); - + /* abstract */ public function sendMessage( + PhabricatorMailExternalMessage $message) { + throw new PhutilMethodNotImplementedException(); + } /** - * Some mailers, notably Amazon SES, do not support us setting a specific - * Message-ID header. + * Return true if this adapter supports setting a "Message-ID" when sending + * email. + * + * This is an ugly implementation detail because mail threading is a horrible + * mess, implemented differently by every client in existence. */ - abstract public function supportsMessageIDHeader(); + public function supportsMessageIDHeader() { + return false; + } + final public function supportsMessageType($message_type) { + if ($this->mediaMap === null) { + $media_map = $this->getSupportedMessageTypes(); + $media_map = array_fuse($media_map); - /** - * Send the message. Generally, this means connecting to some service and - * handing data to it. - * - * If the adapter determines that the mail will never be deliverable, it - * should throw a @{class:PhabricatorMetaMTAPermanentFailureException}. - * - * For temporary failures, throw some other exception or return `false`. - * - * @return bool True on success. - */ - abstract public function send(); + if ($this->media) { + $config_map = $this->media; + $config_map = array_fuse($config_map); + + $media_map = array_intersect_key($media_map, $config_map); + } + + $this->mediaMap = $media_map; + } + + return isset($this->mediaMap[$message_type]); + } + + final public function setMedia(array $media) { + $native_map = $this->getSupportedMessageTypes(); + $native_map = array_fuse($native_map); + + foreach ($media as $medium) { + if (!isset($native_map[$medium])) { + throw new Exception( + pht( + 'Adapter ("%s") is configured for medium "%s", but this is not '. + 'a supported delivery medium. Supported media are: %s.', + $medium, + implode(', ', $native_map))); + } + } + + $this->media = $media; + $this->mediaMap = null; + return $this; + } + + final public function getMedia() { + return $this->media; + } final public function setKey($key) { $this->key = $key; @@ -110,18 +143,4 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { abstract public function newDefaultOptions(); - public function prepareForSend() { - return; - } - - protected function renderAddress($email, $name = null) { - if (strlen($name)) { - return (string)id(new PhutilEmailAddress()) - ->setDisplayName($name) - ->setAddress($email); - } else { - return $email; - } - } - } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php similarity index 91% rename from src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php index f847488019..31168293a9 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php @@ -1,7 +1,7 @@ parameters['From'] = $this->renderAddress($email, $name); - return $this; - } - - public function addReplyTo($email, $name = '') { - $this->parameters['ReplyTo'] = $this->renderAddress($email, $name); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->parameters['To'][] = $email; - } - return $this; - } - - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->parameters['Cc'][] = $email; - } - return $this; - } - - public function addAttachment($data, $filename, $mimetype) { - $this->parameters['Attachments'][] = array( - 'Name' => $filename, - 'ContentType' => $mimetype, - 'Content' => base64_encode($data), - ); - - return $this; - } - - public function addHeader($header_name, $header_value) { - $this->parameters['Headers'][] = array( - 'Name' => $header_name, - 'Value' => $header_value, - ); - return $this; - } - - public function setBody($body) { - $this->parameters['TextBody'] = $body; - return $this; - } - - public function setHTMLBody($html_body) { - $this->parameters['HtmlBody'] = $html_body; - return $this; - } - - public function setSubject($subject) { - $this->parameters['Subject'] = $subject; - return $this; - } - - public function supportsMessageIDHeader() { - return true; - } - - protected function validateOptions(array $options) { - PhutilTypeSpec::checkMap( - $options, - array( - 'access-token' => 'string', - 'inbound-addresses' => 'list', - )); - - // Make sure this is properly formatted. - PhutilCIDRList::newList($options['inbound-addresses']); - } - - public function newDefaultOptions() { - return array( - 'access-token' => null, - 'inbound-addresses' => array( - // Via Postmark support circa February 2018, see: - // - // https://postmarkapp.com/support/article/800-ips-for-firewalls - // - // "Configuring Outbound Email" should be updated if this changes. - '50.31.156.6/32', - ), - ); - } - - public function send() { - $access_token = $this->getOption('access-token'); - - $parameters = $this->parameters; - $flatten = array( - 'To', - 'Cc', - ); - - foreach ($flatten as $key) { - if (isset($parameters[$key])) { - $parameters[$key] = implode(', ', $parameters[$key]); - } - } - - id(new PhutilPostmarkFuture()) - ->setAccessToken($access_token) - ->setMethod('email', $parameters) - ->resolve(); - - return true; - } - -} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php similarity index 97% rename from src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php index b917a93df2..5a295812eb 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php @@ -3,8 +3,8 @@ /** * Mail adapter that uses Mailgun's web API to deliver email. */ -final class PhabricatorMailImplementationMailgunAdapter - extends PhabricatorMailImplementationAdapter { +final class PhabricatorMailMailgunAdapter + extends PhabricatorMailAdapter { const ADAPTERTYPE = 'mailgun'; diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php b/src/applications/metamta/adapter/PhabricatorMailPHPMailerAdapter.php similarity index 97% rename from src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailPHPMailerAdapter.php index fbc8c09cd9..0aeeef0a37 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailPHPMailerAdapter.php @@ -1,7 +1,7 @@ 'string', + 'inbound-addresses' => 'list', + )); + + // Make sure this is properly formatted. + PhutilCIDRList::newList($options['inbound-addresses']); + } + + public function newDefaultOptions() { + return array( + 'access-token' => null, + 'inbound-addresses' => array( + // Via Postmark support circa February 2018, see: + // + // https://postmarkapp.com/support/article/800-ips-for-firewalls + // + // "Configuring Outbound Email" should be updated if this changes. + // + // These addresses were last updated in January 2019. + '50.31.156.6/32', + '50.31.156.77/32', + '18.217.206.57/32', + ), + ); + } + + public function sendMessage(PhabricatorMailExternalMessage $message) { + $access_token = $this->getOption('access-token'); + + $parameters = array(); + + $subject = $message->getSubject(); + if ($subject !== null) { + $parameters['Subject'] = $subject; + } + + $from_address = $message->getFromAddress(); + if ($from_address) { + $parameters['From'] = (string)$from_address; + } + + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + $to = array(); + foreach ($to_addresses as $address) { + $to[] = (string)$address; + } + $parameters['To'] = implode(', ', $to); + } + + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + $cc = array(); + foreach ($cc_addresses as $address) { + $cc[] = (string)$address; + } + $parameters['Cc'] = implode(', ', $cc); + } + + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $parameters['ReplyTo'] = (string)$reply_address; + } + + $headers = $message->getHeaders(); + if ($headers) { + $list = array(); + foreach ($headers as $header) { + $list[] = array( + 'Name' => $header->getName(), + 'Value' => $header->getValue(), + ); + } + $parameters['Headers'] = $list; + } + + $text_body = $message->getTextBody(); + if ($text_body !== null) { + $parameters['TextBody'] = $text_body; + } + + $html_body = $message->getHTMLBody(); + if ($html_body !== null) { + $parameters['HtmlBody'] = $html_body; + } + + $attachments = $message->getAttachments(); + if ($attachments) { + $files = array(); + foreach ($attachments as $attachment) { + $files[] = array( + 'Name' => $attachment->getFilename(), + 'ContentType' => $attachment->getMimeType(), + 'Content' => base64_encode($attachment->getData()), + ); + } + $parameters['Attachments'] = $files; + } + + id(new PhutilPostmarkFuture()) + ->setAccessToken($access_token) + ->setMethod('email', $parameters) + ->setTimeout(60) + ->resolve(); + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php similarity index 97% rename from src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php index eb451adfc9..ea55b16159 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php @@ -3,8 +3,8 @@ /** * Mail adapter that uses SendGrid's web API to deliver email. */ -final class PhabricatorMailImplementationSendGridAdapter - extends PhabricatorMailImplementationAdapter { +final class PhabricatorMailSendGridAdapter + extends PhabricatorMailAdapter { const ADAPTERTYPE = 'sendgrid'; diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php similarity index 96% rename from src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailTestAdapter.php index 61b9bdfb4f..31f61ad5a3 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php @@ -4,8 +4,8 @@ * Mail adapter that doesn't actually send any email, for writing unit tests * against. */ -final class PhabricatorMailImplementationTestAdapter - extends PhabricatorMailImplementationAdapter { +final class PhabricatorMailTestAdapter + extends PhabricatorMailAdapter { const ADAPTERTYPE = 'test'; diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php index 91f656cf97..8de908e4ea 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php @@ -21,7 +21,7 @@ final class PhabricatorMetaMTAMailgunReceiveController array( 'inbound' => true, 'types' => array( - PhabricatorMailImplementationMailgunAdapter::ADAPTERTYPE, + PhabricatorMailMailgunAdapter::ADAPTERTYPE, ), )); foreach ($mailers as $mailer) { diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php index 550a1366f2..a5cc533738 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php @@ -16,7 +16,7 @@ final class PhabricatorMetaMTAPostmarkReceiveController array( 'inbound' => true, 'types' => array( - PhabricatorMailImplementationPostmarkAdapter::ADAPTERTYPE, + PhabricatorMailPostmarkAdapter::ADAPTERTYPE, ), )); if (!$mailers) { diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php index 9ec32f60a1..28ebdbe0ca 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php @@ -15,7 +15,7 @@ final class PhabricatorMetaMTASendGridReceiveController array( 'inbound' => true, 'types' => array( - PhabricatorMailImplementationSendGridAdapter::ADAPTERTYPE, + PhabricatorMailSendGridAdapter::ADAPTERTYPE, ), )); if (!$mailers) { diff --git a/src/applications/metamta/engine/PhabricatorMailMessageEngine.php b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php index 752096b146..c65346bf58 100644 --- a/src/applications/metamta/engine/PhabricatorMailMessageEngine.php +++ b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php @@ -8,8 +8,7 @@ abstract class PhabricatorMailMessageEngine private $actors = array(); private $preferences; - final public function setMailer( - PhabricatorMailImplementationAdapter $mailer) { + final public function setMailer(PhabricatorMailAdapter $mailer) { $this->mailer = $mailer; return $this; diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 078fe00a7a..70d94ccb0c 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -547,13 +547,14 @@ final class PhabricatorMetaMTAMail 'types' => 'optional list', 'inbound' => 'optional bool', 'outbound' => 'optional bool', + 'media' => 'optional list', )); $mailers = array(); $config = PhabricatorEnv::getEnvConfig('cluster.mailers'); - $adapters = PhabricatorMailImplementationAdapter::getAllAdapters(); + $adapters = PhabricatorMailAdapter::getAllAdapters(); $next_priority = -1; foreach ($config as $spec) { @@ -583,6 +584,11 @@ final class PhabricatorMetaMTAMail $mailer->setSupportsInbound(idx($spec, 'inbound', true)); $mailer->setSupportsOutbound(idx($spec, 'outbound', true)); + $media = idx($spec, 'media'); + if ($media !== null) { + $mailer->setMedia($media); + } + $mailers[] = $mailer; } @@ -618,6 +624,24 @@ final class PhabricatorMetaMTAMail } } + // Select only the mailers which can transmit messages with requested media + // types. + if (!empty($constraints['media'])) { + foreach ($mailers as $key => $mailer) { + $supports_any = false; + foreach ($constraints['media'] as $medium) { + if ($mailer->supportsMessageType($medium)) { + $supports_any = true; + break; + } + } + + if (!$supports_any) { + unset($mailers[$key]); + } + } + } + $sorted = array(); $groups = mgroup($mailers, 'getPriority'); krsort($groups); diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php index d20a28fc15..904dd366cb 100644 --- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php +++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php @@ -17,7 +17,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, @@ -28,7 +28,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mailer->setFailTemporarily(true); try { $mail->sendWithMailers(array($mailer)); @@ -44,7 +44,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mailer->setFailPermanently(true); try { $mail->sendWithMailers(array($mailer)); @@ -60,7 +60,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $user = $this->generateNewTestUser(); $phid = $user->getPHID(); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mail = new PhabricatorMetaMTAMail(); $mail->addTos(array($phid)); @@ -182,7 +182,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $supports_message_id, $is_first_mail) { - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mailer->prepareForSend( array( @@ -261,10 +261,10 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $status_queue = PhabricatorMailOutboundStatus::STATUS_QUEUE; $status_fail = PhabricatorMailOutboundStatus::STATUS_FAIL; - $mailer1 = id(new PhabricatorMailImplementationTestAdapter()) + $mailer1 = id(new PhabricatorMailTestAdapter()) ->setKey('mailer1'); - $mailer2 = id(new PhabricatorMailImplementationTestAdapter()) + $mailer2 = id(new PhabricatorMailTestAdapter()) ->setKey('mailer2'); $mailers = array( @@ -350,7 +350,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { ->setBody($string_1kb) ->setHTMLBody($html_1kb); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, @@ -370,7 +370,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { ->setBody($string_1mb) ->setHTMLBody($html_1mb); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, @@ -398,7 +398,7 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { ->setBody($string_1kb) ->setHTMLBody($html_1mb); - $mailer = new PhabricatorMailImplementationTestAdapter(); + $mailer = new PhabricatorMailTestAdapter(); $mail->sendWithMailers(array($mailer)); $this->assertEqual( PhabricatorMailOutboundStatus::STATUS_SENT, diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index d593300b28..e2de59c2bc 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -85,12 +85,18 @@ The supported keys for each mailer are: used to receive inbound mail. - `outbound`: Optional bool. Use `false` to prevent this mailer from being used to send outbound mail. + - `media`: Optional list. Some mailers support delivering multiple + types of messages (like Email and SMS). If you want to configure a mailer + to support only a subset of possible message types, list only those message + types. Normally, you do not need to configure this. See below for a list + of media types. The `type` field can be used to select these third-party mailers: - `mailgun`: Use Mailgun. - `ses`: Use Amazon SES. - `sendgrid`: Use Sendgrid. + - `postmark`: Use Postmark. It also supports these local mailers: @@ -99,7 +105,11 @@ It also supports these local mailers: - `test`: Internal mailer for testing. Does not send mail. You can also write your own mailer by extending -`PhabricatorMailImplementationAdapter`. +`PhabricatorMailAdapter`. + +The `media` field supports these values: + + - `email`: Configure this mailer for email. Once you've selected a mailer, find the corresponding section below for instructions on configuring it. @@ -171,11 +181,13 @@ The option accepts a list of CIDR ranges, like `1.2.3.4/16` (IPv4) or ```lang=json [ - "50.31.156.6/32" + "50.31.156.6/32", + "50.31.156.77/32", + "18.217.206.57/32" ] ``` -The default address ranges were last updated in February 2018, and were +The default address ranges were last updated in January 2019, and were documented at: diff --git a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php index 387014289d..d23ee11d8b 100644 --- a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php +++ b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php @@ -31,7 +31,7 @@ final class PhabricatorClusterMailersConfigType } } - $adapters = PhabricatorMailImplementationAdapter::getAllAdapters(); + $adapters = PhabricatorMailAdapter::getAllAdapters(); $map = array(); foreach ($value as $index => $spec) { From bc97a7d7556ab3a7dfeda1a3f300aaa4e8919361 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 4 Jan 2019 12:23:52 -0800 Subject: [PATCH 10/35] Update Mail test adapter for the newer adapter API and make all tests pass Summary: Depends on D19956. Ref T920. Move the TestAdapter to the new API and adjust a couple of tests for the changes. Test Plan: All tests now pass. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D19957 --- .../adapter/PhabricatorMailTestAdapter.php | 193 +++++++++--------- .../PhabricatorMailReceiverTestCase.php | 4 +- .../PhabricatorMetaMTAMailTestCase.php | 26 ++- .../metamta/util/PhabricatorMailUtil.php | 4 +- 4 files changed, 123 insertions(+), 104 deletions(-) diff --git a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php index 31f61ad5a3..f0840ba7bf 100644 --- a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php @@ -10,114 +10,124 @@ final class PhabricatorMailTestAdapter const ADAPTERTYPE = 'test'; private $guts = array(); - private $config = array(); + + private $supportsMessageID; + private $failPermanently; + private $failTemporarily; + + public function setSupportsMessageID($support) { + $this->supportsMessageID = $support; + return $this; + } + + public function setFailPermanently($fail) { + $this->failPermanently = true; + return $this; + } + + public function setFailTemporarily($fail) { + $this->failTemporarily = true; + return $this; + } + + public function getSupportedMessageTypes() { + return array( + PhabricatorMailEmailMessage::MESSAGETYPE, + ); + } protected function validateOptions(array $options) { - PhutilTypeSpec::checkMap( - $options, - array()); + PhutilTypeSpec::checkMap($options, array()); } public function newDefaultOptions() { return array(); } - public function prepareForSend(array $config = array()) { - $this->config = $config; - } - - public function setFrom($email, $name = '') { - $this->guts['from'] = $email; - $this->guts['from-name'] = $name; - return $this; - } - - public function addReplyTo($email, $name = '') { - if (empty($this->guts['reply-to'])) { - $this->guts['reply-to'] = array(); - } - $this->guts['reply-to'][] = array( - 'email' => $email, - 'name' => $name, - ); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->guts['tos'][] = $email; - } - return $this; - } - - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->guts['ccs'][] = $email; - } - return $this; - } - - public function addAttachment($data, $filename, $mimetype) { - $this->guts['attachments'][] = array( - 'data' => $data, - 'filename' => $filename, - 'mimetype' => $mimetype, - ); - return $this; - } - - public function addHeader($header_name, $header_value) { - $this->guts['headers'][] = array($header_name, $header_value); - return $this; - } - - public function setBody($body) { - $this->guts['body'] = $body; - return $this; - } - - public function setHTMLBody($html_body) { - $this->guts['html-body'] = $html_body; - return $this; - } - - public function setSubject($subject) { - $this->guts['subject'] = $subject; - return $this; - } - public function supportsMessageIDHeader() { - return idx($this->config, 'supportsMessageIDHeader', true); - } - - public function send() { - if (!empty($this->guts['fail-permanently'])) { - throw new PhabricatorMetaMTAPermanentFailureException( - pht('Unit Test (Permanent)')); - } - - if (!empty($this->guts['fail-temporarily'])) { - throw new Exception( - pht('Unit Test (Temporary)')); - } - - $this->guts['did-send'] = true; - return true; + return $this->supportsMessageID; } public function getGuts() { return $this->guts; } - public function setFailPermanently($fail) { - $this->guts['fail-permanently'] = $fail; - return $this; + public function sendMessage(PhabricatorMailExternalMessage $message) { + if ($this->failPermanently) { + throw new PhabricatorMetaMTAPermanentFailureException( + pht('Unit Test (Permanent)')); + } + + if ($this->failTemporarily) { + throw new Exception( + pht('Unit Test (Temporary)')); + } + + $guts = array(); + + $from = $message->getFromAddress(); + $guts['from'] = (string)$from; + + $reply_to = $message->getReplyToAddress(); + if ($reply_to) { + $guts['reply-to'] = (string)$reply_to; + } + + $to_addresses = $message->getToAddresses(); + $to = array(); + foreach ($to_addresses as $address) { + $to[] = (string)$address; + } + $guts['tos'] = $to; + + $cc_addresses = $message->getCCAddresses(); + $cc = array(); + foreach ($cc_addresses as $address) { + $cc[] = (string)$address; + } + $guts['ccs'] = $cc; + + $subject = $message->getSubject(); + if (strlen($subject)) { + $guts['subject'] = $subject; + } + + $headers = $message->getHeaders(); + $header_list = array(); + foreach ($headers as $header) { + $header_list[] = array( + $header->getName(), + $header->getValue(), + ); + } + $guts['headers'] = $header_list; + + $text_body = $message->getTextBody(); + if (strlen($text_body)) { + $guts['body'] = $text_body; + } + + $html_body = $message->getHTMLBody(); + if (strlen($html_body)) { + $guts['html-body'] = $html_body; + } + + $attachments = $message->getAttachments(); + $file_list = array(); + foreach ($attachments as $attachment) { + $file_list[] = array( + 'data' => $attachment->getData(), + 'filename' => $attachment->getFilename(), + 'mimetype' => $attachment->getMimeType(), + ); + } + $guts['attachments'] = $file_list; + + $guts['did-send'] = true; + + $this->guts = $guts; } - public function setFailTemporarily($fail) { - $this->guts['fail-temporarily'] = $fail; - return $this; - } public function getBody() { return idx($this->guts, 'body'); @@ -127,4 +137,5 @@ final class PhabricatorMailTestAdapter return idx($this->guts, 'html-body'); } + } diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php index fb38601717..391acb2285 100644 --- a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php +++ b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php @@ -42,10 +42,10 @@ final class PhabricatorMailReceiverTestCase extends PhabricatorTestCase { } public function testReservedAddresses() { - $default_address = id(new PhabricatorMetaMTAMail()) + $default_address = id(new PhabricatorMailEmailEngine()) ->newDefaultEmailAddress(); - $void_address = id(new PhabricatorMetaMTAMail()) + $void_address = id(new PhabricatorMailEmailEngine()) ->newVoidEmailAddress(); $map = array( diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php index 904dd366cb..7462aaf558 100644 --- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php +++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php @@ -182,21 +182,29 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $supports_message_id, $is_first_mail) { + $user = $this->generateNewTestUser(); + $phid = $user->getPHID(); + $mailer = new PhabricatorMailTestAdapter(); - $mailer->prepareForSend( - array( - 'supportsMessageIDHeader' => $supports_message_id, - )); + $mailer->setSupportsMessageID($supports_message_id); - $thread_id = ''; + $thread_id = 'somethread-12345'; - $mail = new PhabricatorMetaMTAMail(); - $mail->setThreadID($thread_id, $is_first_mail); - $mail->sendWithMailers(array($mailer)); + $mail = id(new PhabricatorMetaMTAMail()) + ->setThreadID($thread_id, $is_first_mail) + ->addTos(array($phid)) + ->sendWithMailers(array($mailer)); $guts = $mailer->getGuts(); - $dict = ipull($guts['headers'], 1, 0); + + $headers = idx($guts, 'headers', array()); + + $dict = array(); + foreach ($headers as $header) { + list($name, $value) = $header; + $dict[$name] = $value; + } if ($is_first_mail && $supports_message_id) { $expect_message_id = true; diff --git a/src/applications/metamta/util/PhabricatorMailUtil.php b/src/applications/metamta/util/PhabricatorMailUtil.php index 17539a0add..672f80f666 100644 --- a/src/applications/metamta/util/PhabricatorMailUtil.php +++ b/src/applications/metamta/util/PhabricatorMailUtil.php @@ -93,13 +93,13 @@ final class PhabricatorMailUtil return true; } - $default_address = id(new PhabricatorMetaMTAMail()) + $default_address = id(new PhabricatorMailEmailEngine()) ->newDefaultEmailAddress(); if (self::matchAddresses($address, $default_address)) { return true; } - $void_address = id(new PhabricatorMetaMTAMail()) + $void_address = id(new PhabricatorMailEmailEngine()) ->newVoidEmailAddress(); if (self::matchAddresses($address, $void_address)) { return true; From d7da3560ec56fa45a2e39f4275414be220f60811 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 5 Jan 2019 04:53:49 -0800 Subject: [PATCH 11/35] Update Mailgun adapter for the new mail adapter API Summary: Ref T920. Ref T5969. Update the Mailgun adapter for the API changes and add a timeout. Test Plan: Configured Mailgun as a mailer, sent mail with subject/to/cc/headers/html/attachments using `bin/mail send-test`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5969, T920 Differential Revision: https://secure.phabricator.com/D19959 --- .../adapter/PhabricatorMailMailgunAdapter.php | 150 ++++++++---------- 1 file changed, 62 insertions(+), 88 deletions(-) diff --git a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php index 5a295812eb..9eb478efc5 100644 --- a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php @@ -8,65 +8,10 @@ final class PhabricatorMailMailgunAdapter const ADAPTERTYPE = 'mailgun'; - private $params = array(); - private $attachments = array(); - - public function setFrom($email, $name = '') { - $this->params['from'] = $email; - $this->params['from-name'] = $name; - return $this; - } - - public function addReplyTo($email, $name = '') { - if (empty($this->params['reply-to'])) { - $this->params['reply-to'] = array(); - } - $this->params['reply-to'][] = $this->renderAddress($email, $name); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->params['tos'][] = $email; - } - return $this; - } - - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->params['ccs'][] = $email; - } - return $this; - } - - public function addAttachment($data, $filename, $mimetype) { - $this->attachments[] = array( - 'data' => $data, - 'name' => $filename, - 'type' => $mimetype, + public function getSupportedMessageTypes() { + return array( + PhabricatorMailEmailMessage::MESSAGETYPE, ); - - return $this; - } - - public function addHeader($header_name, $header_value) { - $this->params['headers'][] = array($header_name, $header_value); - return $this; - } - - public function setBody($body) { - $this->params['body'] = $body; - return $this; - } - - public function setHTMLBody($html_body) { - $this->params['html-body'] = $html_body; - return $this; - } - - public function setSubject($subject) { - $this->params['subject'] = $subject; - return $this; } public function supportsMessageIDHeader() { @@ -89,48 +34,79 @@ final class PhabricatorMailMailgunAdapter ); } - public function send() { - $key = $this->getOption('api-key'); + public function sendMessage(PhabricatorMailExternalMessage $message) { + $api_key = $this->getOption('api-key'); $domain = $this->getOption('domain'); $params = array(); - $params['to'] = implode(', ', idx($this->params, 'tos', array())); - $params['subject'] = idx($this->params, 'subject'); - $params['text'] = idx($this->params, 'body'); - - if (idx($this->params, 'html-body')) { - $params['html'] = idx($this->params, 'html-body'); + $subject = $message->getSubject(); + if ($subject !== null) { + $params['subject'] = $subject; } - $from = idx($this->params, 'from'); - $from_name = idx($this->params, 'from-name'); - $params['from'] = $this->renderAddress($from, $from_name); - - if (idx($this->params, 'reply-to')) { - $replyto = $this->params['reply-to']; - $params['h:reply-to'] = implode(', ', $replyto); + $from_address = $message->getFromAddress(); + if ($from_address) { + $params['from'] = (string)$from_address; } - if (idx($this->params, 'ccs')) { - $params['cc'] = implode(', ', $this->params['ccs']); + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + $to = array(); + foreach ($to_addresses as $address) { + $to[] = (string)$address; + } + $params['to'] = implode(', ', $to); } - foreach (idx($this->params, 'headers', array()) as $header) { - list($name, $value) = $header; - $params['h:'.$name] = $value; + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + $cc = array(); + foreach ($cc_addresses as $address) { + $cc[] = (string)$address; + } + $params['cc'] = implode(', ', $cc); } - $future = new HTTPSFuture( - "https://api:{$key}@api.mailgun.net/v2/{$domain}/messages", - $params); - $future->setMethod('POST'); + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $params['h:reply-to'] = (string)$reply_address; + } - foreach ($this->attachments as $attachment) { + $headers = $message->getHeaders(); + if ($headers) { + foreach ($headers as $header) { + $name = $header->getName(); + $value = $header->getValue(); + $params['h:'.$name] = $value; + } + } + + $text_body = $message->getTextBody(); + if ($text_body !== null) { + $params['text'] = $text_body; + } + + $html_body = $message->getHTMLBody(); + if ($html_body !== null) { + $params['html'] = $html_body; + } + + $mailgun_uri = urisprintf( + 'https://api.mailgun.net/v2/%s/messages', + $domain); + + $future = id(new HTTPSFuture($mailgun_uri, $params)) + ->setMethod('POST') + ->setHTTPBasicAuthCredentials('api', new PhutilOpaqueEnvelope($api_key)) + ->setTimeout(60); + + $attachments = $message->getAttachments(); + foreach ($attachments as $attachment) { $future->attachFileData( 'attachment', - $attachment['data'], - $attachment['name'], - $attachment['type']); + $attachment->getData(), + $attachment->getFilename(), + $attachment->getMimeType()); } list($body) = $future->resolvex(); @@ -151,8 +127,6 @@ final class PhabricatorMailMailgunAdapter 'Request failed with errors: %s.', $message)); } - - return true; } } From 64e3296fe682b7adda8681d90a7f9cc210b92207 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 5 Jan 2019 06:30:22 -0800 Subject: [PATCH 12/35] Upgrade Sendgrid to the modern mailer API; removes "api-user" option Summary: Ref T920. Ref T5969. - Update to the new "$message" API. - Update to Sendgrid v3. - Add a timeout. - This removes the "api-user" option, which Sendgrid no longer seems to use. Test Plan: Sent Sendgrid messages with `bin/mail send-test ...` using subject/headers/attachments/html/to/cc. Reviewers: amckinley Reviewed By: amckinley Subscribers: jbrownEP Maniphest Tasks: T5969, T920 Differential Revision: https://secure.phabricator.com/D19960 --- .../PhabricatorMailSendGridAdapter.php | 217 ++++++++---------- 1 file changed, 94 insertions(+), 123 deletions(-) diff --git a/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php index ea55b16159..133e82b628 100644 --- a/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php @@ -8,166 +8,137 @@ final class PhabricatorMailSendGridAdapter const ADAPTERTYPE = 'sendgrid'; - private $params = array(); + public function getSupportedMessageTypes() { + return array( + PhabricatorMailEmailMessage::MESSAGETYPE, + ); + } protected function validateOptions(array $options) { PhutilTypeSpec::checkMap( $options, array( - 'api-user' => 'string', 'api-key' => 'string', )); } public function newDefaultOptions() { return array( - 'api-user' => null, 'api-key' => null, ); } - public function setFrom($email, $name = '') { - $this->params['from'] = $email; - $this->params['from-name'] = $name; - return $this; - } - - public function addReplyTo($email, $name = '') { - if (empty($this->params['reply-to'])) { - $this->params['reply-to'] = array(); - } - $this->params['reply-to'][] = array( - 'email' => $email, - 'name' => $name, - ); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->params['tos'][] = $email; - } - return $this; - } - - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->params['ccs'][] = $email; - } - return $this; - } - - public function addAttachment($data, $filename, $mimetype) { - if (empty($this->params['files'])) { - $this->params['files'] = array(); - } - $this->params['files'][$filename] = $data; - } - - public function addHeader($header_name, $header_value) { - $this->params['headers'][] = array($header_name, $header_value); - return $this; - } - - public function setBody($body) { - $this->params['body'] = $body; - return $this; - } - - public function setHTMLBody($body) { - $this->params['html-body'] = $body; - return $this; - } - - - public function setSubject($subject) { - $this->params['subject'] = $subject; - return $this; - } - - public function supportsMessageIDHeader() { - return false; - } - - public function send() { - $user = $this->getOption('api-user'); + public function sendMessage(PhabricatorMailExternalMessage $message) { $key = $this->getOption('api-key'); - $params = array(); + $parameters = array(); - $ii = 0; - foreach (idx($this->params, 'tos', array()) as $to) { - $params['to['.($ii++).']'] = $to; + $subject = $message->getSubject(); + if ($subject !== null) { + $parameters['subject'] = $subject; } - $params['subject'] = idx($this->params, 'subject'); - $params['text'] = idx($this->params, 'body'); + $personalizations = array(); - if (idx($this->params, 'html-body')) { - $params['html'] = idx($this->params, 'html-body'); + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + $personalizations['to'] = array(); + foreach ($to_addresses as $address) { + $personalizations['to'][] = $this->newPersonalization($address); + } } - $params['from'] = idx($this->params, 'from'); - if (idx($this->params, 'from-name')) { - $params['fromname'] = $this->params['from-name']; + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + $personalizations['cc'] = array(); + foreach ($cc_addresses as $address) { + $personalizations['cc'][] = $this->newPersonalization($address); + } } - if (idx($this->params, 'reply-to')) { - $replyto = $this->params['reply-to']; + // This is a list of different sets of recipients who should receive copies + // of the mail. We handle "one message to each recipient" ourselves. + $parameters['personalizations'] = array( + $personalizations, + ); - // Pick off the email part, no support for the name part in this API. - $params['replyto'] = $replyto[0]['email']; + $from_address = $message->getFromAddress(); + if ($from_address) { + $parameters['from'] = $this->newPersonalization($from_address); } - foreach (idx($this->params, 'files', array()) as $name => $data) { - $params['files['.$name.']'] = $data; - } - - $headers = idx($this->params, 'headers', array()); - - // See SendGrid Support Ticket #29390; there's no explicit REST API support - // for CC right now but it works if you add a generic "Cc" header. - // - // SendGrid said this is supported: - // "You can use CC as you are trying to do there [by adding a generic - // header]. It is supported despite our limited documentation to this - // effect, I am glad you were able to figure it out regardless. ..." - if (idx($this->params, 'ccs')) { - $headers[] = array('Cc', implode(', ', $this->params['ccs'])); + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $parameters['reply_to'] = $this->newPersonalization($reply_address); } + $headers = $message->getHeaders(); if ($headers) { - // Convert to dictionary. - $headers = ipull($headers, 1, 0); - $headers = json_encode($headers); - $params['headers'] = $headers; + $map = array(); + foreach ($headers as $header) { + $map[$header->getName()] = $header->getValue(); + } + $parameters['headers'] = $map; } - $params['api_user'] = $user; - $params['api_key'] = $key; - - $future = new HTTPSFuture( - 'https://sendgrid.com/api/mail.send.json', - $params); - $future->setMethod('POST'); - - list($body) = $future->resolvex(); - - $response = null; - try { - $response = phutil_json_decode($body); - } catch (PhutilJSONParserException $ex) { - throw new PhutilProxyException( - pht('Failed to JSON decode response.'), - $ex); + $content = array(); + $text_body = $message->getTextBody(); + if ($text_body !== null) { + $content[] = array( + 'type' => 'text/plain', + 'value' => $text_body, + ); } - if ($response['message'] !== 'success') { - $errors = implode(';', $response['errors']); - throw new Exception(pht('Request failed with errors: %s.', $errors)); + $html_body = $message->getHTMLBody(); + if ($html_body !== null) { + $content[] = array( + 'type' => 'text/html', + 'value' => $html_body, + ); + } + $parameters['content'] = $content; + + $attachments = $message->getAttachments(); + if ($attachments) { + $files = array(); + foreach ($attachments as $attachment) { + $files[] = array( + 'content' => base64_encode($attachment->getData()), + 'type' => $attachment->getMimeType(), + 'filename' => $attachment->getFilename(), + 'disposition' => 'attachment', + ); + } + $parameters['attachments'] = $files; } - return true; + $sendgrid_uri = 'https://api.sendgrid.com/v3/mail/send'; + $json_parameters = phutil_json_encode($parameters); + + id(new HTTPSFuture($sendgrid_uri)) + ->setMethod('POST') + ->addHeader('Authorization', "Bearer {$key}") + ->addHeader('Content-Type', 'application/json') + ->setData($json_parameters) + ->setTimeout(60) + ->resolvex(); + + // The SendGrid v3 API does not return a JSON response body. We get a + // non-2XX HTTP response in the case of an error, which throws above. + } + + private function newPersonalization(PhutilEmailAddress $address) { + $result = array( + 'email' => $address->getAddress(), + ); + + $display_name = $address->getDisplayName(); + if ($display_name) { + $result['name'] = $display_name; + } + + return $result; } } From 966a93334c857b7228d2923f1abcac1f887b52f5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 05:51:59 -0800 Subject: [PATCH 13/35] Don't require "CAN_EDIT" to watch/unwatch a project Summary: See T1024. When "CAN_EDIT" became default in T13186, this was missed as an exception. Watching shouldn't require "CAN_EDIT", so exempt it. Test Plan: - Before change: tried to watch a project I could not edit, got a policy error. - After change: watched/unwatched a project I could not edit. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19977 --- .../editor/PhabricatorApplicationTransactionEditor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 854c361614..cd5125d3b3 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1677,6 +1677,9 @@ abstract class PhabricatorApplicationTransactionEditor // You need CAN_EDIT to change members other than yourself. return PhabricatorPolicyCapability::CAN_EDIT; + case PhabricatorObjectHasWatcherEdgeType::EDGECONST: + // See PHI1024. Watching a project does not require CAN_EDIT. + return null; default: return PhabricatorPolicyCapability::CAN_EDIT; } From 43a6f34e7f2bd69ebf01bcda309cdc570c01629d Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 5 Jan 2019 06:57:06 -0800 Subject: [PATCH 14/35] Update the SMTP (PHPMailer) adapter for the new mail API; remove "encoding" and "mailer" Summary: Ref T920. Ref T12404. - Update to the new "$message" API. - Remove "encoding". I believe "base64" is always the best value for this since we stopped seeing issues once we changed the default. - Remove "mailer". This is a legacy option that makes little sense given how configuration now works. - Rename to "SMTP". This doesn't affect users anymore since this mailer has been configured as `smtp` for about a year. - This does NOT add a timeout since the SMTP code is inside PHPMailer (see T12404). Test Plan: Sent messages with many mail features via GMail SMTP and SendGrid SMTP. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12404, T920 Differential Revision: https://secure.phabricator.com/D19961 --- src/__phutil_library_map__.php | 10 +- .../PhabricatorMailAmazonSESAdapter.php | 2 +- .../PhabricatorMailPHPMailerAdapter.php | 150 ----------------- .../adapter/PhabricatorMailSMTPAdapter.php | 154 ++++++++++++++++++ ...php => PhabricatorMailSendmailAdapter.php} | 2 +- 5 files changed, 161 insertions(+), 157 deletions(-) delete mode 100644 src/applications/metamta/adapter/PhabricatorMailPHPMailerAdapter.php create mode 100644 src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php rename src/applications/metamta/adapter/{PhabricatorMailPHPMailerLiteAdapter.php => PhabricatorMailSendmailAdapter.php} (98%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fb6cfed1fa..49d2500b21 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3417,15 +3417,15 @@ phutil_register_library_map(array( 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php', 'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php', - 'PhabricatorMailPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailPHPMailerAdapter.php', - 'PhabricatorMailPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailPHPMailerLiteAdapter.php', 'PhabricatorMailPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php', 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', + 'PhabricatorMailSMTPAdapter' => 'applications/metamta/adapter/PhabricatorMailSMTPAdapter.php', 'PhabricatorMailSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailSendGridAdapter.php', + 'PhabricatorMailSendmailAdapter' => 'applications/metamta/adapter/PhabricatorMailSendmailAdapter.php', 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', @@ -9222,7 +9222,7 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailAdapter' => 'Phobject', - 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailPHPMailerLiteAdapter', + 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailSendmailAdapter', 'PhabricatorMailAttachment' => 'Phobject', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', @@ -9251,15 +9251,15 @@ phutil_register_library_map(array( 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundStatus' => 'Phobject', - 'PhabricatorMailPHPMailerAdapter' => 'PhabricatorMailAdapter', - 'PhabricatorMailPHPMailerLiteAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailPostmarkAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorMailReceiver' => 'Phobject', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', + 'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailSendmailAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php index 31168293a9..5505f5de86 100644 --- a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php @@ -1,7 +1,7 @@ 'string|null', - 'port' => 'int', - 'user' => 'string|null', - 'password' => 'string|null', - 'protocol' => 'string|null', - 'encoding' => 'string', - 'mailer' => 'string', - )); - } - - public function newDefaultOptions() { - return array( - 'host' => null, - 'port' => 25, - 'user' => null, - 'password' => null, - 'protocol' => null, - 'encoding' => 'base64', - 'mailer' => 'smtp', - ); - } - - /** - * @phutil-external-symbol class PHPMailer - */ - public function prepareForSend() { - $root = phutil_get_library_root('phabricator'); - $root = dirname($root); - require_once $root.'/externals/phpmailer/class.phpmailer.php'; - $this->mailer = new PHPMailer($use_exceptions = true); - $this->mailer->CharSet = 'utf-8'; - - $encoding = $this->getOption('encoding'); - $this->mailer->Encoding = $encoding; - - // By default, PHPMailer sends one mail per recipient. We handle - // combining or separating To and Cc higher in the stack, so tell it to - // send mail exactly like we ask. - $this->mailer->SingleTo = false; - - $mailer = $this->getOption('mailer'); - if ($mailer == 'smtp') { - $this->mailer->IsSMTP(); - $this->mailer->Host = $this->getOption('host'); - $this->mailer->Port = $this->getOption('port'); - $user = $this->getOption('user'); - if ($user) { - $this->mailer->SMTPAuth = true; - $this->mailer->Username = $user; - $this->mailer->Password = $this->getOption('password'); - } - - $protocol = $this->getOption('protocol'); - if ($protocol) { - $protocol = phutil_utf8_strtolower($protocol); - $this->mailer->SMTPSecure = $protocol; - } - } else if ($mailer == 'sendmail') { - $this->mailer->IsSendmail(); - } else { - // Do nothing, by default PHPMailer send message using PHP mail() - // function. - } - } - - public function supportsMessageIDHeader() { - return true; - } - - public function setFrom($email, $name = '') { - $this->mailer->SetFrom($email, $name, $crazy_side_effects = false); - return $this; - } - - public function addReplyTo($email, $name = '') { - $this->mailer->AddReplyTo($email, $name); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->mailer->AddAddress($email); - } - return $this; - } - - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->mailer->AddCC($email); - } - return $this; - } - - public function addAttachment($data, $filename, $mimetype) { - $this->mailer->AddStringAttachment( - $data, - $filename, - 'base64', - $mimetype); - return $this; - } - - public function addHeader($header_name, $header_value) { - if (strtolower($header_name) == 'message-id') { - $this->mailer->MessageID = $header_value; - } else { - $this->mailer->AddCustomHeader($header_name.': '.$header_value); - } - return $this; - } - - public function setBody($body) { - $this->mailer->IsHTML(false); - $this->mailer->Body = $body; - return $this; - } - - public function setHTMLBody($html_body) { - $this->mailer->IsHTML(true); - $this->mailer->Body = $html_body; - return $this; - } - - public function setSubject($subject) { - $this->mailer->Subject = $subject; - return $this; - } - - public function hasValidRecipients() { - return true; - } - - public function send() { - return $this->mailer->Send(); - } - -} diff --git a/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php new file mode 100644 index 0000000000..a3c6298279 --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php @@ -0,0 +1,154 @@ + 'string|null', + 'port' => 'int', + 'user' => 'string|null', + 'password' => 'string|null', + 'protocol' => 'string|null', + )); + } + + public function newDefaultOptions() { + return array( + 'host' => null, + 'port' => 25, + 'user' => null, + 'password' => null, + 'protocol' => null, + ); + } + + /** + * @phutil-external-symbol class PHPMailer + */ + public function sendMessage(PhabricatorMailExternalMessage $message) { + $root = phutil_get_library_root('phabricator'); + $root = dirname($root); + require_once $root.'/externals/phpmailer/class.phpmailer.php'; + $smtp = new PHPMailer($use_exceptions = true); + + $smtp->CharSet = 'utf-8'; + $smtp->Encoding = 'base64'; + + // By default, PHPMailer sends one mail per recipient. We handle + // combining or separating To and Cc higher in the stack, so tell it to + // send mail exactly like we ask. + $smtp->SingleTo = false; + + $smtp->IsSMTP(); + $smtp->Host = $this->getOption('host'); + $smtp->Port = $this->getOption('port'); + $user = $this->getOption('user'); + if (strlen($user)) { + $smtp->SMTPAuth = true; + $smtp->Username = $user; + $smtp->Password = $this->getOption('password'); + } + + $protocol = $this->getOption('protocol'); + if ($protocol) { + $protocol = phutil_utf8_strtolower($protocol); + $smtp->SMTPSecure = $protocol; + } + + $subject = $message->getSubject(); + if ($subject !== null) { + $smtp->Subject = $subject; + } + + $from_address = $message->getFromAddress(); + if ($from_address) { + $smtp->SetFrom( + $from_address->getAddress(), + (string)$from_address->getDisplayName(), + $crazy_side_effects = false); + } + + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $smtp->AddReplyTo( + $reply_address->getAddress(), + (string)$reply_address->getDisplayName()); + } + + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + foreach ($to_addresses as $address) { + $smtp->AddAddress( + $address->getAddress(), + (string)$address->getDisplayName()); + } + } + + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + foreach ($cc_addresses as $address) { + $smtp->AddCC( + $address->getAddress(), + (string)$address->getDisplayName()); + } + } + + $headers = $message->getHeaders(); + if ($headers) { + $list = array(); + foreach ($headers as $header) { + $name = $header->getName(); + $value = $header->getValue(); + + if (phutil_utf8_strtolower($name) === 'message-id') { + $smtp->MessageID = $value; + } else { + $smtp->AddCustomHeader("{$name}: {$value}"); + } + } + } + + $text_body = $message->getTextBody(); + if ($text_body !== null) { + $smtp->Body = $text_body; + } + + $html_body = $message->getHTMLBody(); + if ($html_body !== null) { + $smtp->IsHTML(true); + $smtp->Body = $html_body; + if ($text_body !== null) { + $smtp->AltBody = $text_body; + } + } + + $attachments = $message->getAttachments(); + if ($attachments) { + foreach ($attachments as $attachment) { + $smtp->AddStringAttachment( + $attachment->getData(), + $attachment->getFilename(), + 'base64', + $attachment->getMimeType()); + } + } + + $smtp->Send(); + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php similarity index 98% rename from src/applications/metamta/adapter/PhabricatorMailPHPMailerLiteAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php index 4557adc219..920d8da99f 100644 --- a/src/applications/metamta/adapter/PhabricatorMailPHPMailerLiteAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php @@ -5,7 +5,7 @@ * * @concrete-extensible */ -class PhabricatorMailPHPMailerLiteAdapter +class PhabricatorMailSendmailAdapter extends PhabricatorMailAdapter { const ADAPTERTYPE = 'sendmail'; From c3cafffed726397174ce38c97ed26c34fa270afe Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 07:50:20 -0800 Subject: [PATCH 15/35] Update the "SES" and "sendmail" mailers for the new API; remove "encoding" Summary: Ref T13222. Ref T920. This is the last of the upstream adapter updates. Test Plan: - Sent mail with SES. - Sent mail with "sendmail". I don't have sendmail actually configured to an upstream MTA so I'm not 100% sure this worked, but the `sendmail` binary didn't complain and almost all of the code is shared with SES, so I'm reasonably confident this actually works. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T920 Differential Revision: https://secure.phabricator.com/D19965 --- externals/phpmailer/class.phpmailer-lite.php | 96 ++++++++++++++++ src/__phutil_library_map__.php | 2 +- .../adapter/PhabricatorMailAdapter.php | 10 +- .../PhabricatorMailAmazonSESAdapter.php | 34 ++++-- .../PhabricatorMailSendmailAdapter.php | 108 +++--------------- 5 files changed, 136 insertions(+), 114 deletions(-) diff --git a/externals/phpmailer/class.phpmailer-lite.php b/externals/phpmailer/class.phpmailer-lite.php index 610de99438..335625ebad 100644 --- a/externals/phpmailer/class.phpmailer-lite.php +++ b/externals/phpmailer/class.phpmailer-lite.php @@ -42,6 +42,102 @@ if (version_compare(PHP_VERSION, '5.0.0', '<') ) exit("Sorry, this version of PH class PHPMailerLite { + public static function newFromMessage( + PhabricatorMailExternalMessage $message) { + + $mailer = new self($use_exceptions = true); + + // By default, PHPMailerLite sends one mail per recipient. We handle + // combining or separating To and Cc higher in the stack, so tell it to + // send mail exactly like we ask. + $mailer->SingleTo = false; + + $mailer->CharSet = 'utf-8'; + $mailer->Encoding = 'base64'; + + $subject = $message->getSubject(); + if ($subject !== null) { + $mailer->Subject = $subject; + } + + $from_address = $message->getFromAddress(); + if ($from_address) { + $mailer->SetFrom( + $from_address->getAddress(), + (string)$from_address->getDisplayName(), + $crazy_side_effects = false); + } + + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $mailer->AddReplyTo( + $reply_address->getAddress(), + (string)$reply_address->getDisplayName()); + } + + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + foreach ($to_addresses as $address) { + $mailer->AddAddress( + $address->getAddress(), + (string)$address->getDisplayName()); + } + } + + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + foreach ($cc_addresses as $address) { + $mailer->AddCC( + $address->getAddress(), + (string)$address->getDisplayName()); + } + } + + $headers = $message->getHeaders(); + if ($headers) { + foreach ($headers as $header) { + $name = $header->getName(); + $value = $header->getValue(); + + if (phutil_utf8_strtolower($name) === 'message-id') { + $mailer->MessageID = $value; + } else { + $mailer->AddCustomHeader("{$name}: {$value}"); + } + } + } + + $attachments = $message->getAttachments(); + if ($attachments) { + foreach ($attachments as $attachment) { + $mailer->AddStringAttachment( + $attachment->getData(), + $attachment->getFilename(), + 'base64', + $attachment->getMimeType()); + } + } + + $text_body = $message->getTextBody(); + if ($text_body !== null) { + $mailer->Body = $text_body; + } + + $html_body = $message->getHTMLBody(); + if ($html_body !== null) { + $mailer->IsHTML(true); + $mailer->Body = $html_body; + if ($text_body !== null) { + $mailer->AltBody = $text_body; + } + } + + return $mailer; + } + + + + ///////////////////////////////////////////////// // PROPERTIES, PUBLIC ///////////////////////////////////////////////// diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 49d2500b21..95caf27440 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -9222,7 +9222,7 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailAdapter' => 'Phobject', - 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailSendmailAdapter', + 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailAttachment' => 'Phobject', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', diff --git a/src/applications/metamta/adapter/PhabricatorMailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php index b3833e779e..4fb262626d 100644 --- a/src/applications/metamta/adapter/PhabricatorMailAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php @@ -23,14 +23,8 @@ abstract class PhabricatorMailAdapter ->execute(); } - /* abstract */ public function getSupportedMessageTypes() { - throw new PhutilMethodNotImplementedException(); - } - - /* abstract */ public function sendMessage( - PhabricatorMailExternalMessage $message) { - throw new PhutilMethodNotImplementedException(); - } + abstract public function getSupportedMessageTypes(); + abstract public function sendMessage(PhabricatorMailExternalMessage $message); /** * Return true if this adapter supports setting a "Message-ID" when sending diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php index 5505f5de86..a289e5bc73 100644 --- a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php @@ -1,21 +1,17 @@ mailer->Mailer = 'amazon-ses'; - $this->mailer->customMailer = $this; + public function getSupportedMessageTypes() { + return array( + PhabricatorMailEmailMessage::MESSAGETYPE, + ); } public function supportsMessageIDHeader() { - // Amazon SES will ignore any Message-ID we provide. return false; } @@ -26,7 +22,6 @@ final class PhabricatorMailAmazonSESAdapter 'access-key' => 'string', 'secret-key' => 'string', 'endpoint' => 'string', - 'encoding' => 'string', )); } @@ -35,10 +30,27 @@ final class PhabricatorMailAmazonSESAdapter 'access-key' => null, 'secret-key' => null, 'endpoint' => null, - 'encoding' => 'base64', ); } + /** + * @phutil-external-symbol class PHPMailerLite + */ + public function sendMessage(PhabricatorMailExternalMessage $message) { + $root = phutil_get_library_root('phabricator'); + $root = dirname($root); + require_once $root.'/externals/phpmailer/class.phpmailer-lite.php'; + + $mailer = PHPMailerLite::newFromMessage($message); + + $mailer->Mailer = 'amazon-ses'; + $mailer->customMailer = $this; + + $mailer->Send(); + } + + + /** * @phutil-external-symbol class SimpleEmailService */ diff --git a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php index 920d8da99f..05f3c909aa 100644 --- a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php @@ -1,16 +1,20 @@ mailer = new PHPMailerLite($use_exceptions = true); - $this->mailer->CharSet = 'utf-8'; - $encoding = $this->getOption('encoding'); - $this->mailer->Encoding = $encoding; - - // By default, PHPMailerLite sends one mail per recipient. We handle - // combining or separating To and Cc higher in the stack, so tell it to - // send mail exactly like we ask. - $this->mailer->SingleTo = false; - } - - public function supportsMessageIDHeader() { - return true; - } - - public function setFrom($email, $name = '') { - $this->mailer->SetFrom($email, $name, $crazy_side_effects = false); - return $this; - } - - public function addReplyTo($email, $name = '') { - $this->mailer->AddReplyTo($email, $name); - return $this; - } - - public function addTos(array $emails) { - foreach ($emails as $email) { - $this->mailer->AddAddress($email); - } - return $this; - } - - public function addCCs(array $emails) { - foreach ($emails as $email) { - $this->mailer->AddCC($email); - } - return $this; - } - - public function addAttachment($data, $filename, $mimetype) { - $this->mailer->AddStringAttachment( - $data, - $filename, - 'base64', - $mimetype); - return $this; - } - - public function addHeader($header_name, $header_value) { - if (strtolower($header_name) == 'message-id') { - $this->mailer->MessageID = $header_value; - } else { - $this->mailer->AddCustomHeader($header_name.': '.$header_value); - } - return $this; - } - - public function setBody($body) { - $this->mailer->Body = $body; - $this->mailer->IsHTML(false); - return $this; - } - - - /** - * Note: phpmailer-lite does NOT support sending messages with mixed version - * (plaintext and html). So for now lets just use HTML if it's available. - * @param $html - */ - public function setHTMLBody($html_body) { - $this->mailer->Body = $html_body; - $this->mailer->IsHTML(true); - return $this; - } - - public function setSubject($subject) { - $this->mailer->Subject = $subject; - return $this; - } - - public function hasValidRecipients() { - return true; - } - - public function send() { - return $this->mailer->Send(); + $mailer = PHPMailerLite::newFromMessage($message); + $mailer->Send(); } } From 0c0cbb1c09e4bc5a27f1246181754ab22f13499c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 09:52:41 -0800 Subject: [PATCH 16/35] Fix an issue where transactions in mail were always rendered as text Summary: Fixes T12921. Currently, we call `getTitleForHTMLMail()`, but that calls `getTitleForMail()` which forces us into text rendering mode. Instead, have `getTitleForHTML/TextMail()` force the rendering mode, then call `getTitleForMail()` with the desired rendering mode. This causes stories like "epriestely added dependent tasks: x, y." to appear as links in email instead of plain text. Test Plan: Used `bin/mail show-outbound --id ... --dump-html > out.html` to verify HTML mail. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12921 Differential Revision: https://secure.phabricator.com/D19968 --- ...habricatorApplicationTransactionEditor.php | 4 ++-- .../PhabricatorApplicationTransaction.php | 21 +++++++++++++++++-- .../storage/PhabricatorModularTransaction.php | 9 -------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index cd5125d3b3..64f375fd88 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3265,7 +3265,7 @@ abstract class PhabricatorApplicationTransactionEditor } if (!$is_comment || !$seen_comment) { - $header = $xaction->getTitleForMail(); + $header = $xaction->getTitleForTextMail(); if ($header !== null) { $headers[] = $header; } @@ -3350,7 +3350,7 @@ abstract class PhabricatorApplicationTransactionEditor // If this is not the first comment in the mail, add the header showing // who wrote the comment immediately above the comment. if (!$is_initial) { - $header = $xaction->getTitleForMail(); + $header = $xaction->getTitleForTextMail(); if ($header !== null) { $body->addRawPlaintextSection($header); } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 725178d6ca..515fd87394 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -763,12 +763,29 @@ abstract class PhabricatorApplicationTransaction return $this->shouldHideForFeed(); } + private function getTitleForMailWithRenderingTarget($new_target) { + $old_target = $this->getRenderingTarget(); + try { + $this->setRenderingTarget($new_target); + $result = $this->getTitleForMail(); + } catch (Exception $ex) { + $this->setRenderingTarget($old_target); + throw $ex; + } + $this->setRenderingTarget($old_target); + return $result; + } + public function getTitleForMail() { - return id(clone $this)->setRenderingTarget('text')->getTitle(); + return $this->getTitle(); + } + + public function getTitleForTextMail() { + return $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT); } public function getTitleForHTMLMail() { - $title = $this->getTitleForMail(); + $title = $this->getTitleForMailWithRenderingTarget(self::TARGET_HTML); if ($title === null) { return null; } diff --git a/src/applications/transactions/storage/PhabricatorModularTransaction.php b/src/applications/transactions/storage/PhabricatorModularTransaction.php index 38b9d1835d..59466ee472 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransaction.php +++ b/src/applications/transactions/storage/PhabricatorModularTransaction.php @@ -150,15 +150,6 @@ abstract class PhabricatorModularTransaction return parent::getActionStrength(); } - public function getTitleForMail() { - $old_target = $this->getRenderingTarget(); - $new_target = self::TARGET_TEXT; - $this->setRenderingTarget($new_target); - $title = $this->getTitle(); - $this->setRenderingTarget($old_target); - return $title; - } - /* final */ public function getTitleForFeed() { $title = $this->getTransactionImplementation()->getTitleForFeed(); if ($title !== null) { From 96d3e73eed64784343667ac23e6d4e11b016dbf9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 13:31:42 -0800 Subject: [PATCH 17/35] Fix an issue where "CC"-only email improperly wiped CC addresses Summary: Ref T920. See . Test Plan: Used `bin/mail send-test --cc ...` without `--to`, got email. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D19970 --- src/applications/metamta/engine/PhabricatorMailEmailEngine.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php index 51bc1cad07..fc00ccb3bb 100644 --- a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php +++ b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php @@ -54,7 +54,6 @@ final class PhabricatorMailEmailEngine // If that also fails, move the "Cc:" line to "To:". if (!$to_addresses) { $void_address = $this->newVoidEmailAddress(); - $cc_addresses = $to_addresses; $to_addresses = array($void_address); } From 35f0e31ed3b25b5b761a12e0ac21c57420cfb8ef Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 1 Jan 2019 19:04:43 -0800 Subject: [PATCH 18/35] Add a Twilio SMS message adapter Summary: Ref T920. Adds a "phone number" object, an "SMS" message type, and Twilio glue. Test Plan: Used this test script to send myself some text messages after configuring Twilio in `cluster.mailers`. ``` '); } $to_number = $argv[1]; $text_body = $argv[2]; $mailers = PhabricatorMetaMTAMail::newMailers( array( 'outbound' => true, 'media' => array( PhabricatorMailSMSMessage::MESSAGETYPE, ), )); if (!$mailers) { return new Aphront404Response(); } $mailer = head($mailers); $message = id(new PhabricatorMailSMSMessage()) ->setToNumber(new PhabricatorPhoneNumber($to_number)) ->setTextBody($text_body); $mailer->sendMessage($message); ``` Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D19971 --- src/__phutil_library_map__.php | 6 ++ .../adapter/PhabricatorMailTwilioAdapter.php | 61 +++++++++++++++++++ .../future/PhabricatorTwilioFuture.php | 15 +++++ .../message/PhabricatorMailSMSMessage.php | 29 +++++++++ .../message/PhabricatorPhoneNumber.php | 25 ++++++++ 5 files changed, 136 insertions(+) create mode 100644 src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php create mode 100644 src/applications/metamta/message/PhabricatorMailSMSMessage.php create mode 100644 src/applications/metamta/message/PhabricatorPhoneNumber.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 95caf27440..a72434f9a7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3423,6 +3423,7 @@ phutil_register_library_map(array( 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php', + 'PhabricatorMailSMSMessage' => 'applications/metamta/message/PhabricatorMailSMSMessage.php', 'PhabricatorMailSMTPAdapter' => 'applications/metamta/adapter/PhabricatorMailSMTPAdapter.php', 'PhabricatorMailSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailSendGridAdapter.php', 'PhabricatorMailSendmailAdapter' => 'applications/metamta/adapter/PhabricatorMailSendmailAdapter.php', @@ -3430,6 +3431,7 @@ phutil_register_library_map(array( 'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 'PhabricatorMailTestAdapter' => 'applications/metamta/adapter/PhabricatorMailTestAdapter.php', + 'PhabricatorMailTwilioAdapter' => 'applications/metamta/adapter/PhabricatorMailTwilioAdapter.php', 'PhabricatorMailUtil' => 'applications/metamta/util/PhabricatorMailUtil.php', 'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php', 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', @@ -3851,6 +3853,7 @@ phutil_register_library_map(array( 'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php', 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', + 'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php', 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', 'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', @@ -9257,6 +9260,7 @@ phutil_register_library_map(array( 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', + 'PhabricatorMailSMSMessage' => 'PhabricatorMailExternalMessage', 'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailSendmailAdapter' => 'PhabricatorMailAdapter', @@ -9264,6 +9268,7 @@ phutil_register_library_map(array( 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', 'PhabricatorMailTestAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailTwilioAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailUtil' => 'Phobject', 'PhabricatorMainMenuBarExtension' => 'Phobject', 'PhabricatorMainMenuSearchView' => 'AphrontView', @@ -9768,6 +9773,7 @@ phutil_register_library_map(array( 'PhabricatorPholioApplication' => 'PhabricatorApplication', 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', + 'PhabricatorPhoneNumber' => 'Phobject', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', diff --git a/src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php new file mode 100644 index 0000000000..cdeb8d4222 --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php @@ -0,0 +1,61 @@ + 'string', + 'auth-token' => 'string', + 'from-number' => 'string', + )); + + // Construct an object from the "from-number" to validate it. + $number = new PhabricatorPhoneNumber($options['from-number']); + } + + public function newDefaultOptions() { + return array( + 'account-sid' => null, + 'auth-token' => null, + 'from-number' => null, + ); + } + + public function sendMessage(PhabricatorMailExternalMessage $message) { + $account_sid = $this->getOption('account-sid'); + + $auth_token = $this->getOption('auth-token'); + $auth_token = new PhutilOpaqueEnvelope($auth_token); + + $from_number = $this->getOption('from-number'); + $from_number = new PhabricatorPhoneNumber($from_number); + + $to_number = $message->getToNumber(); + $text_body = $message->getTextBody(); + + $parameters = array( + 'From' => $from_number->toE164(), + 'To' => $to_number->toE164(), + 'Body' => $text_body, + ); + + $result = id(new PhabricatorTwilioFuture()) + ->setAccountSID($account_sid) + ->setAuthToken($auth_token) + ->setMethod('Messages.json', $parameters) + ->setTimeout(60) + ->resolve(); + } + +} diff --git a/src/applications/metamta/future/PhabricatorTwilioFuture.php b/src/applications/metamta/future/PhabricatorTwilioFuture.php index 641adb888e..91b0588d23 100644 --- a/src/applications/metamta/future/PhabricatorTwilioFuture.php +++ b/src/applications/metamta/future/PhabricatorTwilioFuture.php @@ -7,6 +7,7 @@ final class PhabricatorTwilioFuture extends FutureProxy { private $authToken; private $method; private $parameters; + private $timeout; public function __construct() { parent::__construct(null); @@ -28,6 +29,15 @@ final class PhabricatorTwilioFuture extends FutureProxy { return $this; } + public function setTimeout($timeout) { + $this->timeout = $timeout; + return $this; + } + + public function getTimeout() { + return $this->timeout; + } + protected function getProxiedFuture() { if (!$this->future) { if ($this->accountSID === null) { @@ -58,6 +68,11 @@ final class PhabricatorTwilioFuture extends FutureProxy { ->setMethod('POST') ->addHeader('Accept', 'application/json'); + $timeout = $this->getTimeout(); + if ($timeout) { + $future->setTimeout($timeout); + } + $this->future = $future; } diff --git a/src/applications/metamta/message/PhabricatorMailSMSMessage.php b/src/applications/metamta/message/PhabricatorMailSMSMessage.php new file mode 100644 index 0000000000..a7e1d10923 --- /dev/null +++ b/src/applications/metamta/message/PhabricatorMailSMSMessage.php @@ -0,0 +1,29 @@ +toNumber = $to_number; + return $this; + } + + public function getToNumber() { + return $this->toNumber; + } + + public function setTextBody($text_body) { + $this->textBody = $text_body; + return $this; + } + + public function getTextBody() { + return $this->textBody; + } + +} diff --git a/src/applications/metamta/message/PhabricatorPhoneNumber.php b/src/applications/metamta/message/PhabricatorPhoneNumber.php new file mode 100644 index 0000000000..6099ad0736 --- /dev/null +++ b/src/applications/metamta/message/PhabricatorPhoneNumber.php @@ -0,0 +1,25 @@ +number = $number; + } + + public function toE164() { + return '+'.$this->number; + } + +} From dc4d7f1f3e8dfc2d25cded48ce821b6b84f63ec8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 14:32:01 -0800 Subject: [PATCH 19/35] Reorder "Merge" transaction to make "Close as Duplicate" produce a "[Merged]" email subject Summary: Fixes T11782. When you "Close as Duplicate", generate a "[Merged]" email by making the merge the first transaction. (There are other, more-deterministic ways to do this with action strength, but this is much simpler and I believe it suffices.) Test Plan: Used "Close as Duplicate", got a "[Merged]" email out of it. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11782 Differential Revision: https://secure.phabricator.com/D19972 --- .../maniphest/relationship/ManiphestTaskRelationship.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/applications/maniphest/relationship/ManiphestTaskRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskRelationship.php index 04f78b8523..a7d3e6f11c 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskRelationship.php @@ -31,13 +31,12 @@ abstract class ManiphestTaskRelationship $subscriber_phids = $this->loadMergeSubscriberPHIDs($tasks); $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) - ->setNewValue(array('+' => $subscriber_phids)); + ->setTransactionType(ManiphestTaskMergedFromTransaction::TRANSACTIONTYPE) + ->setNewValue(mpull($tasks, 'getPHID')); $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType( - ManiphestTaskMergedFromTransaction::TRANSACTIONTYPE) - ->setNewValue(mpull($tasks, 'getPHID')); + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) + ->setNewValue(array('+' => $subscriber_phids)); return $xactions; } From c5f446defb520a8cf0daa0a1dcf750057ca71589 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 15 Jan 2019 06:29:34 -0800 Subject: [PATCH 20/35] Prevent application email addresses from shadowing user email addresses Summary: Fixes T13234. Don't let application email addresses be configured with user addresses. This might prevent an unlikely bit of mischief where someone does this intentionally, detailed in T13234. (Possibly, these tables should just be merged some day, similar to how the "Password" table is now a shared resource that's modular enough for multiple applications to use it.) Test Plan: {F6132259} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13234 Differential Revision: https://secure.phabricator.com/D19974 --- .../PhabricatorMetaMTAApplicationEmailEditor.php | 14 ++++++++++++++ .../storage/PhabricatorMetaMTAReceivedMail.php | 7 +++++++ .../metamta/util/PhabricatorMailUtil.php | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php index 8df02793f7..843e653039 100644 --- a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php +++ b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php @@ -103,6 +103,7 @@ final class PhabricatorMetaMTAApplicationEmailEditor $type, pht('Invalid'), pht('Email address is not formatted properly.')); + continue; } $address = new PhutilEmailAddress($email); @@ -113,6 +114,19 @@ final class PhabricatorMetaMTAApplicationEmailEditor pht( 'This email address is reserved. Choose a different '. 'address.')); + continue; + } + + // See T13234. Prevent use of user email addresses as application + // email addresses. + if (PhabricatorMailUtil::isUserAddress($address)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('In Use'), + pht( + 'This email address is already in use by a user. Choose '. + 'a different address.')); + continue; } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 714e3d3c35..5310cac727 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -170,6 +170,13 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { unset($targets[$key]); continue; } + + // See T13234. Don't process mail if a user has attached this address + // to their account. + if (PhabricatorMailUtil::isUserAddress($target)) { + unset($targets[$key]); + continue; + } } $any_accepted = false; diff --git a/src/applications/metamta/util/PhabricatorMailUtil.php b/src/applications/metamta/util/PhabricatorMailUtil.php index 672f80f666..a5fbc7179e 100644 --- a/src/applications/metamta/util/PhabricatorMailUtil.php +++ b/src/applications/metamta/util/PhabricatorMailUtil.php @@ -108,4 +108,12 @@ final class PhabricatorMailUtil return false; } + public static function isUserAddress(PhutilEmailAddress $address) { + $user_email = id(new PhabricatorUserEmail())->loadOneWhere( + 'address = %s', + $address->getAddress()); + + return (bool)$user_email; + } + } From bd077bfcb7ac73529a56fd106ff2d152b374d5f7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 15:19:29 -0800 Subject: [PATCH 21/35] Update inbound and outbound email documentation Summary: Fixes T8636. Mention Herald for inbound, update some outbound stuff, do some language / organization tweaks. Test Plan: Read documentation. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T8636 Differential Revision: https://secure.phabricator.com/D19973 --- .../configuring_inbound_email.diviner | 99 ++++++++++++++----- .../configuring_outbound_email.diviner | 76 +++++++------- 2 files changed, 114 insertions(+), 61 deletions(-) diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner index 84d4fa48d1..25d818a49b 100644 --- a/src/docs/user/configuration/configuring_inbound_email.diviner +++ b/src/docs/user/configuration/configuring_inbound_email.diviner @@ -4,10 +4,36 @@ This document contains instructions for configuring inbound email, so users may interact with some Phabricator applications via email. -= Preamble = +Preamble +======== -This can be extremely difficult to configure correctly. This is doubly true if -you use a local MTA. +Phabricator can process inbound mail in two general ways: + +**Handling Replies**: When users reply to email notifications about changes, +Phabricator can turn email into comments on the relevant discussion thread. + +**Creating Objects**: You can configure an address like `bugs@yourcompany.com` +to create new objects (like tasks) when users send email. + +In either case, users can interact with objects via `!commands` mail commands +to apply a broader set of changes to objects (like adding subscribers, closing +tasks, or changing priorities) beyond simply commenting. + +To configure inbound mail, you will generally: + + - Configure some mail domain to submit mail to Phabricator for processing. + - For handling replies, set `metamta.reply-handler-domain` in configuration. + - For handling email that creates objects, configure inbound addresses in the + relevant application. + +See below for details on each of these steps. + + +Approaches +========== + +Inbound mail can be extremely difficult to configure correctly. This is doubly +true if you use a local MTA. There are a few approaches available: @@ -22,11 +48,13 @@ 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 = + +Configuring "Reply" Email +========================= 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 +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 @@ -44,22 +72,40 @@ 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' +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 +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 + +Configuring "Create" Email +========================== + +You can set up application email addresses to allow users to create objects via +email. For example, you could configure `bugs@phabricator.example.com` to creat +a Maniphest task out of any email which is sent to it. + +You can find application email settings for each application at: {nav icon=home, name=Home > -name=Applications > -icon=cog, name=Settings} +Applications > +type=instructions, name="Select an Application" > +icon=cog, name=Configure} -= Security = +Not all application support creating objects via email. + +In some applications, including Maniphest, you can also configure Herald rules +with the `[ Content source ]` and/or `[ Receiving email address ]` fields to +route or handle objects based on which address mail was sent to. + +You'll also need to configure the actual mail domain to submit mail to +Phabricator by following the instructions below. Phabricator will let you add +any address as an application address, but can only process mail which is +actually delivered to it. + + +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 @@ -99,7 +145,9 @@ 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 = + +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: @@ -116,7 +164,9 @@ if your inbound email configuration is incorrect or even disabled. Run `bin/mail help ` for detailed help on using these commands. -= Mailgun Setup = + +Mailgun Setup +============= To use Mailgun, you need a Mailgun account. You can sign up at . Provided you have such an account, configure it @@ -128,6 +178,7 @@ like this: example domain with your actual domain. - Configure a mailer in `cluster.mailers` with your Mailgun API key. + Postmark Setup ============== @@ -143,7 +194,8 @@ discussion of the remote address whitelist used to verify that requests this endpoint receives are authentic requests originating from Postmark. -= SendGrid Setup = +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: @@ -159,14 +211,16 @@ inbound email. Provided you have such an account, configure it like this: - 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.reply-handler-domain` to `phabricator.example.com`" + - 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 `bin/mail list-inbound` within a few seconds. -= Local MTA: Installing Mailparse = + +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: @@ -189,7 +243,8 @@ If you get a linker error like this: mailparse.so. This is not the default if you have individual files in `php.d/`. -= Local MTA: Configuring Sendmail = +Local MTA: Configuring Sendmail +=============================== Before you can configure Sendmail, you need to install Mailparse. See the section "Installing Mailparse" above. diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index e2de59c2bc..7691a88d24 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -11,8 +11,8 @@ including a local mailer or various third-party services. Options include: | Send Mail With | Setup | Cost | Inbound | Notes | |---------|-------|------|---------|-------| -| Mailgun | Easy | Cheap | Yes | Recommended | | Postmark | Easy | Cheap | Yes | Recommended | +| Mailgun | Easy | Cheap | Yes | Recommended | | Amazon SES | Easy | Cheap | No | Recommended | | SendGrid | Medium | Cheap | Yes | Discouraged | | External SMTP | Medium | Varies | No | Gmail, etc. | @@ -23,12 +23,11 @@ including a local mailer or various third-party services. Options include: See below for details on how to select and configure mail delivery for each mailer. -Overall, Mailgun and SES are much easier to set up, and using one of them is -recommended. In particular, Mailgun will also let you set up inbound email -easily. +Overall, Postmark and Mailgun are much easier to set up, and using one of them +is recommended. Both will also let you set up inbound email easily. -If you have some internal mail service you'd like to use you can also -write a custom mailer, but this requires digging into the code. +If you have some internal mail service you'd like to use you can also write a +custom mailer, but this requires digging into the code. Phabricator sends mail in the background, so the daemons need to be running for it to be able to deliver mail. You should receive setup warnings if they are @@ -39,14 +38,15 @@ not. For more information on using daemons, see Basics ====== -Regardless of how outbound email is delivered, you should configure these keys -in your configuration: +Before configuring outbound mail, you should first set up +`metamta.default-address` in Configuration. This determines where mail is sent +"From" by default. - - **metamta.default-address** determines where mail is sent "From" by - default. If your domain is `example.org`, set this to something like - `noreply@example.org`. - - **metamta.can-send-as-user** should be left as `false` in most cases, - but see the documentation for details. +If your domain is `example.org`, set this to something +like `noreply@example.org`. + +Ideally, this should be a valid, deliverable address that doesn't bounce if +users accidentally send mail to it. Configuring Mailers @@ -95,7 +95,7 @@ The `type` field can be used to select these third-party mailers: - `mailgun`: Use Mailgun. - `ses`: Use Amazon SES. - - `sendgrid`: Use Sendgrid. + - `sendgrid`: Use SendGrid. - `postmark`: Use Postmark. It also supports these local mailers: @@ -104,12 +104,12 @@ It also supports these local mailers: - `smtp`: Connect directly to an SMTP server. - `test`: Internal mailer for testing. Does not send mail. -You can also write your own mailer by extending -`PhabricatorMailAdapter`. +You can also write your own mailer by extending `PhabricatorMailAdapter`. The `media` field supports these values: - `email`: Configure this mailer for email. + - `sms`: Configure this mailer for SMS. Once you've selected a mailer, find the corresponding section below for instructions on configuring it. @@ -149,22 +149,10 @@ For alternatives and more information on configuration, see @{article:Configuration User Guide: Advanced Configuration} -Mailer: Mailgun -=============== - -Mailgun is a third-party email delivery service. You can learn more at -. Mailgun is easy to configure and works well. - -To use this mailer, set `type` to `mailgun`, then configure these `options`: - - - `api-key`: Required string. Your Mailgun API key. - - `domain`: Required string. Your Mailgun domain. - - Mailer: Postmark ================ -Postmark is a third-party email delivery serivice. You can learn more at +Postmark is a third-party email delivery service. You can learn more at . To use this mailer, set `type` to `postmark`, then configure these `options`: @@ -191,6 +179,18 @@ The default address ranges were last updated in January 2019, and were documented at: +Mailer: Mailgun +=============== + +Mailgun is a third-party email delivery service. You can learn more at +. Mailgun is easy to configure and works well. + +To use this mailer, set `type` to `mailgun`, then configure these `options`: + + - `api-key`: Required string. Your Mailgun API key. + - `domain`: Required string. Your Mailgun domain. + + Mailer: Amazon SES ================== @@ -204,7 +204,7 @@ To use this mailer, set `type` to `ses`, then configure these `options`: - `endpoint`: Required string. Your Amazon SES endpoint. NOTE: Amazon SES **requires you to verify your "From" address**. Configure -which "From" address to use by setting "`metamta.default-address`" in your +which "From" address to use by setting `metamta.default-address` in your config, then follow the Amazon SES verification process to verify it. You won't be able to send email until you do this! @@ -221,27 +221,25 @@ API. To use SMTP, configure Phabricator to use an `smtp` mailer. To use the REST API mailer, set `type` to `sendgrid`, then configure these `options`: - - `api-user`: Required string. Your SendGrid login name. - `api-key`: Required string. Your SendGrid API key. -NOTE: Users have experienced a number of odd issues with SendGrid, compared to -fewer issues with other mailers. We discourage SendGrid unless you're already -using it. +Older versions of the SendGrid API used different sets of credentials, +including an "API User". Make sure you're configuring your "API Key". Mailer: Sendmail ================ -This requires a `sendmail` binary to be installed on -the system. Most MTAs (e.g., sendmail, qmail, postfix) should do this, but your -machine may not have one installed by default. For install instructions, consult -the documentation for your favorite MTA. +This requires a `sendmail` binary to be installed on the system. Most MTAs +(e.g., sendmail, qmail, postfix) should do this, but your machine may not have +one installed by default. For install instructions, consult the documentation +for your favorite MTA. Since you'll be sending the mail yourself, you are subject to things like SPF rules, blackholes, and MTA configuration which are beyond the scope of this document. If you can already send outbound email from the command line or know how to configure it, this option is straightforward. If you have no idea how to -do any of this, strongly consider using Mailgun or Amazon SES instead. +do any of this, strongly consider using Postmark or Mailgun instead. To use this mailer, set `type` to `sendmail`. There are no `options` to configure. From 0a0afa489a5d23e20358a437faf28833ff95d133 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 14:27:34 -0800 Subject: [PATCH 22/35] Wordsmith inbound mail documentation more thoroughly Summary: See D19973. Fix a couple typos and try to make some sections more clear / less scary. Test Plan: Read text. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19986 --- .../configuring_inbound_email.diviner | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner index 25d818a49b..b1ad08b7dd 100644 --- a/src/docs/user/configuration/configuring_inbound_email.diviner +++ b/src/docs/user/configuration/configuring_inbound_email.diviner @@ -15,34 +15,37 @@ Phabricator can turn email into comments on the relevant discussion thread. **Creating Objects**: You can configure an address like `bugs@yourcompany.com` to create new objects (like tasks) when users send email. -In either case, users can interact with objects via `!commands` mail commands -to apply a broader set of changes to objects (like adding subscribers, closing -tasks, or changing priorities) beyond simply commenting. +In either case, users can interact with objects via mail commands to apply a +broader set of changes to objects beyond commenting. (For example, you can use +`!close` to close a task or `!priority` to change task priority.) To configure inbound mail, you will generally: - Configure some mail domain to submit mail to Phabricator for processing. - - For handling replies, set `metamta.reply-handler-domain` in configuration. + - For handling replies, set `metamta.reply-handler-domain` in your + configuration. - For handling email that creates objects, configure inbound addresses in the relevant application. See below for details on each of these steps. -Approaches -========== +Configuration Overview +====================== -Inbound mail can be extremely difficult to configure correctly. This is doubly -true if you use a local MTA. +Usually, the most challenging part of configuring inbound mail is getting mail +delivered to Phabricator for processing. This step can be made much easier if +you use a third-party mail service which can submit mail to Phabricator via +webhooks. -There are a few approaches available: +Some available approaches for delivering mail to Phabricator are: | Receive Mail With | Setup | Cost | Notes | |--------|-------|------|-------| | Mailgun | Easy | Cheap | Recommended | | Postmark | Easy | Cheap | Recommended | | SendGrid | Easy | Cheap | | -| Local MTA | Extremely Difficult | Free | Strongly discouraged! | +| Local MTA | Difficult | Free | Discouraged | The remainder of this document walks through configuring Phabricator to receive mail, and then configuring your chosen transport to deliver mail @@ -53,18 +56,20 @@ Configuring "Reply" Email ========================= 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. +as the "From" address when it sends mail. The exact address it uses can be +configured with `metamta.default-address`. + +When a user takes an action that generates mail, Phabricator sets the +"Reply-To" addresss for the mail to that user's name and address. This means +that users can reply to email to discuss changes, but: the conversation won't +be recorded in Phabricator; and users will not be able to use email commands +to take actions or make edits. 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 +`phabricator.example.com`. Once you set this key, email 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. @@ -82,8 +87,8 @@ Configuring "Create" Email ========================== You can set up application email addresses to allow users to create objects via -email. For example, you could configure `bugs@phabricator.example.com` to creat -a Maniphest task out of any email which is sent to it. +email. For example, you could configure `bugs@phabricator.example.com` to +create a Maniphest task out of any email which is sent to it. You can find application email settings for each application at: @@ -92,7 +97,7 @@ Applications > type=instructions, name="Select an Application" > icon=cog, name=Configure} -Not all application support creating objects via email. +Not all applications support creating objects via email. In some applications, including Maniphest, you can also configure Herald rules with the `[ Content source ]` and/or `[ Receiving email address ]` fields to From a1516fefb67a255e92d4b669d577e6d8ddc69368 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 06:36:10 -0800 Subject: [PATCH 23/35] Fix an issue where "Import Columns" could fail on a board for a project with milestones Summary: See PHI1025. When you "Import Columns", we test if you're trying to import into a board that already has columns. However, this test is too broad (it incorrectly detects "proxy" columns for milestones as columns) and not user-friendly (it returns 400 instead of a readable error). Correct these issues, and refine some of the logic around proxy columns. Test Plan: - Created a project, A. - Created a milestone under that project. - Imported another project's columns to A's workboard. - Before change: Unhelpful 400. - After change: import worked fine. - Also, hit the new error dialogs and read through them. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19978 --- ...habricatorProjectBoardImportController.php | 31 ++++++++++++++----- .../query/PhabricatorProjectColumnQuery.php | 14 +++++++++ .../PhabricatorProjectDatasource.php | 1 + 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardImportController.php b/src/applications/project/controller/PhabricatorProjectBoardImportController.php index c344bc0af0..67bddaaa52 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardImportController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardImportController.php @@ -21,17 +21,28 @@ final class PhabricatorProjectBoardImportController } $this->setProject($project); + $project_id = $project->getID(); + $board_uri = $this->getApplicationURI("board/{$project_id}/"); + + // See PHI1025. We only want to prevent the import if the board already has + // real columns. If it has proxy columns (for example, for milestones) you + // can still import columns from another board. $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) + ->withIsProxyColumn(false) ->execute(); if ($columns) { - return new Aphront400Response(); + return $this->newDialog() + ->setTitle(pht('Workboard Already Has Columns')) + ->appendParagraph( + pht( + 'You can not import columns into this workboard because it '. + 'already has columns. You can only import into an empty '. + 'workboard.')) + ->addCancelButton($board_uri); } - $project_id = $project->getID(); - $board_uri = $this->getApplicationURI("board/{$project_id}/"); - if ($request->isFormPost()) { $import_phid = $request->getArr('importProjectPHID'); $import_phid = reset($import_phid); @@ -39,9 +50,16 @@ final class PhabricatorProjectBoardImportController $import_columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($import_phid)) + ->withIsProxyColumn(false) ->execute(); if (!$import_columns) { - return new Aphront400Response(); + return $this->newDialog() + ->setTitle(pht('Source Workboard Has No Columns')) + ->appendParagraph( + pht( + 'You can not import columns from that workboard because it has '. + 'no importable columns.')) + ->addCancelButton($board_uri); } $table = id(new PhabricatorProjectColumn()) @@ -50,9 +68,6 @@ final class PhabricatorProjectBoardImportController if ($import_column->isHidden()) { continue; } - if ($import_column->getProxy()) { - continue; - } $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence($import_column->getSequence()) diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php index 13f2f52a43..441c33e8cb 100644 --- a/src/applications/project/query/PhabricatorProjectColumnQuery.php +++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php @@ -8,6 +8,7 @@ final class PhabricatorProjectColumnQuery private $projectPHIDs; private $proxyPHIDs; private $statuses; + private $isProxyColumn; public function withIDs(array $ids) { $this->ids = $ids; @@ -34,6 +35,11 @@ final class PhabricatorProjectColumnQuery return $this; } + public function withIsProxyColumn($is_proxy) { + $this->isProxyColumn = $is_proxy; + return $this; + } + public function newResultObject() { return new PhabricatorProjectColumn(); } @@ -156,6 +162,14 @@ final class PhabricatorProjectColumnQuery $this->statuses); } + if ($this->isProxyColumn !== null) { + if ($this->isProxyColumn) { + $where[] = qsprintf($conn, 'proxyPHID IS NOT NULL'); + } else { + $where[] = qsprintf($conn, 'proxyPHID IS NULL'); + } + } + return $where; } diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index e5b24335cf..5b999a997f 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -53,6 +53,7 @@ final class PhabricatorProjectDatasource $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($projs)) + ->withIsProxyColumn(false) ->execute(); $has_cols = mgroup($columns, 'getProjectPHID'); } else { From 6b6c991ad4989cf6a986a65b2ca5f24867e15ef1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 06:59:06 -0800 Subject: [PATCH 24/35] Allow Phortune accounts to customize their billing address and name Summary: See PHI1023. Ref T7607. Occasionally, companies need their billing address (or some other custom text) to appear on invoices to satisfy process or compliance requirements. Allow accounts to have a custom "Billing Name" and a custom "Billing Address" which appear on invoices. Test Plan: {F6134707} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T7607 Differential Revision: https://secure.phabricator.com/D19979 --- resources/celerity/map.php | 4 +- .../20190116.phortune.01.billing.sql | 3 + src/__phutil_library_map__.php | 4 ++ .../cart/PhortuneCartViewController.php | 32 +++++++---- .../editor/PhortuneAccountEditEngine.php | 19 +++++++ .../phortune/storage/PhortuneAccount.php | 6 ++ .../phortune/view/PhortuneInvoiceView.php | 2 +- ...ortuneAccountBillingAddressTransaction.php | 39 +++++++++++++ .../PhortuneAccountBillingNameTransaction.php | 56 +++++++++++++++++++ .../application/phortune/phortune-invoice.css | 2 +- 10 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 resources/sql/autopatches/20190116.phortune.01.billing.sql create mode 100644 src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php create mode 100644 src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8cada227c6..6b170821a6 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -91,7 +91,7 @@ return array( 'rsrc/css/application/pholio/pholio-inline-comments.css' => '722b48c2', 'rsrc/css/application/pholio/pholio.css' => '88ef5ef1', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8', - 'rsrc/css/application/phortune/phortune-invoice.css' => 'e41765fc', + 'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241', 'rsrc/css/application/phortune/phortune.css' => '12e8251a', 'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67', 'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0', @@ -788,7 +788,7 @@ return array( 'phortune-credit-card-form' => 'd12d214f', 'phortune-credit-card-form-css' => '3b9868a8', 'phortune-css' => '12e8251a', - 'phortune-invoice-css' => 'e41765fc', + 'phortune-invoice-css' => '4436b241', 'phrequent-css' => 'bd79cc67', 'phriction-document-css' => '03380da0', 'phui-action-panel-css' => '6c386cbf', diff --git a/resources/sql/autopatches/20190116.phortune.01.billing.sql b/resources/sql/autopatches/20190116.phortune.01.billing.sql new file mode 100644 index 0000000000..77d00e220e --- /dev/null +++ b/resources/sql/autopatches/20190116.phortune.01.billing.sql @@ -0,0 +1,3 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_account + ADD billingName VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + ADD billingAddress LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a72434f9a7..04631101c4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4905,7 +4905,9 @@ phutil_register_library_map(array( 'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php', 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', + 'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php', 'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php', + 'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php', 'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php', 'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php', 'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php', @@ -11067,7 +11069,9 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhortuneAccountAddManagerController' => 'PhortuneController', + 'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType', 'PhortuneAccountBillingController' => 'PhortuneAccountProfileController', + 'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType', 'PhortuneAccountChargeListController' => 'PhortuneController', 'PhortuneAccountController' => 'PhortuneController', 'PhortuneAccountEditController' => 'PhortuneController', diff --git a/src/applications/phortune/controller/cart/PhortuneCartViewController.php b/src/applications/phortune/controller/cart/PhortuneCartViewController.php index 8387cacb07..8108c83b39 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartViewController.php @@ -226,25 +226,37 @@ final class PhortuneCartViewController ->withPHIDs(array($buyer_phid)) ->needProfileImage(true) ->executeOne(); - // TODO: Add account "Contact" info $merchant_contact = new PHUIRemarkupView( - $viewer, $merchant->getContactInfo()); - $description = null; + $viewer, + $merchant->getContactInfo()); + + $account_name = $account->getBillingName(); + if (!strlen($account_name)) { + $account_name = $buyer->getRealName(); + } + + $account_contact = $account->getBillingAddress(); + if (strlen($account_contact)) { + $account_contact = new PHUIRemarkupView( + $viewer, + $account_contact); + } $view = id(new PhortuneInvoiceView()) ->setMerchantName($merchant->getName()) ->setMerchantLogo($merchant->getProfileImageURI()) ->setMerchantContact($merchant_contact) ->setMerchantFooter($merchant->getInvoiceFooter()) - ->setAccountName($buyer->getRealName()) + ->setAccountName($account_name) + ->setAccountContact($account_contact) ->setStatus($error_view) - ->setContent(array( - $description, - $details, - $cart_box, - $charges, - )); + ->setContent( + array( + $details, + $cart_box, + $charges, + )); } $page = $this->newPage() diff --git a/src/applications/phortune/editor/PhortuneAccountEditEngine.php b/src/applications/phortune/editor/PhortuneAccountEditEngine.php index ed0e4b01be..1b6f9a5040 100644 --- a/src/applications/phortune/editor/PhortuneAccountEditEngine.php +++ b/src/applications/phortune/editor/PhortuneAccountEditEngine.php @@ -99,6 +99,25 @@ final class PhortuneAccountEditEngine ->setConduitTypeDescription(pht('New list of managers.')) ->setInitialValue($object->getMemberPHIDs()) ->setValue($member_phids), + + id(new PhabricatorTextEditField()) + ->setKey('billingName') + ->setLabel(pht('Billing Name')) + ->setDescription(pht('Account name for billing purposes.')) + ->setConduitTypeDescription(pht('New account billing name.')) + ->setTransactionType( + PhortuneAccountBillingNameTransaction::TRANSACTIONTYPE) + ->setValue($object->getBillingName()), + + id(new PhabricatorTextAreaEditField()) + ->setKey('billingAddress') + ->setLabel(pht('Billing Address')) + ->setDescription(pht('Account billing address.')) + ->setConduitTypeDescription(pht('New account billing address.')) + ->setTransactionType( + PhortuneAccountBillingAddressTransaction::TRANSACTIONTYPE) + ->setValue($object->getBillingAddress()), + ); return $fields; diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index b0b57645c3..ade98d327f 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -12,11 +12,15 @@ final class PhortuneAccount extends PhortuneDAO PhabricatorPolicyInterface { protected $name; + protected $billingName; + protected $billingAddress; private $memberPHIDs = self::ATTACHABLE; public static function initializeNewAccount(PhabricatorUser $actor) { return id(new self()) + ->setBillingName('') + ->setBillingAddress('') ->attachMemberPHIDs(array()); } @@ -75,6 +79,8 @@ final class PhortuneAccount extends PhortuneDAO self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', + 'billingName' => 'text255', + 'billingAddress' => 'text', ), ) + parent::getConfiguration(); } diff --git a/src/applications/phortune/view/PhortuneInvoiceView.php b/src/applications/phortune/view/PhortuneInvoiceView.php index 89ad3fd1bc..65da418cc2 100644 --- a/src/applications/phortune/view/PhortuneInvoiceView.php +++ b/src/applications/phortune/view/PhortuneInvoiceView.php @@ -82,7 +82,7 @@ final class PhortuneInvoiceView extends AphrontTagView { array( 'class' => 'phortune-mini-header', ), - pht('To:')); + pht('Bill To:')); $bill_to = phutil_tag( 'td', diff --git a/src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php b/src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php new file mode 100644 index 0000000000..f4d62b1dcb --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php @@ -0,0 +1,39 @@ +getBillingAddress(); + } + + public function applyInternalEffects($object, $value) { + $object->setBillingAddress($value); + } + + public function getTitle() { + return pht( + '%s updated the account billing address.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function getMailDiffSectionHeader() { + return pht('CHANGES TO BILLING ADDRESS'); + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($this->getOldValue()) + ->setNewText($this->getNewValue()); + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php b/src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php new file mode 100644 index 0000000000..6c2cde6c9b --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php @@ -0,0 +1,56 @@ +getBillingName(); + } + + public function applyInternalEffects($object, $value) { + $object->setBillingName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && strlen($new)) { + return pht( + '%s changed the billing name for this account from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if (strlen($old)) { + return pht( + '%s removed the billing name for this account (was %s).', + $this->renderAuthor(), + $this->renderOldValue()); + } else { + return pht( + '%s set the billing name for this account to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('billingName'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newRequiredError( + pht('The billing name can be no longer than %s characters.', + new PhutilNumber($max_length))); + } + } + + return $errors; + } + +} diff --git a/webroot/rsrc/css/application/phortune/phortune-invoice.css b/webroot/rsrc/css/application/phortune/phortune-invoice.css index 34bceb4bba..59199f0c94 100644 --- a/webroot/rsrc/css/application/phortune/phortune-invoice.css +++ b/webroot/rsrc/css/application/phortune/phortune-invoice.css @@ -49,7 +49,7 @@ font-weight: bold; text-transform: uppercase; margin-bottom: 4px; - letter-spacing: 0.3em; + letter-spacing: 0.25em; } .phortune-invoice-status { From ff220acae6a4350e15739508a6d5fef0dbba69c4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 06:38:11 -0800 Subject: [PATCH 25/35] Don't bounce mail messages if any recipient was reserved Summary: Ref T13222. If we receive a message and nothing processes it, we normally try to send the user an error message like "hey, nothing handled this, maybe you got the address wrong". Just skip the "send them an error message" part if any recipient was reserved, so if you "Reply All" to a message that is "From: noreply@phabricator" you don't get a relatively unhelpful error. This also makes sure that the "void" address doesn't generate bounces if the "From" is a valid user email address (e.g., with `metamta.can-send-as-user`). That is: - Phabricator needs to send a mail with only "CC" users. - Phabricator puts the "void" address in "To" as a placeholder. - The "void" address happens to route back to Phabricator. We don't want that mail to bounce to anywhere. Normally, it won't: - From is usually "noreply@phabricator", which isn't a user, so we won't send anything back: we only send mail to verified user email addresses. - The message will have "X-Phabricator-Sent-This-Message: true" so we won't process it at all. ...but this is another layer of certainty. Test Plan: Used `bin/mail receive-test` to receive mail to an invalid, unreserved address (bounce/error email) and an invalid, reserved address (no bounce/error email). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19987 --- .../constants/MetaMTAReceivedMailStatus.php | 2 ++ .../PhabricatorMetaMTAReceivedMail.php | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php index 2ec44b847a..faacdc2cfc 100644 --- a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php +++ b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php @@ -16,6 +16,7 @@ final class MetaMTAReceivedMailStatus const STATUS_UNHANDLED_EXCEPTION = 'err:exception'; const STATUS_EMPTY = 'err:empty'; const STATUS_EMPTY_IGNORED = 'err:empty-ignored'; + const STATUS_RESERVED = 'err:reserved-recipient'; public static function getHumanReadableName($status) { $map = array( @@ -32,6 +33,7 @@ final class MetaMTAReceivedMailStatus self::STATUS_UNHANDLED_EXCEPTION => pht('Unhandled Exception'), self::STATUS_EMPTY => pht('Empty Mail'), self::STATUS_EMPTY_IGNORED => pht('Ignored Empty Mail'), + self::STATUS_RESERVED => pht('Reserved Recipient'), ); return idx($map, $status, pht('Processing Exception')); diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 5310cac727..64528ea949 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -161,12 +161,16 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { ->setFilterMethod('isEnabled') ->execute(); + $reserved_recipient = null; $targets = $this->newTargetAddresses(); foreach ($targets as $key => $target) { // Never accept any reserved address as a mail target. This prevents // security issues around "hostmaster@" and bad behavior with // "noreply@". if (PhabricatorMailUtil::isReservedAddress($target)) { + if (!$reserved_recipient) { + $reserved_recipient = $target; + } unset($targets[$key]); continue; } @@ -212,8 +216,26 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { throw $receiver_exception; } + if (!$any_accepted) { - if (!$sender) { + if ($reserved_recipient) { + // If nothing accepted the mail, we normally raise an error to help + // users who mistakenly send mail to "barges@" instead of "bugs@". + + // However, if the recipient list included a reserved recipient, we + // don't bounce the mail with an error. + + // The intent here is that if a user does a "Reply All" and includes + // "From: noreply@phabricator" in the receipient list, we just want + // to drop the mail rather than send them an unhelpful bounce message. + + throw new PhabricatorMetaMTAReceivedMailProcessingException( + MetaMTAReceivedMailStatus::STATUS_RESERVED, + pht( + 'No application handled this mail. This mail was sent to a '. + 'reserved recipient ("%s") so bounces are suppressed.', + (string)$reserved_recipient)); + } else if (!$sender) { // NOTE: Currently, we'll always drop this mail (since it's headed to // an unverified recipient). See T12237. These details are still // useful because they'll appear in the mail logs and Mail web UI. @@ -244,6 +266,10 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { // Don't send an error email back in these cases, since they're // very unlikely to be the sender's fault. break; + case MetaMTAReceivedMailStatus::STATUS_RESERVED: + // This probably is the sender's fault, but it's likely an accident + // that we received the mail at all. + break; case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED: // This error is explicitly ignored. break; From c125ab7a42bf868ff5b164d9a4f1fe3e075db8c9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 11:52:03 -0800 Subject: [PATCH 26/35] Remove "metamta.*.subject-prefix" options Summary: In ~2012, the first of these options was added because someone who hates dogs and works at Asana also hated `[Differential]` in the subject line. The use case there was actually //removing// the text, not changing it, but I made the prefix editable since it seemed like slightly less of a one-off. These options are among the dumbest and most useless config options we have and very rarely used, see T11760. A very small number of instances have configured one of these options. Newer applications stopped providing these options and no one has complained. You can get the same effect with `translation.override`. Although I'm not sure we'll keep that around forever, it's a reasonable replacement today. I'll call out an example in the changelog to help installs that want to preserve this option. If we did want to provide this, it should just be in {nav Applications > Settings} for each application, but I think it's wildly-low-value and "hack via translations" or "local patch" are entirely reasonable if you really want to change these strings. Test Plan: Grepped for `subject-prefix`. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19993 --- src/__phutil_library_map__.php | 12 ------- .../audit/editor/PhabricatorAuditEditor.php | 2 +- .../PhabricatorExtraConfigSetupCheck.php | 18 +++++++++++ .../config/ConpherenceConfigOptions.php | 32 ------------------- .../conpherence/editor/ConpherenceEditor.php | 2 +- .../PhabricatorDifferentialConfigOptions.php | 5 --- ...alDoorkeeperRevisionFeedStoryPublisher.php | 3 +- .../editor/DifferentialTransactionEditor.php | 2 +- .../mail/DifferentialCreateMailReceiver.php | 3 +- .../PhabricatorDiffusionConfigOptions.php | 5 --- ...sionDoorkeeperCommitFeedStoryPublisher.php | 3 +- .../config/PhabricatorFilesConfigOptions.php | 5 --- .../files/editor/PhabricatorFileEditor.php | 2 +- .../files/mail/FileCreateMailReceiver.php | 3 +- .../PhabricatorLegalpadConfigOptions.php | 32 ------------------- .../editor/LegalpadDocumentEditor.php | 2 +- .../config/PhabricatorMacroConfigOptions.php | 29 ----------------- .../macro/editor/PhabricatorMacroEditor.php | 2 +- .../PhabricatorManiphestConfigOptions.php | 5 --- .../editor/ManiphestTransactionEditor.php | 2 +- .../config/PhabricatorOwnersConfigOptions.php | 2 -- ...bricatorOwnersPackageTransactionEditor.php | 2 +- .../config/PhabricatorPasteConfigOptions.php | 32 ------------------- .../paste/editor/PhabricatorPasteEditor.php | 2 +- .../paste/mail/PasteCreateMailReceiver.php | 3 +- .../config/PhabricatorPholioConfigOptions.php | 29 ----------------- .../pholio/editor/PholioMockEditor.php | 2 +- .../PhabricatorPhrictionConfigOptions.php | 30 ----------------- .../PhabricatorRepositoryPushMailWorker.php | 2 +- 29 files changed, 34 insertions(+), 239 deletions(-) delete mode 100644 src/applications/conpherence/config/ConpherenceConfigOptions.php delete mode 100644 src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php delete mode 100644 src/applications/macro/config/PhabricatorMacroConfigOptions.php delete mode 100644 src/applications/paste/config/PhabricatorPasteConfigOptions.php delete mode 100644 src/applications/pholio/config/PhabricatorPholioConfigOptions.php delete mode 100644 src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 04631101c4..648a8cd894 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -341,7 +341,6 @@ phutil_register_library_map(array( 'ConduitWildParameterType' => 'applications/conduit/parametertype/ConduitWildParameterType.php', 'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php', 'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php', - 'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php', 'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php', 'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', 'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php', @@ -3333,7 +3332,6 @@ phutil_register_library_map(array( 'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php', 'PhabricatorLabelProfileMenuItem' => 'applications/search/menuitem/PhabricatorLabelProfileMenuItem.php', 'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php', - 'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php', 'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php', 'PhabricatorLegalpadSignaturePolicyRule' => 'applications/legalpad/policyrule/PhabricatorLegalpadSignaturePolicyRule.php', 'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php', @@ -3362,7 +3360,6 @@ phutil_register_library_map(array( 'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php', 'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php', 'PhabricatorMacroAudioTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioTransaction.php', - 'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php', 'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php', 'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php', 'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php', @@ -3766,7 +3763,6 @@ phutil_register_library_map(array( 'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php', 'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php', 'PhabricatorPasteArchiveController' => 'applications/paste/controller/PhabricatorPasteArchiveController.php', - 'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php', 'PhabricatorPasteContentSearchEngineAttachment' => 'applications/paste/engineextension/PhabricatorPasteContentSearchEngineAttachment.php', 'PhabricatorPasteContentTransaction' => 'applications/paste/xaction/PhabricatorPasteContentTransaction.php', 'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php', @@ -3851,7 +3847,6 @@ phutil_register_library_map(array( 'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php', 'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php', 'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php', - 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php', 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', @@ -3862,7 +3857,6 @@ phutil_register_library_map(array( 'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php', 'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php', 'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php', - 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', 'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php', 'PhabricatorPhurlConfigOptions' => 'applications/config/option/PhabricatorPhurlConfigOptions.php', 'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php', @@ -5743,7 +5737,6 @@ phutil_register_library_map(array( 'ConduitWildParameterType' => 'ConduitParameterType', 'ConpherenceColumnViewController' => 'ConpherenceController', 'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod', - 'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions', 'ConpherenceConstants' => 'Phobject', 'ConpherenceController' => 'PhabricatorController', 'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', @@ -9172,7 +9165,6 @@ phutil_register_library_map(array( 'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorLegalpadApplication' => 'PhabricatorApplication', - 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType', 'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase', @@ -9201,7 +9193,6 @@ phutil_register_library_map(array( 'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroAudioController' => 'PhabricatorMacroController', 'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType', - 'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMacroController' => 'PhabricatorController', 'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorMacroDisableController' => 'PhabricatorMacroController', @@ -9688,7 +9679,6 @@ phutil_register_library_map(array( ), 'PhabricatorPasteApplication' => 'PhabricatorApplication', 'PhabricatorPasteArchiveController' => 'PhabricatorPasteController', - 'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteController' => 'PhabricatorController', @@ -9773,7 +9763,6 @@ phutil_register_library_map(array( 'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhluxApplication' => 'PhabricatorApplication', 'PhabricatorPholioApplication' => 'PhabricatorApplication', - 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPhoneNumber' => 'Phobject', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', @@ -9784,7 +9773,6 @@ phutil_register_library_map(array( 'PhabricatorPhragmentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionApplication' => 'PhabricatorApplication', - 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlApplication' => 'PhabricatorApplication', 'PhabricatorPhurlConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlController' => 'PhabricatorController', diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 1b032b3cf6..d4fa1c32ff 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -437,7 +437,7 @@ final class PhabricatorAuditEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); + return pht('[Diffusion]'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 12b9463166..d5f44c87c8 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -203,6 +203,11 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { $mailers_reason = pht( 'Inbound and outbound mail is now configured with "cluster.mailers".'); + $prefix_reason = pht( + 'Per-application mail subject prefix customization is no longer '. + 'directly supported. Prefixes and other strings may be customized with '. + '"translation.override".'); + $ancient_config += array( 'phid.external-loaders' => pht( @@ -398,6 +403,19 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'phabricator.allow-email-users' => pht( 'Public email is now accepted if the associated address has a '. 'default author, and rejected otherwise.'), + + 'metamta.conpherence.subject-prefix' => $prefix_reason, + 'metamta.differential.subject-prefix' => $prefix_reason, + 'metamta.diffusion.subject-prefix' => $prefix_reason, + 'metamta.files.subject-prefix' => $prefix_reason, + 'metamta.legalpad.subject-prefix' => $prefix_reason, + 'metamta.macro.subject-prefix' => $prefix_reason, + 'metamta.maniphest.subject-prefix' => $prefix_reason, + 'metamta.package.subject-prefix' => $prefix_reason, + 'metamta.paste.subject-prefix' => $prefix_reason, + 'metamta.pholio.subject-prefix' => $prefix_reason, + 'metamta.phriction.subject-prefix' => $prefix_reason, + ); return $ancient_config; diff --git a/src/applications/conpherence/config/ConpherenceConfigOptions.php b/src/applications/conpherence/config/ConpherenceConfigOptions.php deleted file mode 100644 index 24e1cb126d..0000000000 --- a/src/applications/conpherence/config/ConpherenceConfigOptions.php +++ /dev/null @@ -1,32 +0,0 @@ -newOption( - 'metamta.conpherence.subject-prefix', - 'string', - '[Conpherence]') - ->setDescription(pht('Subject prefix for Conpherence mail.')), - ); - } - -} diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index 3b4ad09e11..a1d6431d8c 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -239,7 +239,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.conpherence.subject-prefix'); + return pht('[Conpherence]'); } protected function supportsSearch() { diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php index 0d00207b43..ec2099f8dd 100644 --- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -228,11 +228,6 @@ EOHELP "\n\n". 'This sort of workflow is very unusual. Very few installs should '. 'need to change this option.')), - $this->newOption( - 'metamta.differential.subject-prefix', - 'string', - '[Differential]') - ->setDescription(pht('Subject prefix for Differential mail.')), $this->newOption( 'metamta.differential.attach-patches', 'bool', diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 59223f20da..3cfe7a7141 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -81,8 +81,7 @@ final class DifferentialDoorkeeperRevisionFeedStoryPublisher } private function getTitlePrefix(DifferentialRevision $revision) { - $prefix_key = 'metamta.differential.subject-prefix'; - return PhabricatorEnv::getEnvConfig($prefix_key); + return pht('[Differential]'); } } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 8bc1b4d972..a250d5a2a0 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -563,7 +563,7 @@ final class DifferentialTransactionEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); + return pht('[Differential]'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { diff --git a/src/applications/differential/mail/DifferentialCreateMailReceiver.php b/src/applications/differential/mail/DifferentialCreateMailReceiver.php index 0d4fd1a2be..a77277e983 100644 --- a/src/applications/differential/mail/DifferentialCreateMailReceiver.php +++ b/src/applications/differential/mail/DifferentialCreateMailReceiver.php @@ -69,8 +69,7 @@ final class DifferentialCreateMailReceiver } } - $subject_prefix = - PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); + $subject_prefix = pht('[Differential]'); if (count($diffs)) { $subject = pht( 'You successfully created %d diff(s).', diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php index d15fb678d5..a0c2277f6e 100644 --- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php +++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php @@ -37,11 +37,6 @@ final class PhabricatorDiffusionConfigOptions } return array( - $this->newOption( - 'metamta.diffusion.subject-prefix', - 'string', - '[Diffusion]') - ->setDescription(pht('Subject prefix for Diffusion mail.')), $this->newOption( 'metamta.diffusion.attach-patches', 'bool', diff --git a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php index 712b7ae150..088e5dc71a 100644 --- a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php +++ b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php @@ -172,8 +172,7 @@ final class DiffusionDoorkeeperCommitFeedStoryPublisher } private function getTitlePrefix(PhabricatorRepositoryCommit $commit) { - $prefix_key = 'metamta.diffusion.subject-prefix'; - return PhabricatorEnv::getEnvConfig($prefix_key); + return pht('[Diffusion]'); } } diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php index 751a06ffdb..735ddfcb0d 100644 --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -197,11 +197,6 @@ final class PhabricatorFilesConfigOptions "Set this to a valid Amazon S3 bucket to store files there. You ". "must also configure S3 access keys in the 'Amazon Web Services' ". "group.")), - $this->newOption( - 'metamta.files.subject-prefix', - 'string', - '[File]') - ->setDescription(pht('Subject prefix for Files email.')), $this->newOption('files.enable-imagemagick', 'bool', false) ->setBoolOptions( array( diff --git a/src/applications/files/editor/PhabricatorFileEditor.php b/src/applications/files/editor/PhabricatorFileEditor.php index db974cec65..91d168e68d 100644 --- a/src/applications/files/editor/PhabricatorFileEditor.php +++ b/src/applications/files/editor/PhabricatorFileEditor.php @@ -27,7 +27,7 @@ final class PhabricatorFileEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix'); + return pht('[File]'); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/files/mail/FileCreateMailReceiver.php b/src/applications/files/mail/FileCreateMailReceiver.php index fe314f1f0a..f3f31d9130 100644 --- a/src/applications/files/mail/FileCreateMailReceiver.php +++ b/src/applications/files/mail/FileCreateMailReceiver.php @@ -33,8 +33,7 @@ final class FileCreateMailReceiver } else { $subject = pht('You successfully uploaded a file.'); } - $subject_prefix = - PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix'); + $subject_prefix = pht('[File]'); $file_uris = array(); foreach ($attachment_phids as $phid) { diff --git a/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php b/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php deleted file mode 100644 index a9584fc94b..0000000000 --- a/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php +++ /dev/null @@ -1,32 +0,0 @@ -newOption( - 'metamta.legalpad.subject-prefix', - 'string', - '[Legalpad]') - ->setDescription(pht('Subject prefix for Legalpad email.')), - ); - } - -} diff --git a/src/applications/legalpad/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php index e4b43186ee..4ede196e39 100644 --- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php +++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php @@ -166,7 +166,7 @@ final class LegalpadDocumentEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.legalpad.subject-prefix'); + return pht('[Legalpad]'); } diff --git a/src/applications/macro/config/PhabricatorMacroConfigOptions.php b/src/applications/macro/config/PhabricatorMacroConfigOptions.php deleted file mode 100644 index 2cb01ff29f..0000000000 --- a/src/applications/macro/config/PhabricatorMacroConfigOptions.php +++ /dev/null @@ -1,29 +0,0 @@ -newOption('metamta.macro.subject-prefix', 'string', '[Macro]') - ->setDescription(pht('Subject prefix for Macro email.')), - ); - } - -} diff --git a/src/applications/macro/editor/PhabricatorMacroEditor.php b/src/applications/macro/editor/PhabricatorMacroEditor.php index f59c29b426..91ed23a259 100644 --- a/src/applications/macro/editor/PhabricatorMacroEditor.php +++ b/src/applications/macro/editor/PhabricatorMacroEditor.php @@ -57,7 +57,7 @@ final class PhabricatorMacroEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.macro.subject-prefix'); + return pht('[Macro]'); } protected function shouldPublishFeedStory( diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index c5527aa485..8f2830908b 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -459,11 +459,6 @@ EOTEXT '%s configuration option. The default value (`90`) '. 'corresponds to the default "Needs Triage" priority.', 'maniphest.priorities')), - $this->newOption( - 'metamta.maniphest.subject-prefix', - 'string', - '[Maniphest]') - ->setDescription(pht('Subject prefix for Maniphest mail.')), $this->newOption('maniphest.points', $points_type, array()) ->setSummary(pht('Configure point values for tasks.')) ->setDescription($points_description) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 47a6b1b4f2..0722e0e27a 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -155,7 +155,7 @@ final class ManiphestTransactionEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); + return pht('[Maniphest]'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { diff --git a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php index 0f827672b7..330d465f3c 100644 --- a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php +++ b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php @@ -36,8 +36,6 @@ final class PhabricatorOwnersConfigOptions $fields_example = id(new PhutilJSON())->encodeFormatted($fields_example); return array( - $this->newOption('metamta.package.subject-prefix', 'string', '[Package]') - ->setDescription(pht('Subject prefix for Owners email.')), $this->newOption('owners.fields', $custom_field_type, $default_fields) ->setCustomData($field_base_class) ->setDescription(pht('Select and reorder package fields.')), diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php index c6aad6e2bd..8885872706 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php @@ -27,7 +27,7 @@ final class PhabricatorOwnersPackageTransactionEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix'); + return pht('[Package]'); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/paste/config/PhabricatorPasteConfigOptions.php b/src/applications/paste/config/PhabricatorPasteConfigOptions.php deleted file mode 100644 index 15b32eeb04..0000000000 --- a/src/applications/paste/config/PhabricatorPasteConfigOptions.php +++ /dev/null @@ -1,32 +0,0 @@ -newOption( - 'metamta.paste.subject-prefix', - 'string', - '[Paste]') - ->setDescription(pht('Subject prefix for Paste email.')), - ); - } - -} diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php index c312915727..76ade878c4 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditor.php +++ b/src/applications/paste/editor/PhabricatorPasteEditor.php @@ -41,7 +41,7 @@ final class PhabricatorPasteEditor } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); + return pht('[Paste]'); } protected function getMailTo(PhabricatorLiskDAO $object) { diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php index 844c5f1acc..5562858b8e 100644 --- a/src/applications/paste/mail/PasteCreateMailReceiver.php +++ b/src/applications/paste/mail/PasteCreateMailReceiver.php @@ -44,8 +44,7 @@ final class PasteCreateMailReceiver return; } - $subject_prefix = - PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); + $subject_prefix = pht('[Paste]'); $subject = pht('You successfully created a paste.'); $paste_uri = PhabricatorEnv::getProductionURI($paste->getURI()); $body = new PhabricatorMetaMTAMailBody(); diff --git a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php b/src/applications/pholio/config/PhabricatorPholioConfigOptions.php deleted file mode 100644 index 30bea98554..0000000000 --- a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php +++ /dev/null @@ -1,29 +0,0 @@ -newOption('metamta.pholio.subject-prefix', 'string', '[Pholio]') - ->setDescription(pht('Subject prefix for Pholio email.')), - ); - } - -} diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php index 7f4d589934..df5d55672c 100644 --- a/src/applications/pholio/editor/PholioMockEditor.php +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -139,7 +139,7 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor { } protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix'); + return pht('[Pholio]'); } public function getMailTagsMap() { diff --git a/src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php b/src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php deleted file mode 100644 index 9fada90d3a..0000000000 --- a/src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php +++ /dev/null @@ -1,30 +0,0 @@ -newOption( - 'metamta.phriction.subject-prefix', 'string', '[Phriction]') - ->setDescription(pht('Subject prefix for Phriction email.')), - ); - } - -} diff --git a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php index 554c2cf772..7459073ca8 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php @@ -99,7 +99,7 @@ final class PhabricatorRepositoryPushMailWorker $body->addTextSection(pht('REFERENCES'), implode("\n", $ref_lines)); } - $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); + $prefix = pht('[Diffusion]'); $parts = array(); if ($commit_count) { From 310ad7f8f47bd8e6d643d329c49929e0303ebf5e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 18 Jan 2019 05:50:52 -0800 Subject: [PATCH 27/35] Put a hard limit on password login attempts from the same remote address Summary: Ref T13222. Currently, if a remote address fails a few login attempts (5) in a short period of time (15 minutes) we require a CAPTCHA for each additional attempt. This relies on: - Administrators configuring ReCAPTCHA, which they may just not bother with. - Administrators being comfortable with Google running arbitrary trusted Javascript, which they may not be comfortable with. - ReCAPTCHA actually being effective, which seems likely true for unsophisticated attackers but perhaps less true for more sophisticated attackers (see , for example). (For unsophisticated attackers and researchers, "Rumola" has been the standard CAPTCHA bypass tool for some time. This is an extension that pays humans to solve CAPTCHAs for you. This is not practical at "brute force a strong password" scale. Google appears to have removed it from the Chrome store. The "submit the captcha back to Google's APIs" trick probably isn't practical at brute-force-scale either, but it's easier to imagine weaponizing that than weaponizing human solvers.) Add a hard gate behind the CAPTHCA wall so that we fail into a secure state if there's no CAPTCHA or the attacker can defeat CAPTCHAs at a very low cost. The big downside to this is that an attacker who controls your remote address (e.g., is behind the same NAT device you're behind on corpnet) can lock you out of your account. However: - That //should// be a lot of access (although maybe this isn't that high of a barrier in many cases, since compromising a "smart fridge" or "smart water glass" or whatever might be good enough). - You can still do "Forgot password?" and login via email link, although this may not be obvious. Test Plan: - Logged in normally. - Failed many many login attempts, got hard gated. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19997 --- .../PhabricatorPasswordAuthProvider.php | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index 75deb9dde5..d841f091aa 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -255,18 +255,48 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { $viewer = $request->getUser(); $content_source = PhabricatorContentSource::newFromRequest($request); + $captcha_limit = 5; + $hard_limit = 32; + $limit_window = phutil_units('15 minutes in seconds'); + + $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( + PhabricatorUserLog::ACTION_LOGIN_FAILURE, + $limit_window); + + // If the same remote address has submitted several failed login attempts + // recently, require they provide a CAPTCHA response for new attempts. $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { - $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( - PhabricatorUserLog::ACTION_LOGIN_FAILURE, - 60 * 15); - if (count($failed_attempts) > 5) { + if (count($failed_attempts) > $captcha_limit) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } + // If the user has submitted quite a few failed login attempts recently, + // give them a hard limit. + if (count($failed_attempts) > $hard_limit) { + $guidance = array(); + + $guidance[] = pht( + 'Your remote address has failed too many login attempts recently. '. + 'Wait a few minutes before trying again.'); + + $guidance[] = pht( + 'If you are unable to log in to your account, you can '. + '[[ /login/email | send a reset link to your email address ]].'); + + $guidance = implode("\n\n", $guidance); + + $dialog = $controller->newDialog() + ->setTitle(pht('Too Many Login Attempts')) + ->appendChild(new PHUIRemarkupView($viewer, $guidance)) + ->addCancelButton('/auth/start/', pht('Wait Patiently')); + + return array(null, $dialog); + } + $response = null; $account = null; $log_user = null; From 98bf3a950dcfafe8d9f4485fc6fa2bb394924841 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 18 Jan 2019 09:35:45 -0800 Subject: [PATCH 28/35] Add setup warnings for "local_infile" (MySQL Server) and "mysql[i].allow_local_infile" (PHP Client) Summary: Ref T13238. Warn users about these horrible options and encourage them to defuse them. Test Plan: Hit both warnings, fixed the issues, issues went away. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13238 Differential Revision: https://secure.phabricator.com/D19999 --- .../check/PhabricatorMySQLSetupCheck.php | 28 +++++++++++++++ .../check/PhabricatorPHPConfigSetupCheck.php | 36 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php index dad9ba6d7b..a4048cbc33 100644 --- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php +++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php @@ -382,6 +382,34 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck { new PhutilNumber($delta))); } + $local_infile = $ref->loadRawMySQLConfigValue('local_infile'); + if ($local_infile) { + $summary = pht( + 'The MySQL "local_infile" option is enabled. This option is '. + 'unsafe.'); + + $message = pht( + 'Your MySQL server is configured with the "local_infile" option '. + 'enabled. This option allows an attacker who finds an SQL injection '. + 'hole to escalate their attack by copying files from the webserver '. + 'into the database with "LOAD DATA LOCAL INFILE" queries, then '. + 'reading the file content with "SELECT" queries.'. + "\n\n". + 'You should disable this option in your %s file, in the %s section:'. + "\n\n". + '%s', + phutil_tag('tt', array(), 'my.cnf'), + phutil_tag('tt', array(), '[mysqld]'), + phutil_tag('pre', array(), 'local_infile=0')); + + $this->newIssue('mysql.local_infile') + ->setName(pht('Unsafe MySQL "local_infile" Setting Enabled')) + ->setSummary($summary) + ->setMessage($message) + ->setDatabaseRef($ref) + ->addMySQLConfig('local_infile'); + } + } protected function shouldUseMySQLSearchEngine() { diff --git a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php index cf3148b5be..bee2bc91b8 100644 --- a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php @@ -112,6 +112,42 @@ final class PhabricatorPHPConfigSetupCheck extends PhabricatorSetupCheck { ->setMessage($message); } + + if (extension_loaded('mysqli')) { + $infile_key = 'mysqli.allow_local_infile'; + } else { + $infile_key = 'mysql.allow_local_infile'; + } + + if (ini_get($infile_key)) { + $summary = pht( + 'Disable unsafe option "%s" in PHP configuration.', + $infile_key); + + $message = pht( + 'PHP is currently configured to honor requests from any MySQL server '. + 'it connects to for the content of any local file.'. + "\n\n". + 'This capability supports MySQL "LOAD DATA LOCAL INFILE" queries, but '. + 'allows a malicious MySQL server read access to the local disk: the '. + 'server can ask the client to send the content of any local file, '. + 'and the client will comply.'. + "\n\n". + 'Although it is normally difficult for an attacker to convince '. + 'Phabricator to connect to a malicious MySQL server, you should '. + 'disable this option: this capability is unnecessary and inherently '. + 'dangerous.'. + "\n\n". + 'To disable this option, set: %s', + phutil_tag('tt', array(), pht('%s = 0', $infile_key))); + + $this->newIssue('php.'.$infile_key) + ->setName(pht('Unsafe PHP "Local Infile" Configuration')) + ->setSummary($summary) + ->setMessage($message) + ->addPHPConfig($infile_key); + } + } } From 5537e29ee8e94381427b1628c7f210a6e5140eec Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 09:39:27 -0800 Subject: [PATCH 29/35] Move "Welcome" mail generation out of PhabricatorUser Summary: Ref PHI1027. Currently, `PhabricatorUser` has a couple of mail-related methods which shouldn't really be there in the long term. Immediately, I want to make some adjusments to the welcome email. Move "Welcome" mail generation to a separate class and consolidate all the error handling. (Eventually, "invite" and "verify address" email should move to similar subclasses, too.) Previously, a bunch of errors/conditions got checked in multiple places. The only functional change is that we no longer allow you to send welcome mail to disabled users. Test Plan: - Used "Send Welcome Mail" from profile pages to send mail. - Hit "not admin", "disabled user", "bot/mailing list" errors. - Used `scripts/user/add_user.php` to send welcome mail. - Used "Create New User" to send welcome mail. - Verified mail with `bin/mail show-outbound`. (Cleaned up a couple of minor display issues here.) Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19989 --- scripts/user/add_user.php | 7 +- src/__phutil_library_map__.php | 6 ++ ...atorMailManagementShowOutboundWorkflow.php | 8 ++ .../PhabricatorPeopleNewController.php | 9 +- ...abricatorPeopleProfileManageController.php | 5 +- .../PhabricatorPeopleWelcomeController.php | 29 ++++--- .../mail/PhabricatorPeopleMailEngine.php | 61 ++++++++++++++ .../PhabricatorPeopleMailEngineException.php | 24 ++++++ .../PhabricatorPeopleWelcomeMailEngine.php | 83 +++++++++++++++++++ .../people/storage/PhabricatorUser.php | 50 ----------- 10 files changed, 218 insertions(+), 64 deletions(-) create mode 100644 src/applications/people/mail/PhabricatorPeopleMailEngine.php create mode 100644 src/applications/people/mail/PhabricatorPeopleMailEngineException.php create mode 100644 src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php index 4c598e47e2..2554ab3ddc 100755 --- a/scripts/user/add_user.php +++ b/scripts/user/add_user.php @@ -59,7 +59,12 @@ id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email_object); -$user->sendWelcomeEmail($admin); +$welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) + ->setSender($admin) + ->setRecipient($user); +if ($welcome_engine->canSendMail()) { + $welcome_engine->sendMail(); +} echo pht( "Created user '%s' (realname='%s', email='%s').\n", diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 648a8cd894..82a639f61f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3814,6 +3814,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', 'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php', 'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php', + 'PhabricatorPeopleMailEngine' => 'applications/people/mail/PhabricatorPeopleMailEngine.php', + 'PhabricatorPeopleMailEngineException' => 'applications/people/mail/PhabricatorPeopleMailEngineException.php', 'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php', 'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php', 'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php', @@ -3841,6 +3843,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php', 'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php', 'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php', + 'PhabricatorPeopleWelcomeMailEngine' => 'applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php', 'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php', 'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php', 'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php', @@ -9730,6 +9733,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleMailEngine' => 'Phobject', + 'PhabricatorPeopleMailEngineException' => 'Exception', 'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPeopleNewController' => 'PhabricatorPeopleController', @@ -9757,6 +9762,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleWelcomeMailEngine' => 'PhabricatorPeopleMailEngine', 'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorPhameApplication' => 'PhabricatorApplication', 'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType', diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php index d462314342..f29a63c2eb 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -115,7 +115,14 @@ final class PhabricatorMailManagementShowOutboundWorkflow $info[] = $this->newSectionHeader(pht('HEADERS')); $headers = $message->getDeliveredHeaders(); + if (!$headers) { + $headers = array(); + } + $unfiltered = $message->getUnfilteredHeaders(); + if (!$unfiltered) { + $unfiltered = array(); + } $header_map = array(); foreach ($headers as $header) { @@ -201,6 +208,7 @@ final class PhabricatorMailManagementShowOutboundWorkflow $info[] = null; } else { $info[] = pht('(This message has no HTML body.)'); + $info[] = null; } $console->writeOut('%s', implode("\n", $info)); diff --git a/src/applications/people/controller/PhabricatorPeopleNewController.php b/src/applications/people/controller/PhabricatorPeopleNewController.php index 3521fb840f..44dfe0e8a6 100644 --- a/src/applications/people/controller/PhabricatorPeopleNewController.php +++ b/src/applications/people/controller/PhabricatorPeopleNewController.php @@ -107,8 +107,13 @@ final class PhabricatorPeopleNewController ->makeMailingListUser($user, true); } - if ($welcome_checked && !$is_bot && !$is_list) { - $user->sendWelcomeEmail($admin); + if ($welcome_checked) { + $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) + ->setSender($admin) + ->setRecipient($user); + if ($welcome_engine->canSendMail()) { + $welcome_engine->sendMail(); + } } $response = id(new AphrontRedirectResponse()) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index 046726f39e..3e6c4ffa6e 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -92,8 +92,11 @@ final class PhabricatorPeopleProfileManageController PeopleDisableUsersCapability::CAPABILITY); $can_disable = ($has_disable && !$is_self); - $can_welcome = ($is_admin && $user->canEstablishWebSessions()); + $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) + ->setSender($viewer) + ->setRecipient($user); + $can_welcome = $welcome_engine->canSendMail(); $curtain = $this->newCurtainView($user); $curtain->addAction( diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php index 14b1544b7f..73ee5fd740 100644 --- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php +++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php @@ -3,6 +3,13 @@ final class PhabricatorPeopleWelcomeController extends PhabricatorPeopleController { + public function shouldRequireAdmin() { + // You need to be an administrator to actually send welcome email, but + // we let anyone hit this page so they can get a nice error dialog + // explaining the issue. + return false; + } + public function handleRequest(AphrontRequest $request) { $admin = $this->getViewer(); @@ -14,22 +21,24 @@ final class PhabricatorPeopleWelcomeController return new Aphront404Response(); } - $profile_uri = '/p/'.$user->getUsername().'/'; + $id = $user->getID(); + $profile_uri = "/people/manage/{$id}/"; - if (!$user->canEstablishWebSessions()) { + $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) + ->setSender($admin) + ->setRecipient($user); + + try { + $welcome_engine->validateMail(); + } catch (PhabricatorPeopleMailEngineException $ex) { return $this->newDialog() - ->setTitle(pht('Not a Normal User')) - ->appendParagraph( - pht( - 'You can not send this user a welcome mail because they are not '. - 'a normal user and can not log in to the web interface. Special '. - 'users (like bots and mailing lists) are unable to establish web '. - 'sessions.')) + ->setTitle($ex->getTitle()) + ->appendParagraph($ex->getBody()) ->addCancelButton($profile_uri, pht('Done')); } if ($request->isFormPost()) { - $user->sendWelcomeEmail($admin); + $welcome_engine->sendMail(); return id(new AphrontRedirectResponse())->setURI($profile_uri); } diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php new file mode 100644 index 0000000000..8f8a22b12e --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -0,0 +1,61 @@ +sender = $sender; + return $this; + } + + final public function getSender() { + if (!$this->sender) { + throw new PhutilInvalidStateException('setSender'); + } + return $this->sender; + } + + final public function setRecipient(PhabricatorUser $recipient) { + $this->recipient = $recipient; + return $this; + } + + final public function getRecipient() { + if (!$this->recipient) { + throw new PhutilInvalidStateException('setRecipient'); + } + return $this->recipient; + } + + final public function canSendMail() { + try { + $this->validateMail(); + return true; + } catch (PhabricatorPeopleMailEngineException $ex) { + return false; + } + } + + final public function sendMail() { + $this->validateMail(); + $mail = $this->newMail(); + + $mail + ->setForceDelivery(true) + ->save(); + + return $mail; + } + + abstract public function validateMail(); + abstract protected function newMail(); + + + final protected function throwValidationException($title, $body) { + throw new PhabricatorPeopleMailEngineException($title, $body); + } + +} diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngineException.php b/src/applications/people/mail/PhabricatorPeopleMailEngineException.php new file mode 100644 index 0000000000..fa19bdfa98 --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleMailEngineException.php @@ -0,0 +1,24 @@ +title = $title; + $this->body = $body; + + parent::__construct(pht('%s: %s', $title, $body)); + } + + public function getTitle() { + return $this->title; + } + + public function getBody() { + return $this->body; + } + +} diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php new file mode 100644 index 0000000000..b52de9d519 --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -0,0 +1,83 @@ +getSender(); + $recipient = $this->getRecipient(); + + if (!$sender->getIsAdmin()) { + $this->throwValidationException( + pht('Not an Administrator'), + pht( + 'You can not send welcome mail because you are not an '. + 'administrator. Only administrators may send welcome mail.')); + } + + if ($recipient->getIsDisabled()) { + $this->throwValidationException( + pht('User is Disabled'), + pht( + 'You can not send welcome mail to this user because their account '. + 'is disabled.')); + } + + if (!$recipient->canEstablishWebSessions()) { + $this->throwValidationException( + pht('Not a Normal User'), + pht( + 'You can not send this user welcome mail because they are not '. + 'a normal user and can not log in to the web interface. Special '. + 'users (like bots and mailing lists) are unable to establish '. + 'web sessions.')); + } + } + + protected function newMail() { + $sender = $this->getSender(); + $recipient = $this->getRecipient(); + + $sender_username = $sender->getUserName(); + $sender_realname = $sender->getRealName(); + + $recipient_username = $recipient->getUserName(); + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + + $base_uri = PhabricatorEnv::getProductionURI('/'); + + $engine = new PhabricatorAuthSessionEngine(); + + $uri = $engine->getOneTimeLoginURI( + $recipient, + $recipient->loadPrimaryEmail(), + PhabricatorAuthSessionEngine::ONETIME_WELCOME); + + $body = pht( + "Welcome to Phabricator!\n\n". + "%s (%s) has created an account for you.\n\n". + " Username: %s\n\n". + "To login to Phabricator, follow this link and set a password:\n\n". + " %s\n\n". + "After you have set a password, you can login in the future by ". + "going here:\n\n". + " %s\n", + $sender_username, + $sender_realname, + $recipient_username, + $uri, + $base_uri); + + if (!$is_serious) { + $body .= sprintf( + "\n%s\n", + pht("Love,\nPhabricator")); + } + + return id(new PhabricatorMetaMTAMail()) + ->addTos(array($recipient->getPHID())) + ->setSubject(pht('[Phabricator] Welcome to Phabricator')) + ->setBody($body); + } + +} diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index bb3b52f5f5..e24024d96d 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -555,56 +555,6 @@ final class PhabricatorUser } } - public function sendWelcomeEmail(PhabricatorUser $admin) { - if (!$this->canEstablishWebSessions()) { - throw new Exception( - pht( - 'Can not send welcome mail to users who can not establish '. - 'web sessions!')); - } - - $admin_username = $admin->getUserName(); - $admin_realname = $admin->getRealName(); - $user_username = $this->getUserName(); - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - - $base_uri = PhabricatorEnv::getProductionURI('/'); - - $engine = new PhabricatorAuthSessionEngine(); - $uri = $engine->getOneTimeLoginURI( - $this, - $this->loadPrimaryEmail(), - PhabricatorAuthSessionEngine::ONETIME_WELCOME); - - $body = pht( - "Welcome to Phabricator!\n\n". - "%s (%s) has created an account for you.\n\n". - " Username: %s\n\n". - "To login to Phabricator, follow this link and set a password:\n\n". - " %s\n\n". - "After you have set a password, you can login in the future by ". - "going here:\n\n". - " %s\n", - $admin_username, - $admin_realname, - $user_username, - $uri, - $base_uri); - - if (!$is_serious) { - $body .= sprintf( - "\n%s\n", - pht("Love,\nPhabricator")); - } - - $mail = id(new PhabricatorMetaMTAMail()) - ->addTos(array($this->getPHID())) - ->setForceDelivery(true) - ->setSubject(pht('[Phabricator] Welcome to Phabricator')) - ->setBody($body) - ->saveAndSend(); - } - public function sendUsernameChangeEmail( PhabricatorUser $admin, $old_username) { From 7f950f520b05bbfbd97c8c735fb2ea0e39fe316c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 10:02:42 -0800 Subject: [PATCH 30/35] When password auth is not enabled, don't tell users to set a password in welcome email Summary: See PHI1027. Currently, the "Welcome" mail always tells users to set a password. This definitely isn't helpful if an install doesn't have password auth enabled. We can't necessarily guess what they're supposed to do, so just give them generic instructions ("set up your account"). Upcoming changes will give administrators more control over the mail content. Test Plan: Sent both versions of the mail, used `bin/mail show-outbound` to inspect them for correctness. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19990 --- .../PhabricatorPeopleWelcomeMailEngine.php | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php index b52de9d519..54c8f8fe30 100644 --- a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -38,9 +38,6 @@ final class PhabricatorPeopleWelcomeMailEngine $sender = $this->getSender(); $recipient = $this->getRecipient(); - $sender_username = $sender->getUserName(); - $sender_realname = $sender->getRealName(); - $recipient_username = $recipient->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); @@ -53,31 +50,54 @@ final class PhabricatorPeopleWelcomeMailEngine $recipient->loadPrimaryEmail(), PhabricatorAuthSessionEngine::ONETIME_WELCOME); - $body = pht( - "Welcome to Phabricator!\n\n". - "%s (%s) has created an account for you.\n\n". - " Username: %s\n\n". - "To login to Phabricator, follow this link and set a password:\n\n". - " %s\n\n". - "After you have set a password, you can login in the future by ". - "going here:\n\n". - " %s\n", - $sender_username, - $sender_realname, - $recipient_username, - $uri, - $base_uri); + $message = array(); + + $message[] = pht('Welcome to Phabricator!'); + + $message[] = pht( + '%s (%s) has created an account for you.', + $sender->getUsername(), + $sender->getRealName()); + + $message[] = pht( + ' Username: %s', + $recipient->getUsername()); + + // If password auth is enabled, give the user specific instructions about + // how to add a credential to their account. + + // If we aren't sure what they're supposed to be doing and passwords are + // not enabled, just give them generic instructions. + + $use_passwords = PhabricatorPasswordAuthProvider::getPasswordProvider(); + if ($use_passwords) { + $message[] = pht( + 'To log in to Phabricator, follow this link and set a password:'); + $message[] = pht(' %s', $uri); + $message[] = pht( + 'After you have set a password, you can log in to Phabricator in '. + 'the future by going here:'); + $message[] = pht(' %s', $base_uri); + } else { + $message[] = pht( + 'To log in to your account for the first time, follow this link:'); + $message[] = pht(' %s', $uri); + $message[] = pht( + 'After you set up your account, you can log in to Phabricator in '. + 'the future by going here:'); + $message[] = pht(' %s', $base_uri); + } if (!$is_serious) { - $body .= sprintf( - "\n%s\n", - pht("Love,\nPhabricator")); + $message[] = pht("Love,\nPhabricator"); } + $message = implode("\n\n", $message); + return id(new PhabricatorMetaMTAMail()) ->addTos(array($recipient->getPHID())) ->setSubject(pht('[Phabricator] Welcome to Phabricator')) - ->setBody($body); + ->setBody($message); } } From ab7aceaabf494aa3c17633d0ed511e4797218c92 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 10:16:05 -0800 Subject: [PATCH 31/35] Allow administrators to provide custom welcome text when welcoming users on the profile workflow Summary: See PHI1027. Currently, we allow you to customize invite email, but not most other types of email (approve, welcome). As a step forward, also allow welcome email to be customized with a message. I considered separating the custom text from the main text with something heavyhanded ("alice added this custom message:") or a beautiful ASCII art divider like one of these: https://www.asciiart.eu/art-and-design/dividers ...but nothing truly sung to me. This only works on the profile flow for now. I'm planning to let you set a default message. I may or may not let you customize from "Create New User", seems like the default message probably covers most of that. Probably won't touch `scripts/user/add_user.php` since that's not really exactly super supported. Test Plan: Sent mail with and without custom messages, reviewed it with `bin/mail show-outbound`. {F6137410} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19991 --- ...abricatorPeopleProfileManageController.php | 18 +++++--- .../PhabricatorPeopleWelcomeController.php | 43 ++++++++++++++----- .../PhabricatorPeopleWelcomeMailEngine.php | 20 ++++++++- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index 3e6c4ffa6e..e9faae3d62 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -155,6 +155,18 @@ final class PhabricatorPeopleProfileManageController $disable_name = pht('Disable User'); } + $curtain->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-envelope') + ->setName(pht('Send Welcome Email')) + ->setWorkflow(true) + ->setDisabled(!$can_welcome) + ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/'))); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER)); + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon($disable_icon) @@ -173,11 +185,7 @@ final class PhabricatorPeopleProfileManageController $curtain->addAction( id(new PhabricatorActionView()) - ->setIcon('fa-envelope') - ->setName(pht('Send Welcome Email')) - ->setWorkflow(true) - ->setDisabled(!$can_welcome) - ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/'))); + ->setType(PhabricatorActionView::TYPE_DIVIDER)); return $curtain; } diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php index 73ee5fd740..5ea4437d8a 100644 --- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php +++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php @@ -37,24 +37,45 @@ final class PhabricatorPeopleWelcomeController ->addCancelButton($profile_uri, pht('Done')); } + $v_message = $request->getStr('message'); + if ($request->isFormPost()) { + if (strlen($v_message)) { + $welcome_engine->setWelcomeMessage($v_message); + } + $welcome_engine->sendMail(); return id(new AphrontRedirectResponse())->setURI($profile_uri); } + $form = id(new AphrontFormView()) + ->setViewer($admin) + ->appendInstructions( + pht( + 'This workflow will send this user ("%s") a copy of the "Welcome to '. + 'Phabricator" email that users normally receive when their '. + 'accounts are created by an administrator.', + $user->getUsername())) + ->appendInstructions( + pht( + 'The email will contain a link that the user may use to log in '. + 'to their account. This link bypasses authentication requirements '. + 'and allows them to log in without credentials. Sending a copy of '. + 'this email can be useful if the original was lost or never sent.')) + ->appendInstructions( + pht( + 'The email will identify you as the sender. You may optionally '. + 'include additional text in the mail body by specifying it below.')) + ->appendControl( + id(new AphrontFormTextAreaControl()) + ->setName('message') + ->setLabel(pht('Custom Message')) + ->setValue($v_message)); + return $this->newDialog() ->setTitle(pht('Send Welcome Email')) - ->appendParagraph( - pht( - 'This will send the user another copy of the "Welcome to '. - 'Phabricator" email that users normally receive when their '. - 'accounts are created.')) - ->appendParagraph( - pht( - 'The email contains a link to log in to their account. Sending '. - 'another copy of the email can be useful if the original was lost '. - 'or never sent.')) - ->appendParagraph(pht('The email will identify you as the sender.')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendForm($form) ->addSubmitButton(pht('Send Email')) ->addCancelButton($profile_uri); } diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php index 54c8f8fe30..761703e1b5 100644 --- a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -3,6 +3,17 @@ final class PhabricatorPeopleWelcomeMailEngine extends PhabricatorPeopleMailEngine { + private $welcomeMessage; + + public function setWelcomeMessage($welcome_message) { + $this->welcomeMessage = $welcome_message; + return $this; + } + + public function getWelcomeMessage() { + return $this->welcomeMessage; + } + public function validateMail() { $sender = $this->getSender(); $recipient = $this->getRecipient(); @@ -88,8 +99,13 @@ final class PhabricatorPeopleWelcomeMailEngine $message[] = pht(' %s', $base_uri); } - if (!$is_serious) { - $message[] = pht("Love,\nPhabricator"); + $custom_body = $this->getWelcomeMessage(); + if (strlen($custom_body)) { + $message[] = $custom_body; + } else { + if (!$is_serious) { + $message[] = pht("Love,\nPhabricator"); + } } $message = implode("\n\n", $message); From 2c713b2d25fdde05297b2f94c445ffae9250b4ea Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 10:47:13 -0800 Subject: [PATCH 32/35] Add "Auth Messages" to support customizing onboarding/welcome flows Summary: Ref T13222. Long ago, we had a Config option (`welcome.html`) to let you dump HTML onto the login screen, but this was relatively hard to use and not good from a security perspective. In some cases this was obsoleted by Dashboards, but there's at least some remaining set of use cases for actual login instructions on the login screen. For example, WMF has some guidance on //which// SSO mechanism to use based on what types of account you have. On `secure`, users assume they can register by clicking "Log In With GitHub" or whatever, and it might reduce frustration to tell them upfront that registration is closed. Some other types of auth messaging could also either use customization or defaults (e.g., the invite/welcome/approve mail). We could do this with a bunch of Config options, but I'd generally like to move to a world where there's less stuff in Config and more configuration is contextual. I think it tends to be easier to use, and we get a lot of fringe benefits (granular permissions, API, normal transaction logs, more abililty to customize workflows and provide contextual help/hints, etc). Here, for example, we can provide a remarkup preview, which would be trickier with Config. This does not actually do anything yet. Test Plan: {F6137541} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19992 --- .../20190117.authmessage.01.message.sql | 8 ++ .../20190117.authmessage.02.xaction.sql | 19 +++ src/__phutil_library_map__.php | 37 ++++++ .../PhabricatorAuthApplication.php | 10 ++ .../PhabricatorAuthProviderController.php | 14 +++ .../PhabricatorAuthMessageController.php | 11 ++ .../PhabricatorAuthMessageEditController.php | 31 +++++ .../PhabricatorAuthMessageListController.php | 77 ++++++++++++ .../PhabricatorAuthMessageViewController.php | 104 ++++++++++++++++ ...icatorAuthFactorProviderEditController.php | 2 +- .../PhabricatorAuthMessageEditEngine.php | 108 ++++++++++++++++ .../editor/PhabricatorAuthMessageEditor.php | 22 ++++ .../PhabricatorAuthLoginMessageType.php | 18 +++ .../message/PhabricatorAuthMessageType.php | 32 +++++ .../PhabricatorAuthWelcomeMailMessageType.php | 18 +++ .../phid/PhabricatorAuthMessagePHIDType.php | 32 +++++ .../query/PhabricatorAuthMessageQuery.php | 83 +++++++++++++ ...PhabricatorAuthMessageTransactionQuery.php | 10 ++ .../auth/storage/PhabricatorAuthMessage.php | 116 ++++++++++++++++++ .../PhabricatorAuthMessageTransaction.php | 18 +++ .../PhabricatorAuthMessageTextTransaction.php | 39 ++++++ .../PhabricatorAuthMessageTransactionType.php | 4 + 22 files changed, 812 insertions(+), 1 deletion(-) create mode 100644 resources/sql/autopatches/20190117.authmessage.01.message.sql create mode 100644 resources/sql/autopatches/20190117.authmessage.02.xaction.sql create mode 100644 src/applications/auth/controller/message/PhabricatorAuthMessageController.php create mode 100644 src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php create mode 100644 src/applications/auth/controller/message/PhabricatorAuthMessageListController.php create mode 100644 src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php create mode 100644 src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php create mode 100644 src/applications/auth/editor/PhabricatorAuthMessageEditor.php create mode 100644 src/applications/auth/message/PhabricatorAuthLoginMessageType.php create mode 100644 src/applications/auth/message/PhabricatorAuthMessageType.php create mode 100644 src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php create mode 100644 src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthMessageQuery.php create mode 100644 src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php create mode 100644 src/applications/auth/storage/PhabricatorAuthMessage.php create mode 100644 src/applications/auth/storage/PhabricatorAuthMessageTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php diff --git a/resources/sql/autopatches/20190117.authmessage.01.message.sql b/resources/sql/autopatches/20190117.authmessage.01.message.sql new file mode 100644 index 0000000000..9f4afa2646 --- /dev/null +++ b/resources/sql/autopatches/20190117.authmessage.01.message.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_message ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + messageKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + messageText LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190117.authmessage.02.xaction.sql b/resources/sql/autopatches/20190117.authmessage.02.xaction.sql new file mode 100644 index 0000000000..944de129a0 --- /dev/null +++ b/resources/sql/autopatches/20190117.authmessage.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_messagetransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 82a639f61f..8ee28d39a7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2246,6 +2246,7 @@ phutil_register_library_map(array( 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', 'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php', + 'PhabricatorAuthLoginMessageType' => 'applications/auth/message/PhabricatorAuthLoginMessageType.php', 'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php', 'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php', 'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php', @@ -2261,6 +2262,20 @@ phutil_register_library_map(array( 'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php', 'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php', 'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php', + 'PhabricatorAuthMessage' => 'applications/auth/storage/PhabricatorAuthMessage.php', + 'PhabricatorAuthMessageController' => 'applications/auth/controller/message/PhabricatorAuthMessageController.php', + 'PhabricatorAuthMessageEditController' => 'applications/auth/controller/message/PhabricatorAuthMessageEditController.php', + 'PhabricatorAuthMessageEditEngine' => 'applications/auth/editor/PhabricatorAuthMessageEditEngine.php', + 'PhabricatorAuthMessageEditor' => 'applications/auth/editor/PhabricatorAuthMessageEditor.php', + 'PhabricatorAuthMessageListController' => 'applications/auth/controller/message/PhabricatorAuthMessageListController.php', + 'PhabricatorAuthMessagePHIDType' => 'applications/auth/phid/PhabricatorAuthMessagePHIDType.php', + 'PhabricatorAuthMessageQuery' => 'applications/auth/query/PhabricatorAuthMessageQuery.php', + 'PhabricatorAuthMessageTextTransaction' => 'applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php', + 'PhabricatorAuthMessageTransaction' => 'applications/auth/storage/PhabricatorAuthMessageTransaction.php', + 'PhabricatorAuthMessageTransactionQuery' => 'applications/auth/query/PhabricatorAuthMessageTransactionQuery.php', + 'PhabricatorAuthMessageTransactionType' => 'applications/auth/xaction/PhabricatorAuthMessageTransactionType.php', + 'PhabricatorAuthMessageType' => 'applications/auth/message/PhabricatorAuthMessageType.php', + 'PhabricatorAuthMessageViewController' => 'applications/auth/controller/message/PhabricatorAuthMessageViewController.php', 'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php', 'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php', 'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php', @@ -2337,6 +2352,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', + 'PhabricatorAuthWelcomeMailMessageType' => 'applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php', 'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php', 'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php', 'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php', @@ -7920,6 +7936,7 @@ phutil_register_library_map(array( 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthLoginHandler' => 'Phobject', + 'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', @@ -7935,6 +7952,25 @@ phutil_register_library_map(array( 'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorAuthMessage' => array( + 'PhabricatorAuthDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorAuthMessageController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthMessageEditController' => 'PhabricatorAuthMessageController', + 'PhabricatorAuthMessageEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorAuthMessageEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthMessageListController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthMessagePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthMessageTextTransaction' => 'PhabricatorAuthMessageTransactionType', + 'PhabricatorAuthMessageTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorAuthMessageTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthMessageTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorAuthMessageType' => 'Phobject', + 'PhabricatorAuthMessageViewController' => 'PhabricatorAuthMessageController', 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController', 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', @@ -8030,6 +8066,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', + 'PhabricatorAuthWelcomeMailMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAutoEventListener' => 'PhabricatorEventListener', 'PhabricatorBadgesApplication' => 'PhabricatorApplication', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 62f86a00f8..2c36e935ee 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -94,6 +94,16 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { '(?P[1-9]\d*)/' => 'PhabricatorAuthFactorProviderViewController', ), + + 'message/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorAuthMessageListController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorAuthMessageEditController', + '(?P[1-9]\d*)/' => + 'PhabricatorAuthMessageViewController', + ), + ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php index 2fb4386ef4..2668da1218 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthProviderController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php @@ -31,6 +31,20 @@ abstract class PhabricatorAuthProviderController ->setHref($this->getApplicationURI('mfa/')) ->setIcon('fa-mobile')); + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setName(pht('Onboarding')) + ->setType(PHUIListItemView::TYPE_LABEL)); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setKey('message') + ->setName(pht('Customize Messages')) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($this->getApplicationURI('message/')) + ->setIcon('fa-commenting-o')); + + $nav->selectFilter(null); return $nav; diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageController.php new file mode 100644 index 0000000000..98bb908cfe --- /dev/null +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageController.php @@ -0,0 +1,11 @@ +addTextCrumb(pht('Messages'), $this->getApplicationURI('message/')); + } + +} diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php new file mode 100644 index 0000000000..3cb4a4b0af --- /dev/null +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php @@ -0,0 +1,31 @@ +requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $engine = id(new PhabricatorAuthMessageEditEngine()) + ->setController($this); + + $id = $request->getURIData('id'); + if (!$id) { + $message_key = $request->getStr('messageKey'); + + $message_types = PhabricatorAuthMessageType::getAllMessageTypes(); + $message_type = idx($message_types, $message_key); + if (!$message_type) { + return new Aphront404Response(); + } + + $engine + ->addContextParameter('messageKey', $message_key) + ->setMessageType($message_type); + } + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php new file mode 100644 index 0000000000..a3c518ab36 --- /dev/null +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php @@ -0,0 +1,77 @@ +getViewer(); + + $can_manage = $this->hasApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $types = PhabricatorAuthMessageType::getAllMessageTypes(); + + $messages = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->execute(); + $messages = mpull($messages, null, 'getMessageKey'); + + $list = new PHUIObjectItemListView(); + foreach ($types as $type) { + $message = idx($messages, $type->getMessageTypeKey()); + if ($message) { + $href = $message->getURI(); + $name = $message->getMessageTypeDisplayName(); + } else { + $href = '/auth/message/edit/?messageKey='.$type->getMessageTypeKey(); + $name = $type->getDisplayName(); + } + + $item = id(new PHUIObjectItemView()) + ->setHeader($name) + ->setHref($href) + ->addAttribute($type->getShortDescription()); + + if ($message) { + $item->addIcon('fa-circle', pht('Customized')); + } else { + $item->addIcon('fa-circle-o grey', pht('Default')); + } + + $list->addItem($item); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Messages')) + ->setBorder(true); + + $list->setFlush(true); + $list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Auth Messages')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + + $title = pht('Auth Messages'); + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-commenting-o'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $list, + )); + + $nav = $this->newNavigation() + ->setCrumbs($crumbs) + ->appendChild($view); + + $nav->selectFilter('message'); + + return $this->newPage() + ->setTitle($title) + ->appendChild($nav); + } + +} diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php new file mode 100644 index 0000000000..db7e7e65e0 --- /dev/null +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php @@ -0,0 +1,104 @@ +getViewer(); + + $this->requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $message = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$message) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($message->getObjectName()) + ->setBorder(true); + + $header = $this->buildHeaderView($message); + $properties = $this->buildPropertiesView($message); + $curtain = $this->buildCurtain($message); + + $timeline = $this->buildTransactionTimeline( + $message, + new PhabricatorAuthMessageTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $timeline, + )) + ->addPropertySection(pht('Details'), $properties); + + return $this->newPage() + ->setTitle($message->getMessageTypeDisplayName()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs( + array( + $message->getPHID(), + )) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorAuthMessage $message) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($message->getMessageTypeDisplayName()); + + return $view; + } + + private function buildPropertiesView(PhabricatorAuthMessage $message) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $view->addProperty( + pht('Description'), + $message->getMessageType()->getShortDescription()); + + $view->addSectionHeader( + pht('Message Preview'), + PHUIPropertyListView::ICON_SUMMARY); + + $view->addTextContent( + new PHUIRemarkupView($viewer, $message->getMessageText())); + + return $view; + } + + private function buildCurtain(PhabricatorAuthMessage $message) { + $viewer = $this->getViewer(); + $id = $message->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $message, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($message); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Message')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("message/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php index 5108eaaefd..0dde1b3c6f 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php @@ -23,7 +23,7 @@ final class PhabricatorAuthFactorProviderEditController $engine ->addContextParameter('providerFactorKey', $factor_key) ->setProviderFactor($factor); - } + } return $engine->buildResponse(); } diff --git a/src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php b/src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php new file mode 100644 index 0000000000..0a9aa32de4 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php @@ -0,0 +1,108 @@ +messageType = $type; + return $this; + } + + public function getMessageType() { + return $this->messageType; + } + + protected function newEditableObject() { + $type = $this->getMessageType(); + + if ($type) { + $message = PhabricatorAuthMessage::initializeNewMessage($type); + } else { + $message = new PhabricatorAuthMessage(); + } + + return $message; + } + + protected function newObjectQuery() { + return new PhabricatorAuthMessageQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Auth Message'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Auth Message'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Auth Message'); + } + + protected function getObjectEditShortText($object) { + return $object->getObjectName(); + } + + protected function getObjectCreateShortText() { + return pht('Create Auth Message'); + } + + protected function getObjectName() { + return pht('Auth Message'); + } + + protected function getEditorURI() { + return '/auth/message/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/auth/message/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AuthManageProvidersCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorRemarkupEditField()) + ->setKey('messageText') + ->setTransactionType( + PhabricatorAuthMessageTextTransaction::TRANSACTIONTYPE) + ->setLabel(pht('Message Text')) + ->setDescription(pht('Custom text for the message.')) + ->setValue($object->getMessageText()), + ); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthMessageEditor.php b/src/applications/auth/editor/PhabricatorAuthMessageEditor.php new file mode 100644 index 0000000000..56e8e716cd --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthMessageEditor.php @@ -0,0 +1,22 @@ +getPhobjectClassConstant('MESSAGEKEY', 64); + } + + final public static function getAllMessageTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getMessageTypeKey') + ->execute(); + } + + final public static function newFromKey($key) { + $types = self::getAllMessageTypes(); + + if (empty($types[$key])) { + throw new Exception( + pht( + 'No message type exists with key "%s".', + $key)); + } + + return clone $types[$key]; + } + + abstract public function getDisplayName(); + +} diff --git a/src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php b/src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php new file mode 100644 index 0000000000..fe4b25cbb2 --- /dev/null +++ b/src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php @@ -0,0 +1,18 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withMessageKeys(array $keys) { + $this->messageKeys = $keys; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthMessage(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->messageKeys !== null) { + $where[] = qsprintf( + $conn, + 'messageKey IN (%Ls)', + $this->messageKeys); + } + + return $where; + } + + protected function willFilterPage(array $messages) { + $message_types = PhabricatorAuthMessageType::getAllMessageTypes(); + + foreach ($messages as $key => $message) { + $message_key = $message->getMessageKey(); + + $message_type = idx($message_types, $message_key); + if (!$message_type) { + unset($messages[$key]); + $this->didRejectResult($message); + continue; + } + + $message->attachMessageType($message_type); + } + + return $messages; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php new file mode 100644 index 0000000000..0b2ce79db3 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php @@ -0,0 +1,10 @@ +setMessageKey($type->getMessageTypeKey()) + ->attachMessageType($type); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'messageKey' => 'text64', + 'messageText' => 'text', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_type' => array( + 'columns' => array('messageKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorAuthMessagePHIDType::TYPECONST; + } + + public function getObjectName() { + return pht('Auth Message %d', $this->getID()); + } + + public function getURI() { + return urisprintf('/auth/message/%s', $this->getID()); + } + + public function attachMessageType(PhabricatorAuthMessageType $type) { + $this->messageType = $type; + return $this; + } + + public function getMessageType() { + return $this->assertAttached($this->messageType); + } + + public function getMessageTypeDisplayName() { + return $this->getMessageType()->getDisplayName(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + default: + return false; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + // Even if an install doesn't allow public users, you can still view + // auth messages: otherwise, we can't do things like show you + // guidance on the login screen. + return true; + default: + return false; + } + } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorAuthMessageEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorAuthMessageTransaction(); + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + +} diff --git a/src/applications/auth/storage/PhabricatorAuthMessageTransaction.php b/src/applications/auth/storage/PhabricatorAuthMessageTransaction.php new file mode 100644 index 0000000000..407a9735c6 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthMessageTransaction.php @@ -0,0 +1,18 @@ +getMessageText(); + } + + public function applyInternalEffects($object, $value) { + $object->setMessageText($value); + } + + public function getTitle() { + return pht( + '%s updated the message text.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function getMailDiffSectionHeader() { + return pht('CHANGES TO MESSAGE'); + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($this->getOldValue()) + ->setNewText($this->getNewValue()); + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php new file mode 100644 index 0000000000..eeb1b350f3 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php @@ -0,0 +1,4 @@ + Date: Thu, 17 Jan 2019 18:29:42 -0800 Subject: [PATCH 33/35] Show the customized "Login" message on the login screen Summary: Depends on D19992. Ref T13222. If administrators provide a custom login message, show it on the login screen. Test Plan: {F6137930} - Viewed login screen with and without a custom message. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19994 --- resources/celerity/map.php | 6 ++--- .../PhabricatorAuthLoginController.php | 1 + .../PhabricatorAuthStartController.php | 24 +++++++++++++++++++ .../auth/storage/PhabricatorAuthMessage.php | 16 +++++++++++++ webroot/rsrc/css/application/auth/auth.css | 9 +++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6b170821a6..5764623ad5 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '7fa376a9', + 'core.pkg.css' => 'e94cc920', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -36,7 +36,7 @@ return array( 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', 'rsrc/css/aphront/typeahead.css' => '8779483d', 'rsrc/css/application/almanac/almanac.css' => '2e050f4f', - 'rsrc/css/application/auth/auth.css' => '9f6e4ed8', + 'rsrc/css/application/auth/auth.css' => 'add92fd8', 'rsrc/css/application/base/main-menu-view.css' => '8e2d9a28', 'rsrc/css/application/base/notification-menu.css' => 'e6962e89', 'rsrc/css/application/base/phui-theme.css' => '35883b37', @@ -524,7 +524,7 @@ return array( 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', 'application-search-view-css' => '0f7c06d8', - 'auth-css' => '9f6e4ed8', + 'auth-css' => 'add92fd8', 'bulk-job-css' => '73af99f5', 'conduit-api-css' => 'ce2cfc41', 'config-options-css' => '16c920ae', diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index 39b6318481..54649a6a69 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -17,6 +17,7 @@ final class PhabricatorAuthLoginController if ($parameter_name == 'code') { return true; } + return parent::shouldAllowRestrictedParameter($parameter_name); } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 9af8f25bc7..3dc8c61a51 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -194,6 +194,8 @@ final class PhabricatorAuthStartController $invite_message = $this->renderInviteHeader($invite); } + $custom_message = $this->newCustomStartMessage(); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Login')); $crumbs->setBorder(true); @@ -202,6 +204,7 @@ final class PhabricatorAuthStartController $view = array( $header, $invite_message, + $custom_message, $out, ); @@ -305,4 +308,25 @@ final class PhabricatorAuthStartController ->setURI($auto_uri); } + private function newCustomStartMessage() { + $viewer = $this->getViewer(); + + $text = PhabricatorAuthMessage::loadMessageText( + $viewer, + PhabricatorAuthLoginMessageType::MESSAGEKEY); + + if (!strlen($text)) { + return null; + } + + $remarkup_view = new PHUIRemarkupView($viewer, $text); + + return phutil_tag( + 'div', + array( + 'class' => 'auth-custom-message', + ), + $remarkup_view); + } + } diff --git a/src/applications/auth/storage/PhabricatorAuthMessage.php b/src/applications/auth/storage/PhabricatorAuthMessage.php index ff79cb6990..a1aa928684 100644 --- a/src/applications/auth/storage/PhabricatorAuthMessage.php +++ b/src/applications/auth/storage/PhabricatorAuthMessage.php @@ -61,6 +61,22 @@ final class PhabricatorAuthMessage return $this->getMessageType()->getDisplayName(); } + public static function loadMessageText( + PhabricatorUser $viewer, + $message_key) { + + $message = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withMessageKeys(array($message_key)) + ->executeOne(); + + if (!$message) { + return null; + } + + return $message->getMessageText(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css index a5d326430e..687aaf2bb4 100644 --- a/webroot/rsrc/css/application/auth/auth.css +++ b/webroot/rsrc/css/application/auth/auth.css @@ -55,3 +55,12 @@ .auth-account-view-account-uri { word-break: break-word; } + +.auth-custom-message { + margin: 32px auto 64px; + max-width: 548px; + background: #fff; + padding: 16px; + border: 1px solid {$lightblueborder}; + border-radius: 4px; +} From 6bb31de30596147444dcc844492a964109c95e2c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Jan 2019 19:05:45 -0800 Subject: [PATCH 34/35] Use the customizable "Welcome Mail" message in welcome mail Summary: Depends on D19994. See PHI1027. If an install has customized the "Welcome Mail" message, include it in welcome mail. A special custom message from the profile screen overrides it, if provided. (I fiddled with putting the custom message as "placeholder" text in the remarkup area as a hint, but newlines in "placeholder" text appear to have issues in Safari and Firefox. I think this is probably reasonably clear as-is.) Make both render remarkup-into-text so things like links work properly, as it's reasonably likely that installs will want to link to things. Test Plan: - With custom "Welcome Mail" text, sent mail with no custom override (got custom text) and a custom override (got overridden text). - Linked to some stuff, got sensible links in the mail (`bin/mail show-outbound`). Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19995 --- .../auth/storage/PhabricatorAuthMessage.php | 14 +++++--- .../PhabricatorPeopleWelcomeController.php | 26 ++++++++++---- .../mail/PhabricatorPeopleMailEngine.php | 11 ++++++ .../PhabricatorPeopleWelcomeMailEngine.php | 36 +++++++++++++------ 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/applications/auth/storage/PhabricatorAuthMessage.php b/src/applications/auth/storage/PhabricatorAuthMessage.php index a1aa928684..00f5fbfbaa 100644 --- a/src/applications/auth/storage/PhabricatorAuthMessage.php +++ b/src/applications/auth/storage/PhabricatorAuthMessage.php @@ -61,14 +61,20 @@ final class PhabricatorAuthMessage return $this->getMessageType()->getDisplayName(); } + public static function loadMessage( + PhabricatorUser $viewer, + $message_key) { + return id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withMessageKeys(array($message_key)) + ->executeOne(); + } + public static function loadMessageText( PhabricatorUser $viewer, $message_key) { - $message = id(new PhabricatorAuthMessageQuery()) - ->setViewer($viewer) - ->withMessageKeys(array($message_key)) - ->executeOne(); + $message = self::loadMessage($viewer, $message_key); if (!$message) { return null; diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php index 5ea4437d8a..3fb75265ff 100644 --- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php +++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php @@ -48,26 +48,38 @@ final class PhabricatorPeopleWelcomeController return id(new AphrontRedirectResponse())->setURI($profile_uri); } + $default_message = PhabricatorAuthMessage::loadMessage( + $admin, + PhabricatorAuthWelcomeMailMessageType::MESSAGEKEY); + if (strlen($default_message->getMessageText())) { + $message_instructions = pht( + 'The email will identify you as the sender. You may optionally '. + 'replace the [[ %s | default custom mail body ]] with different text '. + 'by providing a message below.', + $default_message->getURI()); + } else { + $message_instructions = pht( + 'The email will identify you as the sender. You may optionally '. + 'include additional text in the mail body by specifying it below.'); + } + $form = id(new AphrontFormView()) ->setViewer($admin) - ->appendInstructions( + ->appendRemarkupInstructions( pht( 'This workflow will send this user ("%s") a copy of the "Welcome to '. 'Phabricator" email that users normally receive when their '. 'accounts are created by an administrator.', $user->getUsername())) - ->appendInstructions( + ->appendRemarkupInstructions( pht( 'The email will contain a link that the user may use to log in '. 'to their account. This link bypasses authentication requirements '. 'and allows them to log in without credentials. Sending a copy of '. 'this email can be useful if the original was lost or never sent.')) - ->appendInstructions( - pht( - 'The email will identify you as the sender. You may optionally '. - 'include additional text in the mail body by specifying it below.')) + ->appendRemarkupInstructions($message_instructions) ->appendControl( - id(new AphrontFormTextAreaControl()) + id(new PhabricatorRemarkupControl()) ->setName('message') ->setLabel(pht('Custom Message')) ->setValue($v_message)); diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php index 8f8a22b12e..281009341d 100644 --- a/src/applications/people/mail/PhabricatorPeopleMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -58,4 +58,15 @@ abstract class PhabricatorPeopleMailEngine throw new PhabricatorPeopleMailEngineException($title, $body); } + final protected function newRemarkupText($text) { + $recipient = $this->getRecipient(); + + $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) + ->setConfig('viewer', $recipient) + ->setConfig('uri.base', PhabricatorEnv::getProductionURI('/')) + ->setMode(PhutilRemarkupEngine::MODE_TEXT); + + return $engine->markupText($text); + } + } diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php index 761703e1b5..ff7ee71272 100644 --- a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -49,9 +49,6 @@ final class PhabricatorPeopleWelcomeMailEngine $sender = $this->getSender(); $recipient = $this->getRecipient(); - $recipient_username = $recipient->getUserName(); - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $base_uri = PhabricatorEnv::getProductionURI('/'); $engine = new PhabricatorAuthSessionEngine(); @@ -99,13 +96,9 @@ final class PhabricatorPeopleWelcomeMailEngine $message[] = pht(' %s', $base_uri); } - $custom_body = $this->getWelcomeMessage(); - if (strlen($custom_body)) { - $message[] = $custom_body; - } else { - if (!$is_serious) { - $message[] = pht("Love,\nPhabricator"); - } + $message_body = $this->newBody(); + if ($message_body !== null) { + $message[] = $message_body; } $message = implode("\n\n", $message); @@ -116,4 +109,27 @@ final class PhabricatorPeopleWelcomeMailEngine ->setBody($message); } + private function newBody() { + $recipient = $this->getRecipient(); + + $custom_body = $this->getWelcomeMessage(); + if (strlen($custom_body)) { + return $this->newRemarkupText($custom_body); + } + + $default_body = PhabricatorAuthMessage::loadMessageText( + $recipient, + PhabricatorAuthWelcomeMailMessageType::MESSAGEKEY); + if (strlen($default_body)) { + return $this->newRemarkupText($default_body); + } + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + if (!$is_serious) { + return pht("Love,\nPhabricator"); + } + + return null; + } + } From e6ca2b998fd4ba1d197322228f408b2461314659 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 18 Jan 2019 05:36:27 -0800 Subject: [PATCH 35/35] Allow Conduit method call logs to be exported with the standard export pipeline Summary: See PHI1026. Allow installs to export Conduit call logs to a flat format. Also, add date range queries. Test Plan: - Exported some call logs. - Filtered logs by date. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19996 --- .../PhabricatorConduitApplication.php | 22 +++--- .../query/PhabricatorConduitLogQuery.php | 22 ++++++ .../PhabricatorConduitLogSearchEngine.php | 68 +++++++++++++++++++ 3 files changed, 103 insertions(+), 9 deletions(-) diff --git a/src/applications/conduit/application/PhabricatorConduitApplication.php b/src/applications/conduit/application/PhabricatorConduitApplication.php index a036860aad..f9dae71a82 100644 --- a/src/applications/conduit/application/PhabricatorConduitApplication.php +++ b/src/applications/conduit/application/PhabricatorConduitApplication.php @@ -46,16 +46,20 @@ final class PhabricatorConduitApplication extends PhabricatorApplication { public function getRoutes() { return array( '/conduit/' => array( - '(?:query/(?P[^/]+)/)?' => 'PhabricatorConduitListController', + $this->getQueryRoutePattern() => 'PhabricatorConduitListController', 'method/(?P[^/]+)/' => 'PhabricatorConduitConsoleController', - 'log/(?:query/(?P[^/]+)/)?' => - 'PhabricatorConduitLogController', - 'log/view/(?P[^/]+)/' => 'PhabricatorConduitLogController', - 'token/' => 'PhabricatorConduitTokenController', - 'token/edit/(?:(?P\d+)/)?' => - 'PhabricatorConduitTokenEditController', - 'token/terminate/(?:(?P\d+)/)?' => - 'PhabricatorConduitTokenTerminateController', + 'log/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorConduitLogController', + 'view/(?P[^/]+)/' => 'PhabricatorConduitLogController', + ), + 'token/' => array( + '' => 'PhabricatorConduitTokenController', + 'edit/(?:(?P\d+)/)?' => + 'PhabricatorConduitTokenEditController', + 'terminate/(?:(?P\d+)/)?' => + 'PhabricatorConduitTokenTerminateController', + ), 'login/' => 'PhabricatorConduitTokenHandshakeController', ), '/api/(?P[^/]+)' => 'PhabricatorConduitAPIController', diff --git a/src/applications/conduit/query/PhabricatorConduitLogQuery.php b/src/applications/conduit/query/PhabricatorConduitLogQuery.php index 0dacd9406d..6f0f6131d8 100644 --- a/src/applications/conduit/query/PhabricatorConduitLogQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitLogQuery.php @@ -6,6 +6,8 @@ final class PhabricatorConduitLogQuery private $callerPHIDs; private $methods; private $methodStatuses; + private $epochMin; + private $epochMax; public function withCallerPHIDs(array $phids) { $this->callerPHIDs = $phids; @@ -22,6 +24,12 @@ final class PhabricatorConduitLogQuery return $this; } + public function withEpochBetween($epoch_min, $epoch_max) { + $this->epochMin = $epoch_min; + $this->epochMax = $epoch_max; + return $this; + } + public function newResultObject() { return new PhabricatorConduitMethodCallLog(); } @@ -72,6 +80,20 @@ final class PhabricatorConduitLogQuery $method_names); } + if ($this->epochMin !== null) { + $where[] = qsprintf( + $conn, + 'dateCreated >= %d', + $this->epochMin); + } + + if ($this->epochMax !== null) { + $where[] = qsprintf( + $conn, + 'dateCreated <= %d', + $this->epochMax); + } + return $where; } diff --git a/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php index 20d034168d..ab86ae6669 100644 --- a/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php +++ b/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php @@ -34,6 +34,12 @@ final class PhabricatorConduitLogSearchEngine $query->withMethodStatuses($map['statuses']); } + if ($map['epochMin'] || $map['epochMax']) { + $query->withEpochBetween( + $map['epochMin'], + $map['epochMax']); + } + return $query; } @@ -55,6 +61,12 @@ final class PhabricatorConduitLogSearchEngine ->setDescription( pht('Find calls to stable, unstable, or deprecated methods.')) ->setOptions(ConduitAPIMethod::getMethodStatusMap()), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Called After')) + ->setKey('epochMin'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Called Before')) + ->setKey('epochMax'), ); } @@ -106,6 +118,62 @@ final class PhabricatorConduitLogSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } + protected function newExportFields() { + $viewer = $this->requireViewer(); + + return array( + id(new PhabricatorPHIDExportField()) + ->setKey('callerPHID') + ->setLabel(pht('Caller PHID')), + id(new PhabricatorStringExportField()) + ->setKey('caller') + ->setLabel(pht('Caller')), + id(new PhabricatorStringExportField()) + ->setKey('method') + ->setLabel(pht('Method')), + id(new PhabricatorIntExportField()) + ->setKey('duration') + ->setLabel(pht('Call Duration (us)')), + id(new PhabricatorStringExportField()) + ->setKey('error') + ->setLabel(pht('Error')), + ); + } + + protected function newExportData(array $logs) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($logs as $log) { + if ($log->getCallerPHID()) { + $phids[] = $log->getCallerPHID(); + } + } + $handles = $viewer->loadHandles($phids); + + $export = array(); + foreach ($logs as $log) { + $caller_phid = $log->getCallerPHID(); + if ($caller_phid) { + $caller_name = $handles[$caller_phid]->getName(); + } else { + $caller_name = null; + } + + $map = array( + 'callerPHID' => $caller_phid, + 'caller' => $caller_name, + 'method' => $log->getMethod(), + 'duration' => (int)$log->getDuration(), + 'error' => $log->getError(), + ); + + $export[] = $map; + } + + return $export; + } + protected function renderResultList( array $logs, PhabricatorSavedQuery $query,