Skip to content

Commit c29c24d

Browse files
committed
OP#2803 : fix - Changing a folder type only a user preferences
1 parent 43453d1 commit c29c24d

4 files changed

Lines changed: 106 additions & 11 deletions

File tree

app/config/settings/UserSettings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def get_all_user_settings_schema() -> list[Type[SogoSchema]]:
281281
UserContactCategorySettings,
282282
UserMailGeneralSettings,
283283
UserMailCategorySettings,
284+
UserMailViewSettings,
284285
UserExtraSettings]
285286
return all_schemas
286287

app/interface/mail/InterfaceApiMailFolder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from app.auth.User import User
66
from app.module.mail.ModuleMail import ModuleMail
7+
from app.module.user.ModuleUserProfile import ModuleUserProfile
78
from app.module.auth.ModuleUserSource import ModuleUserSource
89
from app.config.settings.DomainSettings import MailSettings, MailSettingsObj
910
from app.utils.exceptions import RequestException
@@ -29,6 +30,7 @@ def __init__(self, process_setting: ProcessSetting, user_domain_settings: dict,
2930
self.user = user
3031

3132
self.mail_module = ModuleMail(self.user, self.mail_settings)
33+
self.user_module = ModuleUserProfile(process_setting, user_domain_settings)
3234

3335
def get_folder_list(self, account_id: str) -> tuple[dict[str, Any], int]:
3436
"""Retrieve the list of mail folders for a given account and return an ApiBaseResponse.
@@ -160,7 +162,7 @@ def change_folder_type(self, account_id: str, folder_path: str, new_type: str) -
160162
:rtype: tuple[dict[str, Any], int]
161163
"""
162164
try:
163-
updated_folder = self.mail_module.change_folder_type(account_id, folder_path, new_type)
165+
updated_folder = self.user_module.change_folder_type(self.user, self.mail_settings, folder_path, new_type)
164166
return create_api_base_response(updated_folder)
165167
except RequestException as ex:
166168
logger_api.error("Request exception in change_folder_type: %s", str(ex))

app/module/user/ModuleUserProfile.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from marshmallow import EXCLUDE, ValidationError
55

66
from app.config.db import tables as tbl
7-
from app.config.settings.UserSettings import get_all_user_settings_schema, user_settings_dict
7+
from app.config.settings.UserSettings import get_all_user_settings_schema, user_settings_dict, UserMailViewSettings, UserMailViewSettingsObj
88
from app.config.settings.SogoSchema import check_data_for_sogo_schemas
99
from app.config.settings.DomainSettings import UserModuleSettingsObj, UserModuleSettings
1010
from app.utils import constants as cs
@@ -19,6 +19,7 @@
1919

2020
if TYPE_CHECKING:
2121
from app.config.settings.ProcessSetting import ProcessSetting
22+
from app.config.settings.DomainSettings import MailSettingsObj
2223
from app.manager.db.ClientSQL import ClientSQL
2324
from app.auth.User import User
2425

@@ -642,6 +643,77 @@ def update_user_preferences(self, uid:str, new_data:dict, subparent:str|None = N
642643
return new_data[real_subparent]
643644
return new_data
644645

646+
#Map a folder "type" to the domain setting (fixed by admin) and user setting (override by user)
647+
#that hold the actual name of the corresponding special folder.
648+
_SPECIAL_FOLDER_SETTINGS_MAP: dict[str, tuple[str, str]] = {
649+
cs.MAIL_FOLDER_SENT: ("SOGO_D_MAIL_SENT", "SOGO_U_SENT_FOLDER_NAME"),
650+
cs.MAIL_FOLDER_DRAFT: ("SOGO_D_MAIL_DRAFT", "SOGO_U_DRAFT_FOLDER_NAME"),
651+
cs.MAIL_FOLDER_JUNK: ("SOGO_D_MAIL_JUNK", "SOGO_U_JUNK_FOLDER_NAME"),
652+
cs.MAIL_FOLDER_TRASH: ("SOGO_D_MAIL_TRASH", "SOGO_U_TRASH_FOLDER_NAME"),
653+
cs.MAIL_FOLDER_TEMPLATE: ("SOGO_D_MAIL_TEMPLATE", "SOGO_U_TEMPLATE_FOLDER_NAME"),
654+
}
655+
656+
def change_folder_type(self, user: User, mail_settings: MailSettingsObj, folder_path: str, new_type: str) -> dict[str, Any]:
657+
"""Change the type of a mail folder.
658+
659+
Assigns the folder ``folder_path`` as the special folder for the given type (SENT, DRAFT, etc).
660+
This updates the user's preference to associate that folder name with the special type.
661+
662+
:param user: The current user
663+
:type user: User
664+
:param mail_settings: The domain mail settings (holds admin-fixed folder names)
665+
:type mail_settings: MailSettingsObj
666+
:param folder_path: The path/name of the folder to assign as the special folder
667+
:type folder_path: str
668+
:param new_type: The new type for the folder (SENT, DRAFT, JUNK, TRASH, TEMPLATE)
669+
:type new_type: str
670+
:return: The updated folder data
671+
:rtype: dict[str, Any]
672+
:raises RequestException: If the type is invalid or the folder is already assigned to another special type
673+
"""
674+
new_type_upper = new_type.upper()
675+
setting_names = self._SPECIAL_FOLDER_SETTINGS_MAP.get(new_type_upper)
676+
if setting_names is None:
677+
logger_user_profile.error("Invalid folder type requested in change_folder_type: %s", new_type)
678+
raise RequestException(err.ERROR_FOLDER_TYPE_INVALID.m, err.ERROR_FOLDER_TYPE_INVALID)
679+
_, user_setting_name = setting_names
680+
681+
# Get the current user mail view settings
682+
user_mail_view_prefs: dict = user.profile.preferences.get(UserMailViewSettings.subparent, {})
683+
# Create a UserMailViewSettingsObj to access the current special folder names
684+
user_mail_view_settings = UserMailViewSettingsObj(user_mail_view_prefs)
685+
686+
# Check that this folder is not already assigned to another special type
687+
for other_type, (other_domain_setting, other_user_setting) in self._SPECIAL_FOLDER_SETTINGS_MAP.items():
688+
if other_type == new_type_upper:
689+
continue # Skip the type we're trying to assign
690+
691+
# Get the current name for this other type, either from user settings or domain settings
692+
other_current_name = getattr(user_mail_view_settings, other_user_setting) or getattr(mail_settings, other_domain_setting)
693+
694+
# If folder_path is already assigned to another special type, reject it
695+
if folder_path == other_current_name:
696+
logger_user_profile.error(
697+
"Folder '%s' is already assigned as '%s' type, cannot assign it to '%s'",
698+
folder_path, other_type, new_type_upper
699+
)
700+
raise RequestException(err.ERROR_FOLDER_TYPE_CANNOT_CHANGE.m, err.ERROR_FOLDER_TYPE_CANNOT_CHANGE)
701+
702+
# Update user preference to assign this folder as the special type
703+
self.update_user_preferences(
704+
user.uid,
705+
{user_setting_name: folder_path},
706+
subparent=UserMailViewSettings.subparent.lower()
707+
)
708+
709+
logger_user_profile.info("Changed folder '%s' type to '%s' for uid: %s", folder_path, new_type_upper, user.uid)
710+
711+
return {
712+
"name": folder_path,
713+
"path": folder_path,
714+
"type": new_type_upper,
715+
}
716+
645717
def get_delegations_given(self, user: User) -> list[str]:
646718
"""
647719
Get all delegations given by the user

tests/test_interface/test_mail/test_InterfaceApiMailFolder.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,19 @@ def __init__(self, login_mail_server="test@example.com"):
5151

5252
class InterfaceApiMailFolderWithInjectedConf(InterfaceApiMailFolder):
5353
"""Subclass of InterfaceApiMailFolder that allows injecting user configuration directly for testing."""
54-
def __init__(self, user_conf, mail_module=None):
54+
def __init__(self, user_conf, mail_module=None, user_module=None):
5555
"""Initialize with injected user configuration for testing.
5656
5757
Does not call the parent __init__ to avoid requiring process_setting,
58-
user_domain_settings and user. Sets mail_module directly if provided.
58+
user_domain_settings and user. Sets mail_module and user_module directly if provided.
5959
"""
6060
# Does not call the parent __init__ to avoid requiring all the parameters it needs
6161
self._user_conf = user_conf # noqa: SLF001
6262
self.mail_module = mail_module
63+
self.user_module = user_module
6364
self.user = FakeUser()
6465
self.user_domain_settings = {}
66+
self.mail_settings = {} # Empty dict for testing
6567

6668

6769
class FakeModuleMail:
@@ -167,15 +169,31 @@ def change_folder_type(self, account_id, folder_path, new_type):
167169
return self.change_folder_type_result
168170

169171

170-
def make_interface(monkeypatch, fake_module, user_conf=None):
172+
class FakeModuleUserProfile:
173+
"""Fake ModuleUserProfile for testing InterfaceApiMailFolder.
174+
175+
Provides a mock implementation of change_folder_type.
176+
"""
177+
def __init__(self):
178+
self.change_folder_type_args = None
179+
self.change_folder_type_result = {"name": "Folder", "path": "Folder", "type": "SENT"}
180+
181+
def change_folder_type(self, user, mail_settings, folder_path, new_type):
182+
"""Simulate changing folder type."""
183+
self.change_folder_type_args = (folder_path, new_type)
184+
return self.change_folder_type_result
185+
186+
def make_interface(monkeypatch, fake_module, user_conf=None, fake_user_module=None):
171187
"""Create an InterfaceApiMailFolderWithInjectedConf with the fake module injected."""
172188
if user_conf is None:
173189
user_conf = {"username": "test@example.com", "password": "pass", "type": "imap"}
190+
if fake_user_module is None:
191+
fake_user_module = FakeModuleUserProfile()
174192
monkeypatch.setattr(
175193
"app.interface.mail.InterfaceApiMailFolder.ModuleMail",
176194
lambda *args, **kwargs: fake_module
177195
)
178-
return InterfaceApiMailFolderWithInjectedConf(user_conf, mail_module=fake_module)
196+
return InterfaceApiMailFolderWithInjectedConf(user_conf, mail_module=fake_module, user_module=fake_user_module)
179197

180198

181199
def patch_module_on_interface(monkeypatch, fake_module):
@@ -519,22 +537,24 @@ def test_rename_folder_module_error(monkeypatch):
519537

520538
def test_change_folder_type_success(monkeypatch):
521539
"""Test changing folder type for a valid account."""
540+
fake_user_module = FakeModuleUserProfile()
541+
fake_user_module.change_folder_type_result = {"name": "Archive", "type": "JUNK"}
522542
fake_module = FakeModuleMail()
523-
fake_module.change_folder_type_result = {"name": "Archive", "type": "JUNK"}
524-
interface = make_interface(monkeypatch, fake_module)
543+
interface = make_interface(monkeypatch, fake_module, fake_user_module=fake_user_module)
525544

526545
result, status_code = interface.change_folder_type(account_id=0, folder_path="Archive", new_type="JUNK")
527546

528547
assert status_code == 200
529548
assert result["data"]["type"] == "JUNK"
530-
assert fake_module.change_folder_type_args == ("Archive", "JUNK")
549+
assert fake_user_module.change_folder_type_args == ("Archive", "JUNK")
531550

532551

533552
def test_change_folder_type_module_error(monkeypatch):
534553
"""Test error handling when folder type change fails."""
554+
fake_user_module = FakeModuleUserProfile()
555+
fake_user_module.change_folder_type = lambda *args, **kwargs: (_ for _ in ()).throw(RequestException("Cannot change type", err.ERROR_VALIDATION_ERROR))
535556
fake_module = FakeModuleMail()
536-
fake_module.change_folder_type = lambda *args: (_ for _ in ()).throw(RequestException("Cannot change type", err.ERROR_VALIDATION_ERROR))
537-
interface = make_interface(monkeypatch, fake_module)
557+
interface = make_interface(monkeypatch, fake_module, fake_user_module=fake_user_module)
538558

539559
result, status_code = interface.change_folder_type(account_id=0, folder_path="INBOX", new_type="JUNK")
540560

0 commit comments

Comments
 (0)