Skip to content

Commit 0f86880

Browse files
committed
Refactor
1 parent 38ca099 commit 0f86880

File tree

11 files changed

+265
-96
lines changed

11 files changed

+265
-96
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"symfony/sendgrid-mailer": "^6.4",
6969
"symfony/twig-bundle": "^6.4",
7070
"symfony/messenger": "^6.4",
71-
"symfony/lock": "^6.4"
71+
"symfony/lock": "^6.4",
72+
"webklex/php-imap": "^6.2",
73+
"ext-imap": "*"
7274
},
7375
"require-dev": {
7476
"phpunit/phpunit": "^9.5",

config/services/services.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,25 @@ services:
4545
autoconfigure: true
4646

4747
PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler: ~
48+
49+
PhpList\Core\Domain\Common\Mail\MailReaderInterface: '@PhpList\Core\Domain\Common\Mail\NativeImapMailReader'
50+
51+
Webklex\PHPIMAP\ClientManager: ~
52+
53+
PhpList\Core\Domain\Common\Mail\WebklexMailReader:
54+
arguments:
55+
$cm: '@Webklex\PHPIMAP\ClientManager'
56+
$config:
57+
host: '%imap_bounce.host%'
58+
port: '%imap_bounce.port%'
59+
encryption: '%imap_bounce.encryption%'
60+
validate_cert: true
61+
username: '%imap_bounce.email%'
62+
password: '%imap_bounce.password%'
63+
protocol: 'imap'
64+
65+
PhpList\Core\Domain\Common\Mail\NativeImapMailReader:
66+
arguments:
67+
$mailbox: '%env(IMAP_MAILBOX)%' # e.g. "{imap.example.com:993/imap/ssl}INBOX" or "/var/mail/user"
68+
$user: '%imap_bounce.email%'
69+
$pass: '%imap_bounce.password%'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Mail;
6+
7+
use DateTimeImmutable;
8+
use IMAP\Connection;
9+
10+
interface MailReaderInterface
11+
{
12+
public function open(string $mailbox, ?string $user = null, ?string $password = null, int $options = 0): Connection;
13+
public function numMessages(Connection $link): int;
14+
public function fetchHeader(Connection $link, int $msgNo): string;
15+
public function headerDate(Connection $link, int $msgNo): DateTimeImmutable;
16+
public function body(Connection $link, int $msgNo): string;
17+
public function delete(Connection $link, int $msgNo): void;
18+
public function close(Connection $link, bool $expunge): void;
19+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Mail;
6+
7+
use DateTimeImmutable;
8+
use IMAP\Connection;
9+
use RuntimeException;
10+
11+
class NativeImapMailReader implements MailReaderInterface
12+
{
13+
public function open(string $mailbox, ?string $user = null, ?string $password = null, int $options = 0): Connection
14+
{
15+
$link = @imap_open($mailbox, (string)$user, (string)$password, $options);
16+
if ($link === false) {
17+
throw new RuntimeException('Cannot open mailbox: '.(imap_last_error() ?: 'unknown error'));
18+
}
19+
return $link;
20+
}
21+
22+
public function numMessages(Connection $link): int
23+
{
24+
return imap_num_msg($link);
25+
}
26+
27+
public function fetchHeader(Connection $link, int $msgNo): string
28+
{
29+
return imap_fetchheader($link, $msgNo) ?: '';
30+
}
31+
32+
public function headerDate(Connection $link, int $msgNo): DateTimeImmutable
33+
{
34+
$info = imap_headerinfo($link, $msgNo);
35+
$date = $info->date ?? null;
36+
37+
return $date ? new DateTimeImmutable($date) : new DateTimeImmutable();
38+
}
39+
40+
public function body(Connection $link, int $msgNo): string
41+
{
42+
return imap_body($link, $msgNo) ?: '';
43+
}
44+
45+
public function delete(Connection $link, int $msgNo): void
46+
{
47+
imap_delete($link, (string)$msgNo);
48+
}
49+
50+
public function close(Connection $link, bool $expunge): void
51+
{
52+
$expunge ? imap_close($link, CL_EXPUNGE) : imap_close($link);
53+
}
54+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Mail;
6+
7+
use Webklex\PHPIMAP\ClientManager;
8+
9+
class WebklexMailReader
10+
{
11+
public function __construct(private ClientManager $cm, private array $config) {}
12+
13+
14+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Model;
6+
7+
use DateTimeImmutable;
8+
9+
class MessageDto
10+
{
11+
public function __construct(
12+
private string $uid,
13+
private string $messageId,
14+
private string $subject,
15+
private string $from,
16+
private array $to,
17+
private ?string $cc = null,
18+
private ?string $bcc = null,
19+
private DateTimeImmutable $date,
20+
private string $bodyText = '',
21+
private string $bodyHtml = '',
22+
private array $attachments = []
23+
) {}
24+
25+
public function getUid(): string { return $this->uid; }
26+
public function getMessageId(): string { return $this->messageId; }
27+
public function getSubject(): string { return $this->subject; }
28+
public function getFrom(): string { return $this->from; }
29+
public function getTo(): array { return $this->to; }
30+
public function getCc(): ?string { return $this->cc; }
31+
public function getBcc(): ?string { return $this->bcc; }
32+
public function getDate(): DateTimeImmutable { return $this->date; }
33+
public function getBodyText(): string { return $this->bodyText; }
34+
public function getBodyHtml(): string { return $this->bodyHtml; }
35+
public function getAttachments(): array { return $this->attachments; }
36+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Service;
6+
7+
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
8+
9+
class MessageParser
10+
{
11+
public function __construct(
12+
private readonly SubscriberManager $subscriberManager,
13+
) {
14+
}
15+
public function decodeBody(string $header, string $body): string
16+
{
17+
$transferEncoding = '';
18+
if (preg_match('/Content-Transfer-Encoding: ([\w-]+)/i', $header, $regs)) {
19+
$transferEncoding = strtolower($regs[1]);
20+
}
21+
22+
return match ($transferEncoding) {
23+
'quoted-printable' => quoted_printable_decode($body),
24+
'base64' => base64_decode($body) ?: '',
25+
default => $body,
26+
};
27+
}
28+
29+
public function findMessageId(string $text): ?string
30+
{
31+
if (preg_match('/(?:X-MessageId|X-Message): (.*)\r\n/iU', $text, $match)) {
32+
return trim($match[1]);
33+
}
34+
35+
return null;
36+
}
37+
38+
public function findUserId(string $text): ?int
39+
{
40+
// Try X-ListMember / X-User first
41+
if (preg_match('/(?:X-ListMember|X-User): (.*)\r\n/iU', $text, $match)) {
42+
$user = trim($match[1]);
43+
if (str_contains($user, '@')) {
44+
return $this->subscriberManager->getSubscriberByEmail($user)?->getId();
45+
} elseif (preg_match('/^\d+$/', $user)) {
46+
return (int)$user;
47+
} elseif ($user !== '') {
48+
return $this->subscriberManager->getSubscriberByEmail($user)?->getId();
49+
}
50+
}
51+
// Fallback: parse any email in the body and see if it is a subscriber
52+
if (preg_match_all('/[._a-zA-Z0-9-]+@[.a-zA-Z0-9-]+/', $text, $regs)) {
53+
foreach ($regs[0] as $email) {
54+
$id = $this->subscriberManager->getSubscriberByEmail($email)?->getId();
55+
if ($id) {
56+
return $id;
57+
}
58+
}
59+
}
60+
61+
return null;
62+
}
63+
}

0 commit comments

Comments
 (0)