diff --git a/src/Client.php b/src/Client.php index c4ce3dd..ce39dd8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -12,6 +12,7 @@ * @author Damian Fernandez Sosa * @author Anish Mistry * @author Jan Schneider + * @author Jean Charles Delépine * @license http://www.horde.org/licenses/bsd BSD */ @@ -82,6 +83,11 @@ class Client */ const AUTH_EXTERNAL = 'EXTERNAL'; + /** + * XOAUTH2 authentication. + */ + const AUTH_XOAUTH2 = 'XOAUTH2'; + /** * The authentication methods this class supports. * @@ -95,6 +101,7 @@ class Client self::AUTH_EXTERNAL, self::AUTH_PLAIN, self::AUTH_LOGIN, + self::AUTH_XOAUTH2, ); /** @@ -161,6 +168,9 @@ class Client * - port: Port of server (DEFAULT: 4190). * - user: Login username (optional). * - password: Login password (optional). + * - xoauth2_token: (mixed) If set, will authenticate via the XOAUTH2 + * mechanism (if available) with this token. Either a + * string or a Horde\ManageSieve\Password object. * - authmethod: Type of login to perform (see $supportedAuthMethods) * (DEFAULT: AUTH_AUTOMATIC). * - euser: Effective user. If authenticating as an administrator, login @@ -196,6 +206,7 @@ public function __construct($params = array()) 'secure' => true, 'timeout' => 5, 'user' => '', + 'xoauth2_token' => null, ), $params ); @@ -216,11 +227,34 @@ public function __construct($params = array()) } if (strlen($this->_params['user']) && - strlen($this->_params['password'])) { + (strlen((string)$this->_params['password']) || $this->getParam('xoauth2_token'))) { $this->_handleConnectAndLogin(); } } + /** + * Get a connection parameter. + * + * @param string $key The parameter key. + * + * @return mixed The parameter value, or null if it doesn't exist. + */ + public function getParam($key) + { + switch ($key) { + case 'xoauth2_token': + if (isset($this->_params[$key]) && + ($this->_params[$key] instanceof Password)) { + return $this->_params[$key]->getPassword(); + } + break; + } + + return isset($this->_params[$key]) + ? $this->_params[$key] + : null; + } + /** * Passes a logger for debug logging. * @@ -612,6 +646,9 @@ protected function _cmdAuthenticate( case self::AUTH_EXTERNAL: $this->_authEXTERNAL($uid, $pwd, $euser); break; + case self::AUTH_XOAUTH2: + $this->_authXOAUTH2($uid, $this->getParam('xoauth2_token'), $euser); + break; default : throw new Exception( $method . ' is not a supported authentication method' @@ -737,6 +774,22 @@ protected function _authEXTERNAL($user, $pass, $euser) return $this->_sendCmd($cmd); } + /** + * Authenticates the user using the XOAUTH2 method. + * + * @param string $user The userid to authenticate as. + * @param string $token The XOAUTH2 token (already formatted). + * @param string $euser The effective uid to authenticate as. Not used. + * + * @throws \Horde\ManageSieve\Exception + */ + protected function _authXOAUTH2($user, $token, $euser) + { + return $this->_sendCmd( + sprintf('AUTHENTICATE "XOAUTH2" "%s"', $token) + ); + } + /** * Removes a script from the server. * diff --git a/src/Password.php b/src/Password.php new file mode 100644 index 0000000..2a358ed --- /dev/null +++ b/src/Password.php @@ -0,0 +1,34 @@ + + * @category Horde + * @copyright 2026 Horde LLC + * @license http://www.horde.org/licenses/bsd BSD + * @package ManageSieve + * @since 2.0.0 + */ +interface Password +{ + /** + * Return the password to use for the server connection. + * + * @return string The password. + */ + public function getPassword(); +} diff --git a/src/Password/Xoauth2.php b/src/Password/Xoauth2.php new file mode 100644 index 0000000..21ce6ad --- /dev/null +++ b/src/Password/Xoauth2.php @@ -0,0 +1,71 @@ + + * @category Horde + * @copyright 2026 Horde LLC + * @license http://www.horde.org/licenses/bsd BSD + * @package ManageSieve + * @since 2.0.0 + */ +class Xoauth2 implements \Horde\ManageSieve\Password +{ + /** + * Access token. + * + * @var string + */ + public $access_token; + + /** + * Username. + * + * @var string + */ + public $username; + + /** + * Constructor. + * + * @param string $username The username. + * @param string $access_token The access token. + */ + public function __construct($username, $access_token) + { + $this->username = $username; + $this->access_token = $access_token; + } + + /** + * Return the password to use for the server connection. + * + * @return string The password. + */ + public function getPassword() + { + // base64("user=" {User} "^Aauth=Bearer " {Access Token} "^A^A") + // ^A represents a Control+A (\001) + return base64_encode( + 'user=' . $this->username . "\1" . + 'auth=Bearer ' . $this->access_token . "\1\1" + ); + } +} diff --git a/test/Unit/Xoauth2Test.php b/test/Unit/Xoauth2Test.php new file mode 100644 index 0000000..954bf89 --- /dev/null +++ b/test/Unit/Xoauth2Test.php @@ -0,0 +1,55 @@ +assertEquals( + 'dXNlcj1zb21ldXNlckBleGFtcGxlLmNvbQFhdXRoPUJlYXJlciB2RjlkZnQ0cW1UYzJOdmIzUmxja0JoZEhSaGRtbHpkR0V1WTI5dENnPT0BAQ==', + $xoauth2->getPassword(), + ); + } + + public function testImplementsPasswordInterface(): void + { + $xoauth2 = new Xoauth2('user@example.com', 'token'); + $this->assertInstanceOf(Password::class, $xoauth2); + } + + public function testUsernameIsAccessible(): void + { + $xoauth2 = new Xoauth2('user@example.com', 'token'); + $this->assertSame('user@example.com', $xoauth2->username); + } + + public function testAccessTokenIsAccessible(): void + { + $xoauth2 = new Xoauth2('user@example.com', 'mytoken'); + $this->assertSame('mytoken', $xoauth2->access_token); + } +}