Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.11% covered (success)
98.11%
52 / 53
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImapFolderManager
98.08% covered (success)
98.08%
51 / 52
80.00% covered (warning)
80.00%
4 / 5
14
0.00% covered (danger)
0.00%
0 / 1
 listFolders
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 getFolderStatus
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 createFolder
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 renameFolder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deleteFolder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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\Infrastructure\Protocol\Imap\Client;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Mail\Application\Service\MimeDecoderService;
12use App\Modules\Mail\Domain\Model\MailFolderDto;
13use App\Modules\Mail\Domain\Model\MailFolderStatusDto;
14use Throwable;
15
16/**
17 * Enterprise IMAP Mailbox Folder Manager.
18 *
19 * Handles IMAP folder discovery, hierarchical path operations, creation, renaming,
20 * deletion, and metadata status queries (RFC 3501 LIST, STATUS, CREATE, RENAME, DELETE).
21 *
22 * @package App\Modules\Mail\Infrastructure\Protocol\Imap\Client
23 */
24final readonly class ImapFolderManager
25{
26    /**
27     * Lists mailbox folders with message status counters.
28     *
29     * @param ImapSocketClient        $client         Socket stream client.
30     * @param ImapResponseParser|null $responseParser Optional protocol response parser.
31     * @return array<MailFolderDto> Discovered mailbox folders.
32     */
33    public function listFolders(
34        ImapSocketClient $client,
35        ?ImapResponseParser $responseParser = null
36    ): array {
37        $lines = $client->executeCommand('LIST "" "*"');
38        $parser = $responseParser ?? new ImapResponseParser(new MimeDecoderService());
39        $folders = $parser->parseListResponse($lines);
40
41        $result = [];
42        foreach ($folders as $folder) {
43            $status = $this->getFolderStatus($client, $folder->path, $parser);
44            $result[] = new MailFolderDto(
45                id: $folder->id,
46                name: $folder->name,
47                path: $folder->path,
48                delimiter: $folder->delimiter,
49                attributes: $folder->attributes,
50                specialUse: $folder->specialUse,
51                totalMessages: $status->totalMessages,
52                unseenMessages: $status->unseenMessages,
53                uidNext: $status->uidNext,
54                uidValidity: $status->uidValidity,
55                highestModseq: $status->highestModseq
56            );
57        }
58
59        return $result;
60    }
61
62    /**
63     * Retrieves folder message status counters (total, unseen, UIDNEXT, UIDVALIDITY, HIGHESTMODSEQ).
64     *
65     * @param ImapSocketClient        $client         Socket stream client.
66     * @param string                  $folderPath     Mailbox folder path.
67     * @param ImapResponseParser|null $responseParser Optional protocol response parser.
68     * @return MailFolderStatusDto Folder status statistics.
69     */
70    public function getFolderStatus(
71        ImapSocketClient $client,
72        string $folderPath,
73        ?ImapResponseParser $responseParser = null
74    ): MailFolderStatusDto {
75        $parser = $responseParser ?? new ImapResponseParser(new MimeDecoderService());
76
77        try {
78            $cmd = sprintf(
79                'STATUS "%s" (MESSAGES UNSEEN UIDNEXT UIDVALIDITY HIGHESTMODSEQ)',
80                addcslashes($folderPath, '"\\')
81            );
82            $lines = $client->executeCommand($cmd);
83            foreach ($lines as $line) {
84                if (str_starts_with($line, '* STATUS')) {
85                    return $parser->parseStatusResponse($folderPath, $line);
86                }
87            }
88        } catch (Throwable) {
89            // Fallback for servers without CONDSTORE
90            try {
91                $cmd = sprintf(
92                    'STATUS "%s" (MESSAGES UNSEEN UIDNEXT UIDVALIDITY)',
93                    addcslashes($folderPath, '"\\')
94                );
95                $lines = $client->executeCommand($cmd);
96                foreach ($lines as $line) {
97                    if (str_starts_with($line, '* STATUS')) {
98                        return $parser->parseStatusResponse($folderPath, $line);
99                    }
100                }
101            } catch (Throwable) {
102                // Return zeroed counters if folder cannot be stat-checked
103            }
104        }
105
106        return new MailFolderStatusDto($folderPath);
107    }
108
109    /**
110     * Creates new mailbox folder.
111     *
112     * @param ImapSocketClient $client       Socket stream client.
113     * @param string           $folderName   Folder name.
114     * @param string|null      $parentFolder Optional parent folder path.
115     * @return MailFolderDto Created folder representation.
116     */
117    public function createFolder(
118        ImapSocketClient $client,
119        string $folderName,
120        ?string $parentFolder = null
121    ): MailFolderDto {
122        $path = $parentFolder !== null && $parentFolder !== ''
123            ? sprintf('%s/%s', rtrim($parentFolder, '/'), $folderName)
124            : $folderName;
125
126        $client->executeCommand(sprintf('CREATE "%s"', addcslashes($path, '"\\')));
127        return new MailFolderDto(
128            id: base64_encode($path),
129            name: $folderName,
130            path: $path
131        );
132    }
133
134    /**
135     * Renames existing mailbox folder.
136     */
137    public function renameFolder(ImapSocketClient $client, string $oldPath, string $newPath): void
138    {
139        $cmd = sprintf('RENAME "%s" "%s"', addcslashes($oldPath, '"\\'), addcslashes($newPath, '"\\'));
140        $client->executeCommand($cmd);
141    }
142
143    /**
144     * Deletes mailbox folder.
145     */
146    public function deleteFolder(ImapSocketClient $client, string $folderPath): void
147    {
148        $client->executeCommand(sprintf('DELETE "%s"', addcslashes($folderPath, '"\\')));
149    }
150}