Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.07% covered (warning)
86.07%
105 / 122
58.33% covered (warning)
58.33%
7 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
MailboxSettingsManager
85.95% covered (warning)
85.95%
104 / 121
58.33% covered (warning)
58.33%
7 / 12
51.87
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
 resolveDefaultMailboxesForUser
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
11.02
 updateMailboxSettings
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
4
 findUserMailbox
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 resetOtherDefaultMailboxes
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
5.50
 computeTargetDefaultUsers
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
8.30
 saveDefaultForUsers
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 reorderMailboxes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 computeMovedMailboxIds
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 moveMailbox
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 parseToggleSetting
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 parseNullableString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
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\Application\Service\Auth;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Mail\Domain\Model\ClientMailbox;
12use PDO;
13
14/**
15 * Enterprise Mailbox Settings & Ordering Manager.
16 *
17 * Manages user-specific mailbox preferences, single-default resolution across multiple accounts,
18 * folder mappings, display badges, signatures, and custom sort ordering.
19 *
20 * @package App\Modules\Mail\Application\Service\Auth
21 */
22final readonly class MailboxSettingsManager
23{
24    /**
25     * MailboxSettingsManager constructor.
26     *
27     * @param PDO    $pdo         Database connection handle.
28     * @param string $tablePrefix Database table prefix.
29     */
30    public function __construct(
31        private PDO $pdo,
32        private string $tablePrefix = 'a_'
33    ) {
34    }
35
36    /**
37     * Resolves and marks the single default mailbox for the given user context.
38     *
39     * @param array<ClientMailbox> $mailboxes Mailboxes accessible by user.
40     * @param int                  $userId    Current user ID.
41     * @return array<ClientMailbox> Updated list with accurate isDefault flags.
42     */
43    public function resolveDefaultMailboxesForUser(array $mailboxes, int $userId): array
44    {
45        if ($mailboxes === []) {
46            return [];
47        }
48
49        if (count($mailboxes) === 1) {
50            return [$mailboxes[0]->withIsDefault(true)];
51        }
52
53        $defaultIndex = null;
54        foreach ($mailboxes as $idx => $mbox) {
55            if ($mbox->isDefaultForUser($userId)) {
56                $defaultIndex = $idx;
57                break;
58            }
59        }
60
61        if ($defaultIndex === null) {
62            foreach ($mailboxes as $idx => $mbox) {
63                if ($mbox->isDefault && $mbox->owner === $userId) {
64                    $defaultIndex = $idx;
65                    break;
66                }
67            }
68        }
69
70        if ($defaultIndex === null) {
71            $defaultIndex = 0;
72        }
73
74        $result = [];
75        foreach ($mailboxes as $idx => $mbox) {
76            $result[] = $mbox->withIsDefault($idx === $defaultIndex);
77        }
78
79        return $result;
80    }
81
82    /**
83     * Updates mailbox settings and default flags.
84     *
85     * @param array<ClientMailbox> $userMailboxes List of mailboxes accessible by user.
86     * @param int                  $mailboxId     Target mailbox ID.
87     * @param int                  $userId        Current user ID for authorization check.
88     * @param array<string, mixed> $data          Settings payload.
89     * @return bool True if updated successfully.
90     */
91    public function updateMailboxSettings(
92        array $userMailboxes,
93        int $mailboxId,
94        int $userId,
95        array $data
96    ): bool {
97        $targetMailbox = $this->findUserMailbox($userMailboxes, $mailboxId);
98        if ($targetMailbox === null) {
99            return false;
100        }
101
102        $isDefault = $this->parseToggleSetting($data, 'is_default', 0);
103        if ($isDefault === 1) {
104            $this->resetOtherDefaultMailboxes($userMailboxes, $mailboxId, $userId);
105        }
106
107        $defaultUsersJson = $this->computeTargetDefaultUsers($targetMailbox, $userId, $isDefault);
108        $showBadge = $this->parseToggleSetting($data, 'show_badge', 1);
109        $notifyNewMail = $this->parseToggleSetting($data, 'notify_new_mail', 1);
110
111        $table = $this->tablePrefix . 'mod_client_mailboxes_records';
112        $sql = "UPDATE `{$table}` SET "
113            . '`from_name` = :from_name, '
114            . '`smtp_reply_to_email` = :smtp_reply_to_email, '
115            . '`folder_sent` = :folder_sent, '
116            . '`folder_drafts` = :folder_drafts, '
117            . '`folder_trash` = :folder_trash, '
118            . '`folder_spam` = :folder_spam, '
119            . '`folder_archive` = :folder_archive, '
120            . '`signature_html` = :signature_html, '
121            . '`is_default` = :is_default, '
122            . '`default_for_users` = :default_for_users, '
123            . '`show_badge` = :show_badge, '
124            . '`notify_new_mail` = :notify_new_mail, '
125            . '`updated_at` = CURRENT_TIMESTAMP(6) '
126            . 'WHERE `id` = :id LIMIT 1';
127
128        $stmt = $this->pdo->prepare($sql);
129        return $stmt->execute([
130            ':id'                  => $mailboxId,
131            ':from_name'           => trim((string) ($data['from_name'] ?? '')),
132            ':smtp_reply_to_email' => $this->parseNullableString($data, 'smtp_reply_to_email'),
133            ':folder_sent'         => $this->parseNullableString($data, 'folder_sent'),
134            ':folder_drafts'       => $this->parseNullableString($data, 'folder_drafts'),
135            ':folder_trash'        => $this->parseNullableString($data, 'folder_trash'),
136            ':folder_spam'         => $this->parseNullableString($data, 'folder_spam'),
137            ':folder_archive'      => $this->parseNullableString($data, 'folder_archive'),
138            ':signature_html'      => !empty($data['signature_html']) ? (string) $data['signature_html'] : null,
139            ':is_default'          => $isDefault,
140            ':default_for_users'   => $defaultUsersJson,
141            ':show_badge'          => $showBadge,
142            ':notify_new_mail'     => $notifyNewMail,
143        ]);
144    }
145
146    /**
147     * Finds mailbox by ID within an accessible list.
148     *
149     * @param array<ClientMailbox> $mailboxes
150     */
151    public function findUserMailbox(array $mailboxes, int $mailboxId): ?ClientMailbox
152    {
153        foreach ($mailboxes as $mbox) {
154            if ($mbox->id === $mailboxId) {
155                return $mbox;
156            }
157        }
158        return null;
159    }
160
161    /**
162     * Resets is_default flags on other mailboxes for user.
163     *
164     * @param array<ClientMailbox> $mailboxes
165     */
166    public function resetOtherDefaultMailboxes(array $mailboxes, int $mailboxId, int $userId): void
167    {
168        $table = $this->tablePrefix . 'mod_client_mailboxes_records';
169        $resetSql = "UPDATE `{$table}` SET `is_default` = 0 WHERE `owner` = :user_id";
170        $resetStmt = $this->pdo->prepare($resetSql);
171        $resetStmt->execute([':user_id' => $userId]);
172
173        foreach ($mailboxes as $otherMbox) {
174            if ($otherMbox->id !== $mailboxId && $otherMbox->isDefaultForUser($userId)) {
175                $otherUsers = array_values(array_filter(
176                    $otherMbox->defaultForUsers,
177                    static fn(int $id): bool => $id !== $userId
178                ));
179                $this->saveDefaultForUsers($otherMbox->id, $otherUsers);
180            }
181        }
182    }
183
184    /**
185     * Computes updated default_for_users JSON array.
186     */
187    public function computeTargetDefaultUsers(ClientMailbox $targetMailbox, int $userId, int $isDefault): ?string
188    {
189        $targetUsers = $targetMailbox->defaultForUsers;
190        $hasUser = in_array($userId, $targetUsers, true);
191        if ($isDefault === 1 && !$hasUser) {
192            $targetUsers[] = $userId;
193        } elseif ($isDefault === 0 && $hasUser) {
194            $targetUsers = array_values(array_filter(
195                $targetUsers,
196                static fn(int $id): bool => $id !== $userId
197            ));
198        }
199
200        return empty($targetUsers) ? null : json_encode(array_values(array_unique($targetUsers)));
201    }
202
203    /**
204     * Persists updated default_for_users JSON array to database.
205     *
206     * @param int        $mailboxId Mailbox primary key.
207     * @param array<int> $users     List of user IDs.
208     */
209    public function saveDefaultForUsers(int $mailboxId, array $users): void
210    {
211        $encoded = empty($users) ? null : json_encode(array_values(array_unique($users)));
212        $table = $this->tablePrefix . 'mod_client_mailboxes_records';
213        $stmt = $this->pdo->prepare(
214            "UPDATE `{$table}` SET `default_for_users` = :val WHERE `id` = :id LIMIT 1"
215        );
216        $stmt->execute([':val' => $encoded, ':id' => $mailboxId]);
217    }
218
219    /**
220     * Updates ordering sequences for user mailboxes.
221     *
222     * @param array<ClientMailbox> $userMailboxes Allowed mailboxes.
223     * @param array<int>           $mailboxIds   Ordered list of mailbox IDs.
224     * @return bool True if reorder succeeded.
225     */
226    public function reorderMailboxes(array $userMailboxes, array $mailboxIds): bool
227    {
228        $allowedIds = array_map(static fn(ClientMailbox $m): int => $m->id, $userMailboxes);
229
230        $order = 10;
231        $table = $this->tablePrefix . 'mod_client_mailboxes_records';
232        $sql = "UPDATE `{$table}` SET `sort_order` = :order WHERE `id` = :id LIMIT 1";
233        $stmt = $this->pdo->prepare($sql);
234
235        foreach ($mailboxIds as $rawId) {
236            $mboxId = (int) $rawId;
237            if (in_array($mboxId, $allowedIds, true)) {
238                $stmt->execute([':order' => $order, ':id' => $mboxId]);
239                $order += 10;
240            }
241        }
242
243        return true;
244    }
245
246    /**
247     * Computes reordered mailbox IDs after moving target mailbox up or down.
248     *
249     * @param array<ClientMailbox> $userMailboxes Allowed mailboxes.
250     * @param int                  $mailboxId     Target mailbox ID.
251     * @param string               $direction     Direction ('up' or 'down').
252     * @return array<int>|null New ordered IDs array or null if move not possible.
253     */
254    public function computeMovedMailboxIds(array $userMailboxes, int $mailboxId, string $direction): ?array
255    {
256        $ids = array_map(static fn(ClientMailbox $m): int => $m->id, $userMailboxes);
257        $currentIndex = array_search($mailboxId, $ids, true);
258        if ($currentIndex === false) {
259            return null;
260        }
261
262        $targetIndex = $direction === 'up' ? $currentIndex - 1 : $currentIndex + 1;
263        if ($targetIndex < 0 || $targetIndex >= count($ids)) {
264            return null;
265        }
266
267        $temp = $ids[$currentIndex];
268        $ids[$currentIndex] = $ids[$targetIndex];
269        $ids[$targetIndex] = $temp;
270
271        return $ids;
272    }
273
274    /**
275     * Shifts a mailbox up or down in sort order.
276     *
277     * @param array<ClientMailbox> $userMailboxes Allowed mailboxes.
278     * @param int                  $mailboxId     Target mailbox ID.
279     * @param string               $direction     Direction ('up' or 'down').
280     * @return bool True if moved.
281     */
282    public function moveMailbox(array $userMailboxes, int $mailboxId, string $direction): bool
283    {
284        $newIds = $this->computeMovedMailboxIds($userMailboxes, $mailboxId, $direction);
285        if ($newIds === null) {
286            return false;
287        }
288
289        return $this->reorderMailboxes($userMailboxes, $newIds);
290    }
291
292    /**
293     * Extracts a boolean toggle flag from input data.
294     *
295     * @param array<string, mixed> $data    Input parameters.
296     * @param string               $key     Key name.
297     * @param int                  $default Default value if unset.
298     * @return int 1 or 0.
299     */
300    public function parseToggleSetting(array $data, string $key, int $default = 1): int
301    {
302        if (!isset($data[$key])) {
303            return $default;
304        }
305        return !empty($data[$key]) ? 1 : 0;
306    }
307
308    /**
309     * Parses an optional trimmed string from an array.
310     *
311     * @param array<string, mixed> $data Input parameters.
312     * @param string               $key  Key name.
313     * @return string|null Trimmed string or null if empty.
314     */
315    public function parseNullableString(array $data, string $key): ?string
316    {
317        return !empty($data[$key]) ? trim((string) $data[$key]) : null;
318    }
319}