Skip to content

Commit 0516586

Browse files
authored
Merge pull request #218 from Slamdunk/microsoft_pop3_xoauth2
Add support for Microsoft POP3 XOAUTH2
2 parents 430e034 + 64b2059 commit 0516586

File tree

7 files changed

+293
-15
lines changed

7 files changed

+293
-15
lines changed

src/Protocol/Pop3.php

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Laminas\Mail\Protocol;
44

5+
use Laminas\Mail\Protocol\Pop3\Response;
56
use Laminas\Stdlib\ErrorHandler;
67

78
use function explode;
@@ -151,25 +152,14 @@ public function sendRequest($request)
151152
*/
152153
public function readResponse($multiline = false)
153154
{
154-
ErrorHandler::start();
155-
$result = fgets($this->socket);
156-
$error = ErrorHandler::stop();
157-
if (! is_string($result)) {
158-
throw new Exception\RuntimeException('read failed - connection closed?', 0, $error);
159-
}
155+
$response = $this->readRemoteResponse();
160156

161-
$result = trim($result);
162-
if (strpos($result, ' ')) {
163-
[$status, $message] = explode(' ', $result, 2);
164-
} else {
165-
$status = $result;
166-
$message = '';
167-
}
168-
169-
if ($status != '+OK') {
157+
if ($response->status() != '+OK') {
170158
throw new Exception\RuntimeException('last request failed');
171159
}
172160

161+
$message = $response->message();
162+
173163
if ($multiline) {
174164
$message = '';
175165
$line = fgets($this->socket);
@@ -185,6 +175,32 @@ public function readResponse($multiline = false)
185175
return $message;
186176
}
187177

178+
/**
179+
* read a response
180+
* return extracted status / message from response
181+
182+
* @throws Exception\RuntimeException
183+
*/
184+
protected function readRemoteResponse(): Response
185+
{
186+
ErrorHandler::start();
187+
$result = fgets($this->socket);
188+
$error = ErrorHandler::stop();
189+
if (! is_string($result)) {
190+
throw new Exception\RuntimeException('read failed - connection closed?', 0, $error);
191+
}
192+
193+
$result = trim($result);
194+
if (strpos($result, ' ')) {
195+
[$status, $message] = explode(' ', $result, 2);
196+
} else {
197+
$status = $result;
198+
$message = '';
199+
}
200+
201+
return new Response($status, $message);
202+
}
203+
188204
/**
189205
* Send request and get response
190206
*

src/Protocol/Pop3/Response.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laminas\Mail\Protocol\Pop3;
6+
7+
/**
8+
* POP3 response value object
9+
*
10+
* @internal
11+
*/
12+
final class Response
13+
{
14+
/** @var string $status */
15+
private $status;
16+
17+
/** @var string $message */
18+
private $message;
19+
20+
public function __construct(string $status, string $message)
21+
{
22+
$this->status = $status;
23+
$this->message = $message;
24+
}
25+
26+
public function status(): string
27+
{
28+
return $this->status;
29+
}
30+
31+
public function message(): string
32+
{
33+
return $this->message;
34+
}
35+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Laminas\Mail\Protocol\Pop3\Xoauth2;
4+
5+
use Laminas\Mail\Protocol\Exception\RuntimeException;
6+
use Laminas\Mail\Protocol\Pop3;
7+
use Laminas\Mail\Protocol\Xoauth2\Xoauth2;
8+
9+
/**
10+
* @final
11+
*/
12+
class Microsoft extends Pop3
13+
{
14+
protected const AUTH_INITIALIZE_REQUEST = 'AUTH XOAUTH2';
15+
protected const AUTH_RESPONSE_INITIALIZED_OK = '+';
16+
17+
/**
18+
* @param string $user the target mailbox to access
19+
* @param string $password OAUTH2 accessToken
20+
* @param bool $tryApop obsolete parameter not used here
21+
*/
22+
public function login($user, $password, $tryApop = true): void
23+
{
24+
$this->sendRequest(self::AUTH_INITIALIZE_REQUEST);
25+
26+
$response = $this->readRemoteResponse();
27+
28+
if ($response->status() != self::AUTH_RESPONSE_INITIALIZED_OK) {
29+
throw new RuntimeException($response->message());
30+
}
31+
32+
$this->request(Xoauth2::encodeXoauth2Sasl($user, $password));
33+
}
34+
}

src/Protocol/Xoauth2/Xoauth2.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laminas\Mail\Protocol\Xoauth2;
6+
7+
use function base64_encode;
8+
use function chr;
9+
use function sprintf;
10+
11+
/**
12+
* @internal
13+
*/
14+
final class Xoauth2
15+
{
16+
/**
17+
* encodes accessToken and target mailbox to Xoauth2 SASL base64 encoded string
18+
*/
19+
public static function encodeXoauth2Sasl(string $targetMailbox, string $accessToken): string
20+
{
21+
return base64_encode(
22+
sprintf(
23+
"user=%s%sauth=Bearer %s%s%s",
24+
$targetMailbox,
25+
chr(0x01),
26+
$accessToken,
27+
chr(0x01),
28+
chr(0x01)
29+
)
30+
);
31+
}
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaminasTest\Mail\Protocol\Pop3;
6+
7+
use Laminas\Mail\Protocol\Pop3\Response;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* @covers Laminas\Mail\Protocol\Pop3\Response
12+
*/
13+
class ResponseTest extends TestCase
14+
{
15+
/** @psalm-suppress InternalClass */
16+
public function testIntegration(): void
17+
{
18+
/** @psalm-suppress InternalMethod */
19+
$response = new Response('+OK', 'Auth');
20+
21+
/** @psalm-suppress InternalMethod */
22+
$this->assertEquals('+OK', $response->status());
23+
24+
/** @psalm-suppress InternalMethod */
25+
$this->assertEquals('Auth', $response->message());
26+
}
27+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaminasTest\Mail\Protocol\Pop3\Xoauth2;
6+
7+
use Laminas\Mail\Exception\RuntimeException;
8+
use Laminas\Mail\Protocol\Pop3\Response;
9+
use Laminas\Mail\Protocol\Pop3\Xoauth2\Microsoft;
10+
use Laminas\Mail\Protocol\Xoauth2\Xoauth2;
11+
use PHPUnit\Framework\TestCase;
12+
13+
use function fopen;
14+
use function rewind;
15+
use function str_replace;
16+
use function stream_get_contents;
17+
18+
/**
19+
* @covers Laminas\Mail\Protocol\Pop3\Xoauth2\Microsoft
20+
*/
21+
class MicrosoftTest extends TestCase
22+
{
23+
/** @psalm-suppress InternalClass */
24+
public function testIntegration(): void
25+
{
26+
/**
27+
* @psalm-suppress PropertyNotSetInConstructor
28+
* @psalm-suppress InvalidExtendClass
29+
*/
30+
$protocol = new class () extends Microsoft {
31+
private string $step;
32+
33+
/** @psalm-suppress InternalClass */
34+
public function readRemoteResponse(): Response
35+
{
36+
if ($this->step === self::AUTH_INITIALIZE_REQUEST) {
37+
/** @psalm-suppress InternalMethod */
38+
return new Response(self::AUTH_RESPONSE_INITIALIZED_OK, 'Auth initialized');
39+
}
40+
41+
/** @psalm-suppress InternalMethod */
42+
return new Response('+OK', 'Authenticated');
43+
}
44+
45+
/**
46+
* Send a request
47+
*
48+
* @param string $request your request without newline
49+
* @throws RuntimeException
50+
*/
51+
public function sendRequest($request): void
52+
{
53+
$this->step = $request;
54+
parent::sendRequest($request);
55+
}
56+
57+
/**
58+
* Open connection to POP3 server
59+
*
60+
* @param string $host hostname or IP address of POP3 server
61+
* @param int|null $port of POP3 server, default is 110 (995 for ssl)
62+
* @param string|bool $ssl use 'SSL', 'TLS' or false
63+
* @throws RuntimeException
64+
* @return string welcome message
65+
*/
66+
public function connect($host, $port = null, $ssl = false)
67+
{
68+
$this->socket = fopen("php://memory", 'rw+');
69+
return '';
70+
}
71+
72+
/**
73+
* @return null|resource
74+
*/
75+
public function getSocket()
76+
{
77+
return $this->socket;
78+
}
79+
};
80+
81+
$protocol->connect('localhost', 0, false);
82+
83+
$protocol->login('test@example.com', '123');
84+
85+
$this->assertInstanceOf(Microsoft::class, $protocol);
86+
87+
$streamContents = '';
88+
if ($socket = $protocol->getSocket()) {
89+
rewind($socket);
90+
$streamContents = stream_get_contents($socket);
91+
$streamContents = str_replace("\r\n", "\n", $streamContents);
92+
}
93+
94+
/** @psalm-suppress InternalMethod */
95+
$xoauth2Sasl = Xoauth2::encodeXoauth2Sasl('test@example.com', '123');
96+
97+
$this->assertEquals(
98+
'AUTH XOAUTH2' . "\n" . $xoauth2Sasl . "\n",
99+
$streamContents
100+
);
101+
}
102+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaminasTest\Mail\Protocol\Xoauth2;
6+
7+
use Laminas\Mail\Protocol\Xoauth2\Xoauth2;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* @covers Laminas\Mail\Protocol\Xoauth2\Xoauth2
12+
*/
13+
class Xoauth2Test extends TestCase
14+
{
15+
/** @psalm-suppress InternalClass */
16+
public function testEncodeXoauth2Sasl(): void
17+
{
18+
$accessToken = 'dXNlcj10ZXN0QGNvbnRvc28ub25taWNyb3NvZnQuY29tAWF1dGg9QmVhcmVyIEV3QkFBbDNCQUFVRkZwVUFvN';
19+
$accessToken .= '0ozVmUwYmpMQldaV0NjbFJDM0VvQUEBAQ==';
20+
21+
/**
22+
* @psalm-suppress InternalMethod
23+
*/
24+
$this->assertEquals(
25+
$accessToken,
26+
Xoauth2::encodeXoauth2Sasl(
27+
'test@contoso.onmicrosoft.com',
28+
'EwBAAl3BAAUFFpUAo7J3Ve0bjLBWZWCclRC3EoAA'
29+
)
30+
);
31+
}
32+
}

0 commit comments

Comments
 (0)