Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.16% covered (success)
92.16%
47 / 51
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebmailFolderOrganizer
92.00% covered (success)
92.00%
46 / 50
66.67% covered (warning)
66.67%
4 / 6
30.46
0.00% covered (danger)
0.00%
0 / 1
 isSystemFolder
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 reorderSiblings
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
5.07
 findFolderByPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getParentPath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getFolderSiblings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 swapSiblingPosition
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
8.19
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\Folder;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Mail\Domain\Model\ClientMailbox;
12use App\Modules\Mail\Domain\Model\MailFolderDto;
13
14/**
15 * Webmail folder reorganization and position organizer.
16 *
17 * Provides operations for detecting system-protected folders, resolving
18 * parent paths, extracting sibling nodes, and calculating swapped/adjacent positions.
19 *
20 * @package App\Modules\Mail\Application\Service\Folder
21 */
22final readonly class WebmailFolderOrganizer
23{
24    /**
25     * Checks if a folder is a standard system folder that cannot be deleted or nested.
26     *
27     * @param ClientMailbox $mailbox    Mailbox configuration.
28     * @param string        $folderPath Folder path.
29     * @return bool True if system folder.
30     */
31    public function isSystemFolder(ClientMailbox $mailbox, string $folderPath): bool
32    {
33        $lower = strtolower($folderPath);
34        return $lower === 'inbox'
35            || $folderPath === $mailbox->folderInbox
36            || $folderPath === $mailbox->folderSent
37            || $folderPath === $mailbox->folderTrash
38            || $folderPath === $mailbox->folderDrafts
39            || $folderPath === $mailbox->folderSpam
40            || $folderPath === $mailbox->folderArchive;
41    }
42
43    /**
44     * Slices and reorders siblings list by placing source before or after target.
45     *
46     * @param array<string> $siblings   List of sibling folder paths.
47     * @param string        $sourcePath Path to move.
48     * @param string        $targetPath Reference path.
49     * @param string        $position   'before' or 'after'.
50     * @return array<string> Reordered siblings.
51     */
52    public function reorderSiblings(
53        array $siblings,
54        string $sourcePath,
55        string $targetPath,
56        string $position
57    ): array {
58        $effectivePos = $position;
59        if (strtolower($targetPath) === 'inbox' && $effectivePos === 'before') {
60            $effectivePos = 'after';
61        }
62
63        $filtered = array_values(array_filter(
64            $siblings,
65            static fn(string $p): bool => $p !== $sourcePath
66        ));
67
68        $targetIdx = array_search($targetPath, $filtered, true);
69        if ($targetIdx === false) {
70            $filtered[] = $sourcePath;
71            return $filtered;
72        }
73
74        $insertIdx = $effectivePos === 'before' ? (int) $targetIdx : ((int) $targetIdx + 1);
75        array_splice($filtered, $insertIdx, 0, [$sourcePath]);
76
77        return $filtered;
78    }
79
80    /**
81     * Finds folder DTO by path in flat list.
82     *
83     * @param array<MailFolderDto> $flatList List of folder DTOs.
84     * @param string $path Folder path.
85     * @return MailFolderDto|null Matching folder or null.
86     */
87    public function findFolderByPath(array $flatList, string $path): ?MailFolderDto
88    {
89        foreach ($flatList as $f) {
90            if ($f->path === $path) {
91                return $f;
92            }
93        }
94
95        return null;
96    }
97
98    /**
99     * Resolves parent path for folder using its delimiter.
100     *
101     * @param string $path Target folder path.
102     * @param string $delim Folder hierarchy delimiter.
103     * @return string|null Parent path or null if top-level.
104     */
105    public function getParentPath(string $path, string $delim): ?string
106    {
107        $lastDelim = strrpos($path, $delim);
108        return $lastDelim !== false ? substr($path, 0, $lastDelim) : null;
109    }
110
111    /**
112     * Retrieves sibling folder paths sharing the same parent path.
113     *
114     * @param array<MailFolderDto> $flatList Flat list of folder DTOs.
115     * @param string|null $parentPath Parent folder path.
116     * @return array<string> List of sibling folder paths.
117     */
118    public function getFolderSiblings(array $flatList, ?string $parentPath): array
119    {
120        $siblings = [];
121        foreach ($flatList as $f) {
122            $fDelim = $f->delimiter !== '' ? $f->delimiter : '/';
123            $fLastDelim = strrpos($f->path, $fDelim);
124            $fParent = $fLastDelim !== false ? substr($f->path, 0, $fLastDelim) : null;
125            if ($fParent === $parentPath) {
126                $siblings[] = $f->path;
127            }
128        }
129
130        return $siblings;
131    }
132
133    /**
134     * Swaps sibling position based on direction ('up' or 'down').
135     *
136     * @param array<string> $siblings Array of sibling paths.
137     * @param string $folderPath Folder path to move.
138     * @param string $direction Direction to move ('up' or 'down').
139     * @return array<string>|null Swapped siblings array or null if no move possible.
140     */
141    public function swapSiblingPosition(array $siblings, string $folderPath, string $direction): ?array
142    {
143        $idx = array_search($folderPath, $siblings, true);
144        if ($idx === false) {
145            return null;
146        }
147
148        $targetIdx = match ($direction) {
149            'up'    => $idx > 0 ? $idx - 1 : null,
150            'down'  => $idx < count($siblings) - 1 ? $idx + 1 : null,
151            default => null,
152        };
153
154        if ($targetIdx === null) {
155            return null;
156        }
157
158        $temp = $siblings[$targetIdx];
159        $siblings[$targetIdx] = $folderPath;
160        $siblings[$idx] = $temp;
161
162        return $siblings;
163    }
164}