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/resources/celerity/map.php b/resources/celerity/map.php index 316357bb7a..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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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/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/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/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/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); } } 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 07ecac9a17..8ee28d39a7 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', @@ -2190,6 +2189,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 +2207,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', @@ -2234,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', @@ -2249,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', @@ -2277,6 +2304,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', @@ -2324,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', @@ -3319,7 +3348,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', @@ -3348,7 +3376,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', @@ -3373,8 +3400,11 @@ 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', 'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php', 'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php', 'PhabricatorMailEmailMessage' => 'applications/metamta/message/PhabricatorMailEmailMessage.php', @@ -3382,14 +3412,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', @@ -3400,19 +3423,28 @@ 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', 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php', 'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.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', + 'PhabricatorMailSMSMessage' => 'applications/metamta/message/PhabricatorMailSMSMessage.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', + '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', @@ -3747,7 +3779,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', @@ -3799,6 +3830,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', @@ -3826,14 +3859,15 @@ 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', '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', 'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', @@ -3842,7 +3876,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', @@ -4885,7 +4918,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', @@ -5721,7 +5756,6 @@ phutil_register_library_map(array( 'ConduitWildParameterType' => 'ConduitParameterType', 'ConpherenceColumnViewController' => 'ConpherenceController', 'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod', - 'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions', 'ConpherenceConstants' => 'Phobject', 'ConpherenceController' => 'PhabricatorController', 'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', @@ -7834,6 +7868,7 @@ phutil_register_library_map(array( 'PhabricatorAuthAccountView' => 'AphrontView', 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthAuthFactorProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthCSRFEngine' => 'Phobject', 'PhabricatorAuthChallenge' => array( @@ -7854,6 +7889,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', @@ -7884,6 +7936,7 @@ phutil_register_library_map(array( 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthLoginHandler' => 'Phobject', + 'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', @@ -7899,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', @@ -7930,11 +8002,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', @@ -7993,6 +8066,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', + 'PhabricatorAuthWelcomeMailMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAutoEventListener' => 'PhabricatorEventListener', 'PhabricatorBadgesApplication' => 'PhabricatorApplication', @@ -9131,7 +9205,6 @@ phutil_register_library_map(array( 'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorLegalpadApplication' => 'PhabricatorApplication', - 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType', 'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase', @@ -9160,7 +9233,6 @@ phutil_register_library_map(array( 'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType', 'PhabricatorMacroAudioController' => 'PhabricatorMacroController', 'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType', - 'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMacroController' => 'PhabricatorController', 'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorMacroDisableController' => 'PhabricatorMacroController', @@ -9185,8 +9257,11 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', + 'PhabricatorMailAdapter' => 'Phobject', + 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailAttachment' => 'Phobject', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', + 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', 'PhabricatorMailEmailHeraldField' => 'HeraldField', 'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorMailEmailMessage' => 'PhabricatorMailExternalMessage', @@ -9194,14 +9269,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', @@ -9212,19 +9280,28 @@ phutil_register_library_map(array( 'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorMailMessageEngine' => 'Phobject', 'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter', 'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction', 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 'PhabricatorMailOutboundStatus' => 'Phobject', + 'PhabricatorMailPostmarkAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorMailReceiver' => 'Phobject', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', + 'PhabricatorMailSMSMessage' => 'PhabricatorMailExternalMessage', + 'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailSendmailAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMailStamp' => 'Phobject', 'PhabricatorMailTarget' => 'Phobject', + 'PhabricatorMailTestAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailTwilioAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailUtil' => 'Phobject', 'PhabricatorMainMenuBarExtension' => 'Phobject', 'PhabricatorMainMenuSearchView' => 'AphrontView', @@ -9642,7 +9719,6 @@ phutil_register_library_map(array( ), 'PhabricatorPasteApplication' => 'PhabricatorApplication', 'PhabricatorPasteArchiveController' => 'PhabricatorPasteController', - 'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteController' => 'PhabricatorController', @@ -9694,6 +9770,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleMailEngine' => 'Phobject', + 'PhabricatorPeopleMailEngineException' => 'Exception', 'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPeopleNewController' => 'PhabricatorPeopleController', @@ -9721,14 +9799,15 @@ phutil_register_library_map(array( 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleWelcomeMailEngine' => 'PhabricatorPeopleMailEngine', 'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorPhameApplication' => 'PhabricatorApplication', 'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhluxApplication' => 'PhabricatorApplication', 'PhabricatorPholioApplication' => 'PhabricatorApplication', - 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', + 'PhabricatorPhoneNumber' => 'Phobject', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', @@ -9737,7 +9816,6 @@ phutil_register_library_map(array( 'PhabricatorPhragmentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionApplication' => 'PhabricatorApplication', - 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlApplication' => 'PhabricatorApplication', 'PhabricatorPhurlConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlController' => 'PhabricatorController', @@ -11022,7 +11100,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/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/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/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index ff4ed1f136..2c36e935ee 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -85,6 +85,25 @@ 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', + ), + + '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/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/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..2668da1218 --- /dev/null +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php @@ -0,0 +1,57 @@ +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->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; + } + + public function buildApplicationMenu() { + return $this->newNavigation()->getMenu(); + } + +} 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/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..0dde1b3c6f --- /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 @@ +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 @@ +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/message/PhabricatorAuthLoginMessageType.php b/src/applications/auth/message/PhabricatorAuthLoginMessageType.php new file mode 100644 index 0000000000..666df7e989 --- /dev/null +++ b/src/applications/auth/message/PhabricatorAuthLoginMessageType.php @@ -0,0 +1,18 @@ +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 @@ +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/phid/PhabricatorAuthMessagePHIDType.php b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php new file mode 100644 index 0000000000..cc37880ac8 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php @@ -0,0 +1,32 @@ +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; 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 @@ +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 @@ +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 @@ +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(); + } + + 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 = self::loadMessage($viewer, $message_key); + + if (!$message) { + return null; + } + + return $message->getMessageText(); + } + + +/* -( 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 @@ +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 @@ +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 @@ +setViewer($viewer) 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, diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index e597c52897..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( @@ -394,6 +399,23 @@ 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.'), + + '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/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'); + } +} 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); + } + } } 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/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index c5d9cf027b..ce24d48ead 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -188,13 +188,10 @@ 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( - '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/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/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/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 cfd7470099..a77277e983 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']; @@ -67,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).', @@ -108,10 +109,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/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 2cf946aea8..f3f31d9130 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,14 +22,18 @@ 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); } 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/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/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/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/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/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/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/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/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; } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php similarity index 52% rename from src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailAdapter.php index a8dba2335b..4fb262626d 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php @@ -1,13 +1,16 @@ getPhobjectClassConstant('ADAPTERTYPE'); @@ -20,37 +23,61 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { ->execute(); } - - 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 getSupportedMessageTypes(); + abstract public function sendMessage(PhabricatorMailExternalMessage $message); /** - * 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 +137,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 61% rename from src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php rename to src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php index f847488019..a289e5bc73 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.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 PhabricatorMailImplementationAmazonSESAdapter 'access-key' => 'string', 'secret-key' => 'string', 'endpoint' => 'string', - 'encoding' => 'string', )); } @@ -35,10 +30,27 @@ final class PhabricatorMailImplementationAmazonSESAdapter '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/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php deleted file mode 100644 index b917a93df2..0000000000 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php +++ /dev/null @@ -1,158 +0,0 @@ -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, - ); - - 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() { - return true; - } - - protected function validateOptions(array $options) { - PhutilTypeSpec::checkMap( - $options, - array( - 'api-key' => 'string', - 'domain' => 'string', - )); - } - - public function newDefaultOptions() { - return array( - 'api-key' => null, - 'domain' => null, - ); - } - - public function send() { - $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'); - } - - $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); - } - - if (idx($this->params, 'ccs')) { - $params['cc'] = implode(', ', $this->params['ccs']); - } - - foreach (idx($this->params, 'headers', array()) as $header) { - list($name, $value) = $header; - $params['h:'.$name] = $value; - } - - $future = new HTTPSFuture( - "https://api:{$key}@api.mailgun.net/v2/{$domain}/messages", - $params); - $future->setMethod('POST'); - - foreach ($this->attachments as $attachment) { - $future->attachFileData( - 'attachment', - $attachment['data'], - $attachment['name'], - $attachment['type']); - } - - 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); - } - - if (!idx($response, 'id')) { - $message = $response['message']; - throw new Exception( - pht( - 'Request failed with errors: %s.', - $message)); - } - - return true; - } - -} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php deleted file mode 100644 index fbc8c09cd9..0000000000 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php +++ /dev/null @@ -1,150 +0,0 @@ - '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/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php deleted file mode 100644 index 427248e39b..0000000000 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php +++ /dev/null @@ -1,123 +0,0 @@ - 'string', - )); - } - - public function newDefaultOptions() { - return array( - 'encoding' => 'base64', - ); - } - - /** - * @phutil-external-symbol class PHPMailerLite - */ - public function prepareForSend() { - $root = phutil_get_library_root('phabricator'); - $root = dirname($root); - require_once $root.'/externals/phpmailer/class.phpmailer-lite.php'; - $this->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(); - } - -} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php deleted file mode 100644 index 2b80905604..0000000000 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php +++ /dev/null @@ -1,120 +0,0 @@ -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/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php deleted file mode 100644 index eb451adfc9..0000000000 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php +++ /dev/null @@ -1,173 +0,0 @@ - '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'); - $key = $this->getOption('api-key'); - - $params = array(); - - $ii = 0; - foreach (idx($this->params, 'tos', array()) as $to) { - $params['to['.($ii++).']'] = $to; - } - - $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'); - } - - $params['from'] = idx($this->params, 'from'); - if (idx($this->params, 'from-name')) { - $params['fromname'] = $this->params['from-name']; - } - - if (idx($this->params, 'reply-to')) { - $replyto = $this->params['reply-to']; - - // Pick off the email part, no support for the name part in this API. - $params['replyto'] = $replyto[0]['email']; - } - - 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'])); - } - - if ($headers) { - // Convert to dictionary. - $headers = ipull($headers, 1, 0); - $headers = json_encode($headers); - $params['headers'] = $headers; - } - - $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); - } - - if ($response['message'] !== 'success') { - $errors = implode(';', $response['errors']); - throw new Exception(pht('Request failed with errors: %s.', $errors)); - } - - return true; - } - -} diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php deleted file mode 100644 index 61b9bdfb4f..0000000000 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php +++ /dev/null @@ -1,130 +0,0 @@ -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; - } - - public function getGuts() { - return $this->guts; - } - - public function setFailPermanently($fail) { - $this->guts['fail-permanently'] = $fail; - return $this; - } - - public function setFailTemporarily($fail) { - $this->guts['fail-temporarily'] = $fail; - return $this; - } - - public function getBody() { - return idx($this->guts, 'body'); - } - - public function getHTMLBody() { - return idx($this->guts, 'html-body'); - } - -} diff --git a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php new file mode 100644 index 0000000000..9eb478efc5 --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php @@ -0,0 +1,132 @@ + 'string', + 'domain' => 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'api-key' => null, + 'domain' => null, + ); + } + + public function sendMessage(PhabricatorMailExternalMessage $message) { + $api_key = $this->getOption('api-key'); + $domain = $this->getOption('domain'); + $params = array(); + + $subject = $message->getSubject(); + if ($subject !== null) { + $params['subject'] = $subject; + } + + $from_address = $message->getFromAddress(); + if ($from_address) { + $params['from'] = (string)$from_address; + } + + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + $to = array(); + foreach ($to_addresses as $address) { + $to[] = (string)$address; + } + $params['to'] = implode(', ', $to); + } + + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + $cc = array(); + foreach ($cc_addresses as $address) { + $cc[] = (string)$address; + } + $params['cc'] = implode(', ', $cc); + } + + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $params['h:reply-to'] = (string)$reply_address; + } + + $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->getData(), + $attachment->getFilename(), + $attachment->getMimeType()); + } + + 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); + } + + if (!idx($response, 'id')) { + $message = $response['message']; + throw new Exception( + pht( + 'Request failed with errors: %s.', + $message)); + } + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php new file mode 100644 index 0000000000..d84d8f8bfa --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php @@ -0,0 +1,128 @@ + '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/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/PhabricatorMailSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php new file mode 100644 index 0000000000..133e82b628 --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php @@ -0,0 +1,144 @@ + 'string', + )); + } + + public function newDefaultOptions() { + return array( + 'api-key' => null, + ); + } + + public function sendMessage(PhabricatorMailExternalMessage $message) { + $key = $this->getOption('api-key'); + + $parameters = array(); + + $subject = $message->getSubject(); + if ($subject !== null) { + $parameters['subject'] = $subject; + } + + $personalizations = array(); + + $to_addresses = $message->getToAddresses(); + if ($to_addresses) { + $personalizations['to'] = array(); + foreach ($to_addresses as $address) { + $personalizations['to'][] = $this->newPersonalization($address); + } + } + + $cc_addresses = $message->getCCAddresses(); + if ($cc_addresses) { + $personalizations['cc'] = array(); + foreach ($cc_addresses as $address) { + $personalizations['cc'][] = $this->newPersonalization($address); + } + } + + // 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, + ); + + $from_address = $message->getFromAddress(); + if ($from_address) { + $parameters['from'] = $this->newPersonalization($from_address); + } + + $reply_address = $message->getReplyToAddress(); + if ($reply_address) { + $parameters['reply_to'] = $this->newPersonalization($reply_address); + } + + $headers = $message->getHeaders(); + if ($headers) { + $map = array(); + foreach ($headers as $header) { + $map[$header->getName()] = $header->getValue(); + } + $parameters['headers'] = $map; + } + + $content = array(); + $text_body = $message->getTextBody(); + if ($text_body !== null) { + $content[] = array( + 'type' => 'text/plain', + 'value' => $text_body, + ); + } + + $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; + } + + $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; + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php new file mode 100644 index 0000000000..05f3c909aa --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php @@ -0,0 +1,45 @@ + 'string', + )); + } + + public function newDefaultOptions() { + return array( + '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->Send(); + } + +} diff --git a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php new file mode 100644 index 0000000000..f0840ba7bf --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php @@ -0,0 +1,141 @@ +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()); + } + + public function newDefaultOptions() { + return array(); + } + + public function supportsMessageIDHeader() { + return $this->supportsMessageID; + } + + public function getGuts() { + return $this->guts; + } + + 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 getBody() { + return idx($this->guts, 'body'); + } + + public function getHTMLBody() { + return idx($this->guts, 'html-body'); + } + + +} 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/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..faacdc2cfc 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'; @@ -17,13 +16,13 @@ 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( 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'), @@ -34,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/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/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/editor/PhabricatorMetaMTAApplicationEmailEditor.php b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php index 2cbd164a87..843e653039 100644 --- a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php +++ b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php @@ -103,6 +103,30 @@ final class PhabricatorMetaMTAApplicationEmailEditor $type, pht('Invalid'), pht('Email address is not formatted properly.')); + continue; + } + + $address = new PhutilEmailAddress($email); + if (PhabricatorMailUtil::isReservedAddress($address)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Reserved'), + 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/engine/PhabricatorMailEmailEngine.php b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php new file mode 100644 index 0000000000..fc00ccb3bb --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php @@ -0,0 +1,648 @@ +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(); + $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..c65346bf58 --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php @@ -0,0 +1,54 @@ +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/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/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/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php index 0fc7dd14b9..f29a63c2eb 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php @@ -115,10 +115,13 @@ final class PhabricatorMailManagementShowOutboundWorkflow $info[] = $this->newSectionHeader(pht('HEADERS')); $headers = $message->getDeliveredHeaders(); + if (!$headers) { + $headers = array(); + } + $unfiltered = $message->getUnfilteredHeaders(); if (!$unfiltered) { - $headers = $message->generateHeaders(); - $unfiltered = $headers; + $unfiltered = array(); } $header_map = array(); @@ -205,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/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; + } + +} 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/receiver/__tests__/PhabricatorMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php index 85389c3256..391acb2285 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 PhabricatorMailEmailEngine()) + ->newDefaultEmailAddress(); + + $void_address = id(new PhabricatorMailEmailEngine()) + ->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/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/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 15c8a0945a..70d94ccb0c 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. @@ -515,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) { @@ -551,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; } @@ -586,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); @@ -597,10 +653,6 @@ final class PhabricatorMetaMTAMail } } - foreach ($sorted as $mailer) { - $mailer->prepareForSend(); - } - return $sorted; } @@ -627,36 +679,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 +744,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 +785,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 = PhabricatorEnv::getEnvConfig('metamta.default-address'); - 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 +838,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 +1038,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 +1060,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,66 +1095,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() { - $install_uri = PhabricatorEnv::getURI('/'); - $install_uri = new PhutilURI($install_uri); - - return $install_uri->getDomain(); - } - - public function newVoidEmailAddress() { - $domain = $this->newMailDomain(); - $address = "void-recipient@{$domain}"; - return new PhutilEmailAddress($address); - } - /* -( Routing )------------------------------------------------------------ */ @@ -1563,27 +1191,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( diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 95f6048c01..64528ea949 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,132 @@ 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(); + + $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; + } + + // See T13234. Don't process mail if a user has attached this address + // to their account. + if (PhabricatorMailUtil::isUserAddress($target)) { + unset($targets[$key]); + continue; } } - $receiver->receiveMail($this, $sender); + $any_accepted = false; + $receiver_exception = null; + 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; + } + } + } + } + + if ($receiver_exception) { + throw $receiver_exception; + } + + + if (!$any_accepted) { + 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. + + 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: @@ -159,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; @@ -311,51 +422,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 +500,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__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php index d20a28fc15..7462aaf558 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,21 +182,29 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase { $supports_message_id, $is_first_mail) { - $mailer = new PhabricatorMailImplementationTestAdapter(); + $user = $this->generateNewTestUser(); + $phid = $user->getPHID(); - $mailer->prepareForSend( - array( - 'supportsMessageIDHeader' => $supports_message_id, - )); + $mailer = new PhabricatorMailTestAdapter(); - $thread_id = ''; + $mailer->setSupportsMessageID($supports_message_id); - $mail = new PhabricatorMetaMTAMail(); - $mail->setThreadID($thread_id, $is_first_mail); - $mail->sendWithMailers(array($mailer)); + $thread_id = 'somethread-12345'; + + $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; @@ -261,10 +269,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 +358,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 +378,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 +406,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/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/metamta/util/PhabricatorMailUtil.php b/src/applications/metamta/util/PhabricatorMailUtil.php index 60eb89dea2..a5fbc7179e 100644 --- a/src/applications/metamta/util/PhabricatorMailUtil.php +++ b/src/applications/metamta/util/PhabricatorMailUtil.php @@ -62,4 +62,58 @@ 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 PhabricatorMailEmailEngine()) + ->newDefaultEmailAddress(); + if (self::matchAddresses($address, $default_address)) { + return true; + } + + $void_address = id(new PhabricatorMailEmailEngine()) + ->newVoidEmailAddress(); + if (self::matchAddresses($address, $void_address)) { + return true; + } + + return false; + } + + public static function isUserAddress(PhutilEmailAddress $address) { + $user_email = id(new PhabricatorUserEmail())->loadOneWhere( + 'address = %s', + $address->getAddress()); + + return (bool)$user_email; + } + } 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 992d1e60b5..5562858b8e 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,20 +27,24 @@ 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()); - $subject_prefix = - PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); + $sender = $this->getSender(); + if (!$sender) { + return; + } + + $subject_prefix = pht('[Paste]'); $subject = pht('You successfully created a paste.'); $paste_uri = PhabricatorEnv::getProductionURI($paste->getURI()); $body = new PhabricatorMetaMTAMailBody(); @@ -56,5 +61,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/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..e9faae3d62 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( @@ -152,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) @@ -170,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 14b1544b7f..3fb75265ff 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,38 +21,73 @@ 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')); } + $v_message = $request->getStr('message'); + if ($request->isFormPost()) { - $user->sendWelcomeEmail($admin); + if (strlen($v_message)) { + $welcome_engine->setWelcomeMessage($v_message); + } + + $welcome_engine->sendMail(); 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) + ->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())) + ->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.')) + ->appendRemarkupInstructions($message_instructions) + ->appendControl( + id(new PhabricatorRemarkupControl()) + ->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/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php new file mode 100644 index 0000000000..281009341d --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -0,0 +1,72 @@ +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); + } + + 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/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..ff7ee71272 --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -0,0 +1,135 @@ +welcomeMessage = $welcome_message; + return $this; + } + + public function getWelcomeMessage() { + return $this->welcomeMessage; + } + + public function validateMail() { + $sender = $this->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(); + + $base_uri = PhabricatorEnv::getProductionURI('/'); + + $engine = new PhabricatorAuthSessionEngine(); + + $uri = $engine->getOneTimeLoginURI( + $recipient, + $recipient->loadPrimaryEmail(), + PhabricatorAuthSessionEngine::ONETIME_WELCOME); + + $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); + } + + $message_body = $this->newBody(); + if ($message_body !== null) { + $message[] = $message_body; + } + + $message = implode("\n\n", $message); + + return id(new PhabricatorMetaMTAMail()) + ->addTos(array($recipient->getPHID())) + ->setSubject(pht('[Phabricator] Welcome to Phabricator')) + ->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; + } + +} 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) { 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/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/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/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/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/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/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 { 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) { 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) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 854c361614..64f375fd88 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; } @@ -3262,7 +3265,7 @@ abstract class PhabricatorApplicationTransactionEditor } if (!$is_comment || !$seen_comment) { - $header = $xaction->getTitleForMail(); + $header = $xaction->getTitleForTextMail(); if ($header !== null) { $headers[] = $header; } @@ -3347,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) { diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner index 84d4fa48d1..b1ad08b7dd 100644 --- a/src/docs/user/configuration/configuring_inbound_email.diviner +++ b/src/docs/user/configuration/configuring_inbound_email.diviner @@ -4,39 +4,72 @@ 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: -There are a few approaches available: +**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 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 your + configuration. + - For handling email that creates objects, configure inbound addresses in the + relevant application. + +See below for details on each of these steps. + + +Configuration Overview +====================== + +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. + +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 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 -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. @@ -44,22 +77,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 +create 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 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 +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 +150,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 +169,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 +183,7 @@ like this: example domain with your actual domain. - Configure a mailer in `cluster.mailers` with your Mailgun API key. + Postmark Setup ============== @@ -143,7 +199,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 +216,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 +248,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 10805b77d9..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 @@ -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. + - `sendgrid`: Use SendGrid. + - `postmark`: Use Postmark. It also supports these local mailers: @@ -98,8 +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 -`PhabricatorMailImplementationAdapter`. +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. @@ -139,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`: @@ -171,14 +169,28 @@ 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: +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 ================== @@ -192,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! @@ -209,33 +221,31 @@ 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. -Mailer: STMP +Mailer: SMTP ============ You can use this adapter to send mail via an external SMTP server, like Gmail. 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) { 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; +} 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 { 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