Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.51% covered (success)
94.51%
86 / 91
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MailApiController
94.44% covered (success)
94.44%
85 / 90
83.33% covered (warning)
83.33%
5 / 6
21.08
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 testSmtpConnection
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 testServerConnection
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 testMailboxConnection
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
4.22
 enqueue
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
5
 validateEnqueuePayload
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Mail\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Mail\Application\Contract\MailSenderServiceInterface;
12use App\Modules\Mail\Domain\Repository\MailRepositoryInterface;
13use App\Shared\Infrastructure\Http\ApiResponseTrait;
14use Psr\Http\Message\ResponseFactoryInterface;
15use Psr\Http\Message\ResponseInterface;
16use Psr\Http\Message\ServerRequestInterface;
17use Throwable;
18
19/**
20 * REST API Controller for Outgoing Mail and SMTP Infrastructure.
21 *
22 * Exposes versioned endpoints for testing live SMTP connectivity, queuing
23 * templated email messages, and inspecting delivery status.
24 *
25 * @package App\Modules\Mail\Presentation\Api
26 */
27final readonly class MailApiController
28{
29    use ApiResponseTrait;
30
31    /**
32     * MailApiController constructor.
33     *
34     * @param ResponseFactoryInterface   $responseFactory PSR-17 response factory.
35     * @param MailRepositoryInterface    $repository      Mail repository.
36     * @param MailSenderServiceInterface $sender          Mail sender service.
37     */
38    public function __construct(
39        private ResponseFactoryInterface $responseFactory,
40        private MailRepositoryInterface $repository,
41        private MailSenderServiceInterface $sender
42    ) {
43    }
44
45    /**
46     * Handles POST /api/v1/mail/smtp/{id}/test-connection
47     *
48     * @param ServerRequestInterface $request HTTP request.
49     * @param int                    $smtpId  SMTP server configuration ID.
50     * @return ResponseInterface JSON response with test results.
51     */
52    public function testSmtpConnection(ServerRequestInterface $request, int $smtpId): ResponseInterface
53    {
54        $smtp = $this->repository->findSmtpById($smtpId);
55        if ($smtp === null) {
56            return $this->jsonError(
57                $this->responseFactory,
58                sprintf('SMTP configuration with ID %d not found.', $smtpId),
59                404
60            );
61        }
62
63        $payload = $this->parseJsonBody($request);
64        $testRecipient = (string)($payload['recipient_email'] ?? $smtp->fromEmail);
65
66        if (filter_var($testRecipient, FILTER_VALIDATE_EMAIL) === false) {
67            return $this->jsonValidation($this->responseFactory, [
68                'recipient_email' => ['A valid test recipient email address is required.'],
69            ]);
70        }
71
72        $result = $this->sender->testConnection($smtp, $testRecipient);
73        return $result['success']
74            ? $this->jsonSuccess($this->responseFactory, [
75                'smtp_id'   => $smtpId,
76                'recipient' => $testRecipient,
77                'message'   => $result['message'],
78            ])
79            : $this->jsonError($this->responseFactory, $result['message'], 502);
80    }
81
82    /**
83     * Handles POST /api/v1/mail/servers/{id}/test-connection
84     *
85     * @param int $serverId Mail server configuration ID.
86     * @return ResponseInterface JSON response with test results.
87     */
88    public function testServerConnection(int $serverId): ResponseInterface
89    {
90        $server = $this->repository->findMailServerById($serverId);
91        if ($server === null) {
92            return $this->jsonError(
93                $this->responseFactory,
94                sprintf('Mail server with ID %d not found.', $serverId),
95                404
96            );
97        }
98
99        $result = $this->sender->testMailServerConnection($server);
100        return $result['success']
101            ? $this->jsonSuccess($this->responseFactory, [
102                'server_id' => $serverId,
103                'message'   => $result['message'],
104            ])
105            : $this->jsonError($this->responseFactory, $result['message'], 502);
106    }
107
108    /**
109     * Handles POST /api/v1/mail/mailboxes/{id}/test-connection
110     *
111     * @param int $mailboxId Mailbox configuration ID.
112     * @return ResponseInterface JSON response with test results.
113     */
114    public function testMailboxConnection(int $mailboxId): ResponseInterface
115    {
116        $mailbox = $this->repository->findClientMailboxById($mailboxId);
117        if ($mailbox === null) {
118            return $this->jsonError(
119                $this->responseFactory,
120                sprintf('Client mailbox with ID %d not found.', $mailboxId),
121                404
122            );
123        }
124
125        $server = $this->repository->findMailServerById($mailbox->mailServerId);
126        if ($server === null) {
127            return $this->jsonError(
128                $this->responseFactory,
129                sprintf('Associated mail server with ID %d not found.', $mailbox->mailServerId),
130                404
131            );
132        }
133
134        $result = $this->sender->testMailboxConnection($mailbox, $server);
135        return $result['success']
136            ? $this->jsonSuccess($this->responseFactory, [
137                'mailbox_id' => $mailboxId,
138                'message'    => $result['message'],
139            ])
140            : $this->jsonError($this->responseFactory, $result['message'], 502);
141    }
142
143    /**
144     * Handles POST /api/v1/mail/enqueue
145     *
146     * @param ServerRequestInterface $request HTTP request with template and parameters.
147     * @return ResponseInterface JSON response with enqueued message ID.
148     */
149    public function enqueue(ServerRequestInterface $request): ResponseInterface
150    {
151        $payload = $this->parseJsonBody($request);
152        $templateCode = (string)($payload['template_code'] ?? '');
153        $recipientEmail = (string)($payload['recipient_email'] ?? '');
154        $validationErrors = $this->validateEnqueuePayload($templateCode, $recipientEmail);
155
156        if ($validationErrors !== []) {
157            return $this->jsonValidation($this->responseFactory, $validationErrors);
158        }
159
160        $recipientName = isset($payload['recipient_name']) ? (string)$payload['recipient_name'] : null;
161        $params = is_array($payload['params'] ?? null) ? $payload['params'] : [];
162        $priority = (string)($payload['priority'] ?? 'normal');
163        $lang = (string)($payload['language_code'] ?? 'en');
164
165        try {
166            /** @var array<string, scalar> $typedParams */
167            $typedParams = array_filter($params, static fn($v): bool => is_scalar($v));
168
169            $queueId = $this->sender->enqueueFromTemplate(
170                templateCode: $templateCode,
171                recipientEmail: $recipientEmail,
172                recipientName: $recipientName,
173                params: $typedParams,
174                priority: $priority,
175                languageCode: $lang
176            );
177
178            return $this->jsonSuccess($this->responseFactory, [
179                'queue_id' => $queueId,
180                'status'   => 'enqueued',
181            ], 201);
182        } catch (Throwable $e) {
183            return $this->jsonError($this->responseFactory, $e->getMessage(), 500);
184        }
185    }
186
187    /**
188     * Validates required enqueue parameters.
189     *
190     * @param string $templateCode   Template identifier.
191     * @param string $recipientEmail Recipient email.
192     * @return array<string, list<string>> Validation error dictionary.
193     */
194    private function validateEnqueuePayload(string $templateCode, string $recipientEmail): array
195    {
196        $errors = [];
197        if ($templateCode === '') {
198            $errors['template_code'] = ['Template code is required.'];
199        }
200        if ($recipientEmail === '') {
201            $errors['recipient_email'] = ['Recipient email is required.'];
202        } elseif (filter_var($recipientEmail, FILTER_VALIDATE_EMAIL) === false) {
203            $errors['recipient_email'] = ['A valid recipient email address is required.'];
204        }
205        return $errors;
206    }
207}