Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
63 / 63 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
1 / 1 |
| ClientMailbox | |
100.00% |
62 / 62 |
|
100.00% |
8 / 8 |
23 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| stringOrNull | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| decodeJsonArray | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
| fromRow | |
100.00% |
49 / 49 |
|
100.00% |
1 / 1 |
9 | |||
| isDefaultForUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| copyWith | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| withIsDefault | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| withDecryptedPassword | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Modules\Mail\Domain\Model; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | /** |
| 12 | * Client User and Business Mailbox Aggregate Model. |
| 13 | * |
| 14 | * Encapsulates incoming protocol (IMAP/JMAP), dedicated outgoing SMTP transport, |
| 15 | * special folder mappings, and synchronization sequence metadata. |
| 16 | * |
| 17 | * @package App\Modules\Mail\Domain\Model |
| 18 | */ |
| 19 | final readonly class ClientMailbox |
| 20 | { |
| 21 | /** |
| 22 | * ClientMailbox constructor. |
| 23 | * |
| 24 | * @param int $id Primary ID. |
| 25 | * @param string $name Mailbox label / account name. |
| 26 | * @param string $email Email address. |
| 27 | * @param string $fromName Display sender name. |
| 28 | * @param int $mailServerId Associated MailServer ID. |
| 29 | * @param string $protocolType Protocol type ('imap', 'jmap', 'exchange'). |
| 30 | * @param string $username Login username. |
| 31 | * @param string $password AEAD encrypted password. |
| 32 | * @param string|null $smtpHost Dedicated SMTP hostname or null for server fallback. |
| 33 | * @param int|null $smtpPort Dedicated SMTP port or null for server fallback. |
| 34 | * @param string $smtpEncryption Dedicated SMTP encryption ('ssl', 'tls', 'starttls', 'none'). |
| 35 | * @param string $smtpAuthMethod Dedicated SMTP auth method ('login', 'plain', 'cram-md5'). |
| 36 | * @param string|null $smtpUsername Dedicated SMTP username or null for mailbox fallback. |
| 37 | * @param string|null $smtpPassword Dedicated AEAD encrypted SMTP password or null for fallback. |
| 38 | * @param string|null $smtpFromEmail Dedicated SMTP sender email or null for mailbox fallback. |
| 39 | * @param string|null $smtpFromName Dedicated SMTP sender name or null for mailbox fallback. |
| 40 | * @param string|null $smtpReplyToEmail Dedicated reply-to email. |
| 41 | * @param string $folderInbox Mapped Inbox folder name. |
| 42 | * @param string|null $folderSent Mapped Sent folder name. |
| 43 | * @param string|null $folderDrafts Mapped Drafts folder name. |
| 44 | * @param string|null $folderTrash Mapped Trash folder name. |
| 45 | * @param string|null $folderSpam Mapped Spam folder name. |
| 46 | * @param string|null $folderArchive Mapped Archive folder name. |
| 47 | * @param string|null $signatureHtml Configured HTML signature or null. |
| 48 | * @param array<string, mixed>|null $folderOrder Custom folder ordering. |
| 49 | * @param array<string, mixed>|null $cachedFolders Cached folder tree hierarchy. |
| 50 | * @param int|null $highestModseq IMAP CONDSTORE highest modification sequence. |
| 51 | * @param int|null $uidNext IMAP UIDNEXT expected next message UID. |
| 52 | * @param int|null $uidValidity IMAP UIDVALIDITY mailbox identifier. |
| 53 | * @param bool $isShared Whether shared among team members. |
| 54 | * @param bool $isDefault Whether this is the default/primary mailbox. |
| 55 | * @param bool $showBadge Whether unread count is included in topbar badge. |
| 56 | * @param bool $notifyNewMail Whether new mail triggers desktop notifications. |
| 57 | * @param int $sortOrder Display ordering sequence. |
| 58 | * @param string $syncStatus Status: ok, auth_error, connection_error, pending. |
| 59 | * @param string|null $lastSyncAt Last successful sync timestamp. |
| 60 | * @param bool $isActive Active flag. |
| 61 | * @param int $owner Owner user ID. |
| 62 | * @param array<int> $coOwners List of co-owner user IDs. |
| 63 | * @param array<int> $defaultForUsers List of user IDs for whom this mailbox is default. |
| 64 | */ |
| 65 | public function __construct( |
| 66 | public int $id, |
| 67 | public string $name, |
| 68 | public string $email, |
| 69 | public string $fromName, |
| 70 | public int $mailServerId, |
| 71 | public string $protocolType = 'imap', |
| 72 | public string $username = '', |
| 73 | public string $password = '', |
| 74 | public ?string $smtpHost = null, |
| 75 | public ?int $smtpPort = 465, |
| 76 | public string $smtpEncryption = 'ssl', |
| 77 | public string $smtpAuthMethod = 'login', |
| 78 | public ?string $smtpUsername = null, |
| 79 | public ?string $smtpPassword = null, |
| 80 | public ?string $smtpFromEmail = null, |
| 81 | public ?string $smtpFromName = null, |
| 82 | public ?string $smtpReplyToEmail = null, |
| 83 | public string $folderInbox = 'INBOX', |
| 84 | public ?string $folderSent = null, |
| 85 | public ?string $folderDrafts = null, |
| 86 | public ?string $folderTrash = null, |
| 87 | public ?string $folderSpam = null, |
| 88 | public ?string $folderArchive = null, |
| 89 | public ?string $signatureHtml = null, |
| 90 | public ?array $folderOrder = null, |
| 91 | public ?array $cachedFolders = null, |
| 92 | public ?int $highestModseq = null, |
| 93 | public ?int $uidNext = null, |
| 94 | public ?int $uidValidity = null, |
| 95 | public bool $isShared = false, |
| 96 | public bool $isDefault = false, |
| 97 | public bool $showBadge = true, |
| 98 | public bool $notifyNewMail = true, |
| 99 | public int $sortOrder = 0, |
| 100 | public string $syncStatus = 'ok', |
| 101 | public ?string $lastSyncAt = null, |
| 102 | public bool $isActive = true, |
| 103 | public int $owner = 1, |
| 104 | public array $coOwners = [], |
| 105 | public array $defaultForUsers = [] |
| 106 | ) { |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Extracts a nullable string from array if set and not empty. |
| 111 | * |
| 112 | * @param array<string, mixed> $row Database record. |
| 113 | * @param string $key Field key. |
| 114 | * @return string|null Non-empty string or null. |
| 115 | */ |
| 116 | private static function stringOrNull(array $row, string $key): ?string |
| 117 | { |
| 118 | return isset($row[$key]) && $row[$key] !== '' ? (string) $row[$key] : null; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Safely decodes a JSON or array field from database row. |
| 123 | * |
| 124 | * @param mixed $value Raw field value. |
| 125 | * @return array<string, mixed>|null Decoded array or null. |
| 126 | */ |
| 127 | private static function decodeJsonArray(mixed $value): ?array |
| 128 | { |
| 129 | if (is_string($value) && $value !== '') { |
| 130 | $decoded = json_decode($value, true); |
| 131 | return is_array($decoded) ? $decoded : null; |
| 132 | } |
| 133 | |
| 134 | return is_array($value) ? $value : null; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Instantiates ClientMailbox from database row. |
| 139 | * |
| 140 | * @param array<string, mixed> $row Database record. |
| 141 | * @param array<int> $coOwners Optional co-owner user IDs. |
| 142 | * @return self Populated instance. |
| 143 | */ |
| 144 | public static function fromRow(array $row, array $coOwners = []): self |
| 145 | { |
| 146 | $defaultForUsers = []; |
| 147 | if (isset($row['default_for_users'])) { |
| 148 | $decoded = self::decodeJsonArray($row['default_for_users']); |
| 149 | if (is_array($decoded)) { |
| 150 | $defaultForUsers = array_map('intval', array_values($decoded)); |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | return new self( |
| 155 | id: (int) ($row['id'] ?? 0), |
| 156 | name: (string) ($row['name'] ?? ''), |
| 157 | email: (string) ($row['email'] ?? ''), |
| 158 | fromName: (string) ($row['from_name'] ?? ''), |
| 159 | mailServerId: (int) ($row['mail_server_id'] ?? 0), |
| 160 | protocolType: (string) ($row['protocol_type'] ?? 'imap'), |
| 161 | username: (string) ($row['username'] ?? ''), |
| 162 | password: (string) ($row['password'] ?? ''), |
| 163 | smtpHost: self::stringOrNull($row, 'smtp_host'), |
| 164 | smtpPort: isset($row['smtp_port']) ? (int) $row['smtp_port'] : null, |
| 165 | smtpEncryption: (string) ($row['smtp_encryption'] ?? 'ssl'), |
| 166 | smtpAuthMethod: (string) ($row['smtp_auth_method'] ?? 'login'), |
| 167 | smtpUsername: self::stringOrNull($row, 'smtp_username'), |
| 168 | smtpPassword: self::stringOrNull($row, 'smtp_password'), |
| 169 | smtpFromEmail: self::stringOrNull($row, 'smtp_from_email'), |
| 170 | smtpFromName: self::stringOrNull($row, 'smtp_from_name'), |
| 171 | smtpReplyToEmail: self::stringOrNull($row, 'smtp_reply_to_email'), |
| 172 | folderInbox: (string) ($row['folder_inbox'] ?? 'INBOX'), |
| 173 | folderSent: self::stringOrNull($row, 'folder_sent'), |
| 174 | folderDrafts: self::stringOrNull($row, 'folder_drafts'), |
| 175 | folderTrash: self::stringOrNull($row, 'folder_trash'), |
| 176 | folderSpam: self::stringOrNull($row, 'folder_spam'), |
| 177 | folderArchive: self::stringOrNull($row, 'folder_archive'), |
| 178 | signatureHtml: self::stringOrNull($row, 'signature_html'), |
| 179 | folderOrder: self::decodeJsonArray($row['folder_order'] ?? null), |
| 180 | cachedFolders: self::decodeJsonArray($row['cached_folders'] ?? null), |
| 181 | highestModseq: isset($row['highest_modseq']) ? (int) $row['highest_modseq'] : null, |
| 182 | uidNext: isset($row['uid_next']) ? (int) $row['uid_next'] : null, |
| 183 | uidValidity: isset($row['uid_validity']) ? (int) $row['uid_validity'] : null, |
| 184 | isShared: (bool) ($row['is_shared'] ?? false), |
| 185 | isDefault: (bool) ($row['is_default'] ?? false), |
| 186 | showBadge: (bool) ($row['show_badge'] ?? true), |
| 187 | notifyNewMail: (bool) ($row['notify_new_mail'] ?? true), |
| 188 | sortOrder: (int) ($row['sort_order'] ?? 0), |
| 189 | syncStatus: (string) ($row['sync_status'] ?? 'ok'), |
| 190 | lastSyncAt: self::stringOrNull($row, 'last_sync_at'), |
| 191 | isActive: isset($row['is_active']) |
| 192 | ? (bool) ($row['is_active']) |
| 193 | : (($row['status'] ?? 'active') === 'active' && ((int) ($row['special_access'] ?? 1) === 1)), |
| 194 | owner: (int) ($row['owner'] ?? 1), |
| 195 | coOwners: $coOwners, |
| 196 | defaultForUsers: $defaultForUsers |
| 197 | ); |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Checks if this mailbox is configured as default for the given user ID. |
| 202 | * |
| 203 | * @param int $userId Target user ID. |
| 204 | * @return bool True if mailbox is marked default for user. |
| 205 | */ |
| 206 | public function isDefaultForUser(int $userId): bool |
| 207 | { |
| 208 | return in_array($userId, $this->defaultForUsers, true); |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Returns a clone of this mailbox with updated isDefault flag. |
| 213 | * |
| 214 | * @param bool $isDefault New default flag status for the requesting user context. |
| 215 | * @return self New instance with updated isDefault flag. |
| 216 | */ |
| 217 | /** |
| 218 | * Creates a mutated clone of this mailbox with updated properties. |
| 219 | * |
| 220 | * @param array<string, mixed> $overrides Overridden properties. |
| 221 | */ |
| 222 | private function copyWith(array $overrides): self |
| 223 | { |
| 224 | $props = get_object_vars($this); |
| 225 | foreach ($overrides as $key => $value) { |
| 226 | $props[$key] = $value; |
| 227 | } |
| 228 | |
| 229 | return new self(...$props); |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Returns a clone of this mailbox with updated isDefault flag. |
| 234 | * |
| 235 | * @param bool $isDefault New default flag status for the requesting user context. |
| 236 | * @return self New instance with updated isDefault flag. |
| 237 | */ |
| 238 | public function withIsDefault(bool $isDefault): self |
| 239 | { |
| 240 | return $this->copyWith(['isDefault' => $isDefault]); |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Returns a clone of this mailbox with a decrypted password for protocol connections. |
| 245 | * |
| 246 | * @param string $plainPassword Plaintext decrypted password. |
| 247 | * @return self New instance with decrypted password. |
| 248 | */ |
| 249 | public function withDecryptedPassword(string $plainPassword): self |
| 250 | { |
| 251 | return $this->copyWith(['password' => $plainPassword]); |
| 252 | } |
| 253 | } |