Skip to content

Commit 951730b

Browse files
authored
Merge pull request #277 from buggregator/issue/274
SMTP module: Fix response code and Cyrillic character handling issues
2 parents f01ff72 + b40f7f7 commit 951730b

31 files changed

+807
-105
lines changed

.github/workflows/docker-dev-image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
push: true
4444
build-args: |
4545
APP_VERSION=${{ steps.previoustag.outputs.tag }}
46-
FRONTEND_IMAGE_TAG=dev
46+
FRONTEND_IMAGE_TAG=latest
4747
BRANCH=${{ steps.previoustag.outputs.tag }}
4848
tags:
4949
ghcr.io/${{ github.repository }}:dev, ghcr.io/${{ github.repository }}:${{ steps.previoustag.outputs.tag }}

.github/workflows/phpunit-mysql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
6363

6464
- name: Restore Composer Cache
65-
uses: actions/cache@v2
65+
uses: actions/cache@v3
6666
with:
6767
path: ${{ steps.composer-cache.outputs.dir }}
6868
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}

.github/workflows/phpunit-pgsql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
6161

6262
- name: Restore Composer Cache
63-
uses: actions/cache@v2
63+
uses: actions/cache@v3
6464
with:
6565
path: ${{ steps.composer-cache.outputs.dir }}
6666
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ protoc-gen-php-grpc*
1313
.db
1414
.sqlhistory
1515
*Zone.Identifier
16+
.context

.php-cs-fixer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
(new PhpCsFixer\Finder())
1616
->files()
1717
->name('*.php')
18-
->in([__DIR__ . '/app/src', __DIR__ . '/app/modules']),
18+
->in([__DIR__ . '/app/src', __DIR__ . '/app/modules', __DIR__ . '/tests']),
1919
)
2020
->setCacheFile('.cache/.php-cs-fixer.cache');

app/modules/Smtp/Application/Mail/Message.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public function getBccs(): array
3636
return \array_values(
3737
\array_filter($this->allRecipients, function (string $recipient) {
3838
foreach (\array_merge($this->recipients, $this->ccs) as $publicRecipient) {
39-
if (\str_contains($publicRecipient, $recipient)) {
39+
// Use email from the public recipient array
40+
if (isset($publicRecipient['email']) && \str_contains($publicRecipient['email'], $recipient)) {
4041
return false;
4142
}
4243
}

app/modules/Smtp/Application/Storage/Message.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
namespace Modules\Smtp\Application\Storage;
66

7-
use Modules\Smtp\Application\Mail\Parser;
8-
97
final class Message
108
{
119
public function __construct(
@@ -45,21 +43,32 @@ public function setFrom(string $from): void
4543

4644
public function appendBody(string $body): void
4745
{
48-
$this->body .= \preg_replace("/^(\.\.)/m", '.', $body);
46+
// Handle escaped periods at the beginning of lines per SMTP spec
47+
$safeBody = \preg_replace("/^(\.\.)/m", '.', $body);
48+
49+
// Ensure body is properly appended even with multi-byte characters
50+
$this->body .= $safeBody;
4951
}
5052

5153
public function bodyHasEos(): bool
5254
{
53-
return \str_ends_with($this->body, "\r\n.\r\n");
55+
// More robust check for end of stream marker
56+
// This handles potential encoding issues with multi-byte characters
57+
return \mb_substr($this->body, -5) === "\r\n.\r\n";
5458
}
5559

5660
public function getBody(): string
5761
{
58-
return \str_replace("\r\n.\r\n", '', $this->body);
62+
// Remove the end of stream marker in a way that's safe for multi-byte strings
63+
if ($this->bodyHasEos()) {
64+
return \mb_substr($this->body, 0, \mb_strlen($this->body) - 5);
65+
}
66+
67+
return $this->body;
5968
}
6069

6170
public function parse(): \Modules\Smtp\Application\Mail\Message
6271
{
63-
return (new Parser())->parse($this->getBody());
72+
return ParserFactory::getInstance()->create()->parse($this->getBody(), $this->recipients);
6473
}
6574
}
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 Modules\Smtp\Application\Storage;
6+
7+
use Modules\Smtp\Application\Mail\Parser;
8+
9+
/**
10+
* Factory for creating Parser instances
11+
* This class exists primarily to facilitate testing by allowing
12+
* the Parser instantiation to be mocked or overridden.
13+
*
14+
* @internal
15+
*/
16+
final class ParserFactory
17+
{
18+
// Static instance for singleton pattern
19+
private static ?self $instance = null;
20+
21+
// The Parser instance or null
22+
private ?Parser $parser = null;
23+
24+
/**
25+
* Get the factory instance
26+
*/
27+
public static function getInstance(): self
28+
{
29+
if (!self::$instance instanceof \Modules\Smtp\Application\Storage\ParserFactory) {
30+
self::$instance = new self();
31+
}
32+
33+
return self::$instance;
34+
}
35+
36+
/**
37+
* Reset the factory instance (useful for testing)
38+
*/
39+
public static function reset(): void
40+
{
41+
self::$instance = null;
42+
}
43+
44+
/**
45+
* Set a custom Parser instance
46+
*
47+
* @param Parser $parser The Parser instance to use
48+
*/
49+
public function setParser(Parser $parser): void
50+
{
51+
$this->parser = $parser;
52+
}
53+
54+
/**
55+
* Create a Parser instance
56+
*
57+
* @return Parser The Parser instance
58+
*/
59+
public function create(): Parser
60+
{
61+
return $this->parser ?? new Parser();
62+
}
63+
}

app/modules/Smtp/Interfaces/TCP/Service.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public function handle(Request $request): ResponseInterface
6565
$response = $this->makeResponse(ResponseMessage::closing(), close: true);
6666
$message = $this->emailBodyStorage->cleanup($request->connectionUuid);
6767
} elseif ($request->body === "DATA\r\n") {
68+
// Reset the body to empty string when starting a new DATA command
69+
// This prevents confusion between multiple DATA commands in the same session
70+
$message->body = '';
6871
$response = $this->makeResponse(ResponseMessage::provideBody());
6972
$message->waitBody = true;
7073
} elseif ($request->body === "RSET\r\n") {
@@ -75,13 +78,16 @@ public function handle(Request $request): ResponseInterface
7578
} elseif ($message->waitBody) {
7679
$message->appendBody($request->body);
7780

78-
$response = $this->makeResponse(ResponseMessage::ok());
79-
81+
// FIX: Only send one response when data ends
8082
if ($message->bodyHasEos()) {
8183
$uuid = $this->dispatchMessage($message->parse(), project: $message->username);
82-
8384
$response = $this->makeResponse(ResponseMessage::accepted($uuid));
8485
$dispatched = true;
86+
// Reset the waitBody flag to false since we've processed the message
87+
$message->waitBody = false;
88+
} else {
89+
// Only send "OK" response if we're not at the end of data
90+
$response = $this->makeResponse(ResponseMessage::ok());
8591
}
8692
}
8793

app/modules/context.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json'
2+
3+
documents:
4+
- description: Events module
5+
outputPath: module/events.md
6+
sources:
7+
- type: file
8+
sourcePaths:
9+
- ./Events
10+
11+
- description: SMTP module
12+
outputPath: module/smtp.md
13+
sources:
14+
- type: file
15+
sourcePaths:
16+
- ./Smtp
17+
- ../../docs/smtp.md

0 commit comments

Comments
 (0)