--- /dev/null
+<?php
+
+class CRM_OAuth_MailSetup {
+
+ /**
+ * Return a list of setup-options based on OAuth2 services.
+ *
+ * @see CRM_Utils_Hook::mailSetupActions()
+ */
+ public static function buildSetupLinks() {
+ $clients = Civi\Api4\OAuthClient::get(0)->addWhere('is_active', '=', 1)->execute();
+ $providers = Civi\Api4\OAuthProvider::get(0)->execute()->indexBy('name');
+
+ $setupActions = [];
+ foreach ($clients as $client) {
+ $provider = $providers[$client['provider']] ?? NULL;
+ if ($provider === NULL) {
+ continue;
+ }
+ // v api OptionValue.get option_group_id=mail_protocol
+ if (!empty($provider['mailSettingsTemplate'])) {
+ $setupActions['oauth_' . $client['id']] = [
+ 'title' => sprintf('%s (ID #%s)', $provider['title'] ?? $provider['name'] ?? ts('OAuth2'), $client['id']),
+ 'callback' => ['CRM_OAuth_MailSetup', 'setup'],
+ 'oauth_client_id' => $client['id'],
+ ];
+ }
+ }
+
+ return $setupActions;
+ }
+
+ /**
+ * When a user chooses to add one of our mail options, we kick off
+ * the authorization-code workflow.
+ *
+ * @param array $setupAction
+ * The chosen descriptor from mailSetupActions.
+ * @return array
+ * With keys:
+ * - url: string, the final URL to go to.
+ * @see CRM_Utils_Hook::mailSetupActions()
+ */
+ public static function setup($setupAction) {
+ $authCode = Civi\Api4\OAuthClient::authorizationCode(0)
+ ->addWhere('id', '=', $setupAction['oauth_client_id'])
+ ->setStorage('OAuthSysToken')
+ ->setTag('MailSettings:setup')
+ ->execute()
+ ->single();
+
+ return [
+ 'url' => $authCode['url'],
+ ];
+ }
+
+ /**
+ * When the user returns with a token, we add a new record to
+ * civicrm_mail_settings with defaults and redirect to the edit screen.
+ *
+ * @param array $token
+ * OAuthSysToken
+ * @param string $nextUrl
+ */
+ public static function onReturn($token, &$nextUrl) {
+ if ($token['tag'] !== 'MailSettings:setup') {
+ return;
+ }
+
+ $client = \Civi\Api4\OAuthClient::get(0)->addWhere('id', '=', $token['client_id'])->execute()->single();
+ $provider = \Civi\Api4\OAuthProvider::get(0)->addWhere('name', '=', $client['provider'])->execute()->single();
+
+ $vars = ['token' => $token, 'client' => $client, 'provider' => $provider];
+ $mailSettings = civicrm_api4('MailSettings', 'create', [
+ 'values' => self::evalArrayTemplate($provider['mailSettingsTemplate'], $vars),
+ ])->single();
+
+ \Civi\Api4\OAuthSysToken::update(0)
+ ->addWhere('id', '=', $token['id'])
+ ->setValues(['tag' => 'MailSettings:' . $mailSettings['id']])
+ ->execute();
+
+ CRM_Core_Session::setStatus(
+ ts('Here are the account defaults we detected for %1. Please check them carefully.', [
+ 1 => $mailSettings['name'],
+ ]),
+ ts('Account created!'),
+ 'info'
+ );
+
+ $nextUrl = CRM_Utils_System::url('civicrm/admin/mailSettings', [
+ 'action' => 'update',
+ 'id' => $mailSettings['id'],
+ 'reset' => 1,
+ ], TRUE, NULL, FALSE);
+ }
+
+ /**
+ * @param array $template
+ * List of key-value expressions.
+ * Ex: ['name' => '{{person.first}} {{person.last}}']
+ * Expressions begin with the dotted-name of a variable.
+ * Optionally, the value may be piped through other functions
+ * @param array $vars
+ * Array tree of data to interpolate.
+ * @return array
+ * The template array, with '{{...}}' expressions evaluated.
+ */
+ public static function evalArrayTemplate($template, $vars) {
+ $filters = [
+ 'getMailDomain' => function($v) {
+ $parts = explode('@', $v);
+ return $parts[1] ?? NULL;
+ },
+ 'getMailUser' => function($v) {
+ $parts = explode('@', $v);
+ return $parts[0] ?? NULL;
+ },
+ ];
+
+ $lookupVars = function($m) use ($vars, $filters) {
+ $parts = explode('|', $m[1]);
+ $value = (string) CRM_Utils_Array::pathGet($vars, explode('.', array_shift($parts)));
+ foreach ($parts as $part) {
+ if (isset($filters[$part])) {
+ $value = $filters[$part]($value);
+ }
+ else {
+ $value = NULL;
+ }
+ }
+ return $value;
+ };
+
+ $values = [];
+ foreach ($template as $key => $value) {
+ $values[$key] = is_string($value)
+ ? preg_replace_callback(';{{([a-zA-Z0-9_\.\|]+)}};', $lookupVars, $value)
+ : $value;
+ }
+ return $values;
+ }
+
+}
--- /dev/null
+<?php
+
+use CRM_OAuth_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Test helper functions in CRM_OAuth_MailSetup.
+ *
+ * @group headless
+ */
+class CRM_OAuth_MailSetupTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()->install('oauth-client')->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ public function testEvalArrayTemplate() {
+ $vars = array(
+ 'token' => [
+ 'client_id' => 10,
+ 'resource_owner' => ['mail' => 'foo@bar.com'],
+ ],
+ 'client' => [
+ 'id' => 1,
+ 'provider' => 'ms-exchange',
+ 'guid' => 'abcd-1234-efgh-5678',
+ 'secret' => '8765-hgfe-4321-dcba',
+ 'options' => NULL,
+ 'is_active' => TRUE,
+ 'created_date' => '2020-10-29 10:11:12',
+ 'modified_date' => '2020-10-29 10:11:12',
+ ],
+ 'provider' => [
+ 'name' => 'foozball',
+ 'title' => 'Foozball Association',
+ 'options' => [
+ 'urlAuthorize' => 'https://login.example.com/common/oauth2/v2.0/authorize',
+ 'urlAccessToken' => 'https://login.example.com/common/oauth2/v2.0/token',
+ 'urlResourceOwnerDetails' => 'https://resource.example.com/v9.0/me',
+ 'scopeSeparator' => ' ',
+ 'scopes' => [],
+ ],
+ 'mailSettingsTemplate' => [
+ 'name' => '{{provider.title}}: {{token.resource_owner.mail}}',
+ 'domain' => '{{token.resource_owner.mail|getMailDomain}}',
+ 'localpart' => NULL,
+ 'return_path' => NULL,
+ 'protocol:name' => 'IMAP',
+ 'server' => 'imap.foozball.com',
+ 'username' => '{{token.resource_owner.mail}}',
+ 'password' => NULL,
+ 'is_ssl' => TRUE,
+ ],
+ 'class' => 'Civi\\OAuth\\CiviGenericProvider',
+ ],
+ );
+ $expected = [
+ 'name' => 'Foozball Association: foo@bar.com',
+ 'domain' => 'bar.com',
+ 'localpart' => NULL,
+ 'return_path' => NULL,
+ 'protocol:name' => 'IMAP',
+ 'server' => 'imap.foozball.com',
+ 'username' => 'foo@bar.com',
+ 'password' => '',
+ 'is_ssl' => TRUE,
+ ];
+ $actual = \CRM_OAuth_MailSetup::evalArrayTemplate($vars['provider']['mailSettingsTemplate'], $vars);
+ $this->assertEquals($expected, $actual);
+ $this->assertTrue($actual['localpart'] === NULL);
+ }
+
+}