diff --git a/imapclient/imapclient.py b/imapclient/imapclient.py index e86cefcf..d1a11ff9 100644 --- a/imapclient/imapclient.py +++ b/imapclient/imapclient.py @@ -15,7 +15,7 @@ from datetime import date, datetime from logging import getLogger, LoggerAdapter from operator import itemgetter -from typing import List, Optional +from typing import List, Optional, Tuple from . import exceptions, imap4, response_lexer, tls from .datetime_util import datetime_to_INTERNALDATE, format_criteria_date @@ -42,6 +42,12 @@ "FLAGGED", "DRAFT", "RECENT", + "ALL", + "ARCHIVE", + "DRAFTS", + "JUNK", + "SENT", + "TRASH", ] @@ -76,6 +82,11 @@ if "MOVE" not in imaplib.Commands: imaplib.Commands["MOVE"] = ("AUTH", "SELECTED") +# .. and LIST-EXTENDED for RFC6154. +if "LIST-EXTENDED" not in imaplib.Commands: + imaplib.Commands["LIST-EXTENDED"] = ("AUTH", "SELECTED") + + # System flags DELETED = rb"\Deleted" SEEN = rb"\Seen" @@ -727,6 +738,29 @@ def xlist_folders(self, directory="", pattern="*"): """ return self._do_list("XLIST", directory, pattern) + @require_capability("SPECIAL-USE") + def list_special_folders( + self, directory: str = "", pattern: str = "*" + ) -> List[Tuple[Tuple[bytes, ...], bytes, str]]: + """List folders with SPECIAL-USE attributes. + + This method uses the RFC 6154 LIST extension to efficiently query + folders with special-use attributes without listing all folders. + + Args: + directory: Base directory to search (default: "") + pattern: Pattern to match folder names (default: "*") + + Returns: + List of (flags, delimiter, name) tuples. Flags may contain + special-use attributes like b'\\Sent', b'\\Archive', etc. + + Raises: + CapabilityError: If server doesn't support SPECIAL-USE + IMAPClientError: If the LIST command fails + """ + return self._do_list_extended("LIST", directory, pattern, "SPECIAL-USE") + def list_sub_folders(self, directory="", pattern="*"): """Return a list of subscribed folders on the server as ``(flags, delimiter, name)`` tuples. @@ -744,6 +778,16 @@ def _do_list(self, cmd, directory, pattern): typ, dat = self._imap._untagged_response(typ, dat, cmd) return self._proc_folder_list(dat) + def _do_list_extended(self, cmd, directory, pattern, selection_option): + directory = self._normalise_folder(directory) + pattern = self._normalise_folder(pattern) + typ, dat = self._imap._simple_command( + cmd, directory, pattern, "RETURN", "(%s)" % selection_option + ) + self._checkok(cmd, typ, dat) + typ, dat = self._imap._untagged_response(typ, dat, cmd) + return self._proc_folder_list(dat) + def _proc_folder_list(self, folder_data): # Filter out empty strings and None's. # This also deals with the special case of - no 'untagged' @@ -777,10 +821,15 @@ def find_special_folder(self, folder_flag): Returns the name of the folder if found, or None otherwise. """ # Detect folder by looking for known attributes - # TODO: avoid listing all folders by using extended LIST (RFC6154) - for folder in self.list_folders(): - if folder and len(folder[0]) > 0 and folder_flag in folder[0]: - return folder[2] + # Use RFC 6154 SPECIAL-USE extension when available for efficiency + if self.has_capability("SPECIAL-USE"): + for folder in self.list_special_folders(): + if folder and len(folder[0]) > 0 and folder_flag in folder[0]: + return folder[2] + else: + for folder in self.list_folders(): + if folder and len(folder[0]) > 0 and folder_flag in folder[0]: + return folder[2] # Detect folder by looking for common names # We only look for folders in the "personal" namespace of the user @@ -1026,11 +1075,83 @@ def close_folder(self): """ return self._command_and_check("close", unpack=True) - def create_folder(self, folder): - """Create *folder* on the server returning the server response string.""" - return self._command_and_check( - "create", self._normalise_folder(folder), unpack=True - ) + def create_folder(self, folder: str, special_use: Optional[bytes] = None) -> str: + """Create folder with optional SPECIAL-USE attribute. + + Creates a new folder on the IMAP server. When special_use is provided, + the folder will be marked with the specified special-use attribute + according to RFC 6154. + + Args: + folder: Folder name to create + special_use: Optional special-use attribute (e.g., SENT, DRAFTS, JUNK, etc.) + Must be one of the RFC 6154 constants: ALL, ARCHIVE, DRAFTS, + JUNK, SENT, TRASH + + Returns: + Server response string + + Raises: + CapabilityError: If server doesn't support CREATE-SPECIAL-USE when + special_use is provided + IMAPClientError: If the CREATE command fails + + Examples: + Standard folder creation (existing behavior): + >>> client.create_folder("INBOX.NewFolder") + + Special-use folder creation (new feature): + >>> client.create_folder("INBOX.MySent", special_use=imapclient.SENT) + >>> client.create_folder("INBOX.MyDrafts", special_use=imapclient.DRAFTS) + """ + if special_use is not None: + return self._create_folder_with_special_use(folder, special_use) + else: + # Use standard CREATE command (existing behavior) + return self._command_and_check( + "create", self._normalise_folder(folder), unpack=True + ) + + def _create_folder_with_special_use(self, folder: str, special_use: bytes) -> str: + """Create folder with SPECIAL-USE attribute using RFC 6154 CREATE extension. + + Args: + folder: Folder name to create + special_use: Special-use attribute (bytes) + + Returns: + Server response string + + Raises: + CapabilityError: If server doesn't support CREATE-SPECIAL-USE + IMAPClientError: If special_use is not a valid RFC 6154 constant + """ + if not self.has_capability("CREATE-SPECIAL-USE"): + raise exceptions.CapabilityError( + "Server does not support CREATE-SPECIAL-USE" + ) + + # Validate special_use against known RFC 6154 constants + valid_special_uses = {ALL, ARCHIVE, DRAFTS, JUNK, SENT, TRASH} + if special_use not in valid_special_uses: + raise exceptions.IMAPClientError( + f"Invalid special_use attribute: {special_use!r}. " + f"Must be one of: {', '.join(attr.decode('ascii') for attr in valid_special_uses)}" + ) + + normalized_folder = self._normalise_folder(folder) + + # Construct CREATE command with USE attribute: CREATE "folder" (USE (special_use)) + use_clause = b"(USE (" + special_use + b"))" + + # Note: Using direct _imap.create() instead of _command_and_check() to pass + # the additional use_clause parameter for RFC 6154 CREATE-SPECIAL-USE extension. + # Error handling remains functionally equivalent to standard pattern. + typ, data = self._imap.create(normalized_folder, use_clause) + if typ != "OK": + raise exceptions.IMAPClientError(f"CREATE command failed: {data}") + + return data[0].decode("ascii", "replace") def rename_folder(self, old_name, new_name): """Change the name of a folder on the server.""" diff --git a/livetest.py b/livetest.py index afdafa9c..cb7a2102 100644 --- a/livetest.py +++ b/livetest.py @@ -1119,6 +1119,98 @@ def test_getacl(self): rights = self.client.getacl(folder) self.assertIn(who, [u for u, r in rights]) + def test_list_special_folders(self): + self.skip_unless_capable("SPECIAL-USE") + + # Test basic list_special_folders functionality + special_folders = self.client.list_special_folders() + self.assertIsInstance(special_folders, list) + + # Verify the response format + for flags, delimiter, name in special_folders: + self.assertIsInstance(flags, tuple) + self.assertIsInstance(delimiter, bytes) + self.assertIsInstance(name, (str, bytes)) + + # Check if any flag is a special-use attribute + special_use_flags = [ + b"\\Archive", + b"\\Drafts", + b"\\Flagged", + b"\\Junk", + b"\\Sent", + b"\\Trash", + b"\\All", + ] + has_special_use = any(flag in special_use_flags for flag in flags) + if has_special_use: + # If we found a special-use folder, that's good evidence + # that the RFC 6154 implementation is working + break + + # Test with pattern parameter + inbox_folders = self.client.list_special_folders("", "INBOX*") + self.assertIsInstance(inbox_folders, list) + + def test_create_special_use_folder(self): + """Test special-use folder creation against live server.""" + self.skip_unless_capable("CREATE-SPECIAL-USE") + + # Import the special-use constants + from imapclient import ARCHIVE, DRAFTS, SENT, TRASH + + # Test folder names with timestamp to avoid conflicts + timestamp = str(int(time.time())) + test_folders = [ + (f"TestSent_{timestamp}", SENT), + (f"TestDrafts_{timestamp}", DRAFTS), + (f"TestArchive_{timestamp}", ARCHIVE), + (f"TestTrash_{timestamp}", TRASH), + ] + + created_folders = [] + + try: + for folder_name, special_use in test_folders: + # Create the special-use folder + result = self.client.create_folder( + folder_name, special_use=special_use + ) + self.assertIsInstance(result, str) + created_folders.append(folder_name) + + # Verify the folder was created by listing it + folders = self.client.list_folders() + folder_names = [name for flags, delimiter, name in folders] + self.assertIn(folder_name, folder_names) + + # If server supports SPECIAL-USE, verify the special-use attribute + if self.client.has_capability("SPECIAL-USE"): + special_folders = self.client.list_special_folders() + + # Look for our created folder in special folders list + found_special = False + for flags, delimiter, name in special_folders: + if name == folder_name: + # Verify it has the expected special-use attribute + self.assertIn(special_use, flags) + found_special = True + break + + # Note: Some servers may not immediately reflect the special-use + # attribute in LIST responses, so we don't fail if not found + if found_special: + print(f"Verified special-use attribute for {folder_name}") + + finally: + # Clean up: delete test folders + for folder_name in created_folders: + try: + self.client.delete_folder(folder_name) + except IMAPClientError: + # Folder might already be deleted or not exist + pass + LiveTest.conf = conf LiveTest.use_uid = use_uid diff --git a/tests/test_imapclient.py b/tests/test_imapclient.py index 0819eb72..4c57d5ab 100644 --- a/tests/test_imapclient.py +++ b/tests/test_imapclient.py @@ -278,6 +278,197 @@ def test_find_special_folder_without_special_use_nor_namespace(self): self.assertEqual(folder, "Sent Items") +class TestSpecialUseFolders(IMAPClientTest): + def test_list_special_folders_capability_required(self): + """Test that SPECIAL-USE capability is required.""" + self.client._cached_capabilities = (b"IMAP4REV1",) + self.assertRaises(CapabilityError, self.client.list_special_folders) + + def test_list_special_folders_basic(self): + """Test basic special folder listing.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ( + "LIST", + [ + b'(\\HasNoChildren \\Drafts) "/" "INBOX.Drafts"', + b'(\\HasNoChildren \\Sent) "/" "INBOX.Sent"', + b'(\\HasNoChildren \\Archive) "/" "INBOX.Archive"', + ], + ) + + folders = self.client.list_special_folders() + + self.client._imap._simple_command.assert_called_once_with( + "LIST", b'""', b'"*"', "RETURN", "(SPECIAL-USE)" + ) + self.assertEqual(len(folders), 3) + self.assertEqual( + folders[0], ((b"\\HasNoChildren", b"\\Drafts"), b"/", "INBOX.Drafts") + ) + self.assertEqual( + folders[1], ((b"\\HasNoChildren", b"\\Sent"), b"/", "INBOX.Sent") + ) + self.assertEqual( + folders[2], ((b"\\HasNoChildren", b"\\Archive"), b"/", "INBOX.Archive") + ) + + def test_list_special_folders_with_params(self): + """Test list_special_folders with directory and pattern parameters.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ( + "LIST", + [ + b'(\\HasNoChildren \\Trash) "/" "INBOX.Trash"', + ], + ) + + folders = self.client.list_special_folders("INBOX", "T*") + + self.client._imap._simple_command.assert_called_once_with( + "LIST", b'"INBOX"', b'"T*"', "RETURN", "(SPECIAL-USE)" + ) + self.assertEqual(len(folders), 1) + self.assertEqual( + folders[0], ((b"\\HasNoChildren", b"\\Trash"), b"/", "INBOX.Trash") + ) + + def test_list_special_folders_server_response_empty(self): + """Test list_special_folders with empty server response.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ("LIST", [None]) + + folders = self.client.list_special_folders() + + self.client._imap._simple_command.assert_called_once_with( + "LIST", b'""', b'"*"', "RETURN", "(SPECIAL-USE)" + ) + self.assertEqual(folders, []) + + def test_list_special_folders_server_response_multiple_attributes(self): + """Test parsing of server responses with multiple special-use attributes.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ( + "LIST", + [ + b'(\\HasNoChildren \\Sent \\Archive) "/" "Multi-Purpose"', + b'(\\Trash) "/" "Trash"', + ], + ) + + folders = self.client.list_special_folders() + + self.assertEqual(len(folders), 2) + self.assertEqual( + folders[0], + ((b"\\HasNoChildren", b"\\Sent", b"\\Archive"), b"/", "Multi-Purpose"), + ) + self.assertEqual(folders[1], ((b"\\Trash",), b"/", "Trash")) + + def test_list_special_folders_imap_command_failed(self): + """Test list_special_folders handles IMAP command failures.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("NO", [b"Command failed"]) + + self.assertRaises(IMAPClientError, self.client.list_special_folders) + + def test_find_special_folder_uses_rfc6154_when_available(self): + """Test that find_special_folder uses RFC 6154 when SPECIAL-USE capability exists.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ( + "LIST", + [ + b'(\\HasNoChildren \\Sent) "/" "Sent Messages"', + ], + ) + + folder = self.client.find_special_folder(b"\\Sent") + + # Should call LIST with SPECIAL-USE extension, not regular LIST + self.client._imap._simple_command.assert_called_once_with( + "LIST", b'""', b'"*"', "RETURN", "(SPECIAL-USE)" + ) + self.assertEqual(folder, "Sent Messages") + + def test_find_special_folder_fallback_without_capability(self): + """Test find_special_folder falls back to list_folders when no SPECIAL-USE.""" + self.client._cached_capabilities = (b"IMAP4REV1",) # No SPECIAL-USE + + # First call: list_folders() - looks for folders by attributes + # Second call: list_folders(pattern="Sent") - looks for folders by name + call_count = 0 + + def mock_simple_command(cmd, *args): + nonlocal call_count + call_count += 1 + return ("OK", [b"something"]) + + def mock_untagged_response(typ, dat, cmd): + if call_count == 1: + # First call returns no folders with \Sent attribute + return ("LIST", [b'(\\HasNoChildren) "/" "INBOX"']) + else: + # Second call (by name pattern) returns "Sent Items" + return ("LIST", [b'(\\HasNoChildren) "/" "Sent Items"']) + + self.client._imap._simple_command.side_effect = mock_simple_command + self.client._imap._untagged_response.side_effect = mock_untagged_response + + folder = self.client.find_special_folder(b"\\Sent") + + # Should call regular LIST command without SPECIAL-USE (twice - by attributes then by name) + self.assertEqual(self.client._imap._simple_command.call_count, 2) + self.client._imap._simple_command.assert_any_call("LIST", b'""', b'"*"') + self.client._imap._simple_command.assert_any_call("LIST", b'""', b'"Sent"') + self.assertEqual(folder, "Sent Items") + + def test_list_special_folders_with_folder_encoding_disabled(self): + """Test list_special_folders with folder_encode disabled.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client.folder_encode = False + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ( + "LIST", + [ + b'(\\HasNoChildren \\Sent) "/" "Hello&AP8-world"', + ], + ) + + folders = self.client.list_special_folders() + + self.client._imap._simple_command.assert_called_once_with( + "LIST", '""', '"*"', "RETURN", "(SPECIAL-USE)" + ) + self.assertEqual(len(folders), 1) + # Name should remain as bytes when folder_encode is False + self.assertEqual( + folders[0], ((b"\\HasNoChildren", b"\\Sent"), b"/", b"Hello&AP8-world") + ) + + def test_list_special_folders_with_utf7_decoding(self): + """Test list_special_folders with UTF-7 folder name decoding.""" + self.client._cached_capabilities = (b"SPECIAL-USE",) + self.client._imap._simple_command.return_value = ("OK", [b"something"]) + self.client._imap._untagged_response.return_value = ( + "LIST", + [ + b'(\\HasNoChildren \\Sent) "/" "Hello&AP8-world"', + ], + ) + + folders = self.client.list_special_folders() + + self.assertEqual(len(folders), 1) + # Name should be decoded from UTF-7 when folder_encode is True (default) + self.assertEqual( + folders[0], ((b"\\HasNoChildren", b"\\Sent"), b"/", "Hello\xffworld") + ) + + class TestSelectFolder(IMAPClientTest): def test_normal(self): self.client._command_and_check = Mock() @@ -1104,6 +1295,208 @@ def test_tagged_response_with_parse_error(self): client._consume_until_tagged_response(sentinel.tag, b"IDLE") +class TestCreateSpecialUseFolder(IMAPClientTest): + def test_create_folder_backward_compatibility(self): + """Test that create_folder() works unchanged without special_use parameter.""" + self.client._command_and_check = Mock() + self.client._command_and_check.return_value = b"OK CREATE completed" + + result = self.client.create_folder("INBOX.TestFolder") + + self.client._command_and_check.assert_called_once_with( + "create", b'"INBOX.TestFolder"', unpack=True + ) + self.assertEqual(result, b"OK CREATE completed") + + def test_create_folder_with_special_use_capability_required(self): + """Test CREATE-SPECIAL-USE capability requirement when special_use provided.""" + self.client._cached_capabilities = (b"IMAP4REV1",) + + self.assertRaises( + CapabilityError, + self.client.create_folder, + "INBOX.TestSent", + special_use=b"\\Sent", + ) + + def test_create_folder_with_special_use_basic(self): + """Test basic special-use folder creation with valid attributes.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + result = self.client.create_folder("INBOX.MySent", special_use=b"\\Sent") + + self.client._imap.create.assert_called_once_with( + b'"INBOX.MySent"', b"(USE (\\Sent))" + ) + self.assertEqual(result, "CREATE completed") + + def test_create_folder_with_special_use_sent_constant(self): + """Test creation with SENT RFC 6154 constant.""" + from imapclient import SENT + + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + result = self.client.create_folder("INBOX.MySent", special_use=SENT) + + self.client._imap.create.assert_called_once_with( + b'"INBOX.MySent"', b"(USE (\\Sent))" + ) + self.assertEqual(result, "CREATE completed") + + def test_create_folder_with_special_use_drafts_constant(self): + """Test creation with DRAFTS RFC 6154 constant.""" + from imapclient import DRAFTS + + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + result = self.client.create_folder("INBOX.MyDrafts", special_use=DRAFTS) + + self.client._imap.create.assert_called_once_with( + b'"INBOX.MyDrafts"', b"(USE (\\Drafts))" + ) + self.assertEqual(result, "CREATE completed") + + def test_create_folder_with_special_use_all_rfc6154_constants(self): + """Test creation with all RFC 6154 constants (SENT, DRAFTS, JUNK, etc.).""" + from imapclient import ALL, ARCHIVE, DRAFTS, JUNK, SENT, TRASH + + test_cases = [ + (SENT, "INBOX.MySent", b"(USE (\\Sent))"), + (DRAFTS, "INBOX.MyDrafts", b"(USE (\\Drafts))"), + (JUNK, "INBOX.MyJunk", b"(USE (\\Junk))"), + (ARCHIVE, "INBOX.MyArchive", b"(USE (\\Archive))"), + (TRASH, "INBOX.MyTrash", b"(USE (\\Trash))"), + (ALL, "INBOX.MyAll", b"(USE (\\All))"), + ] + + for special_use, folder_name, expected_use_clause in test_cases: + with self.subTest(special_use=special_use): + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + result = self.client.create_folder(folder_name, special_use=special_use) + + self.client._imap.create.assert_called_once_with( + b'"' + folder_name.encode("ascii") + b'"', expected_use_clause + ) + self.assertEqual(result, "CREATE completed") + + def test_create_folder_with_special_use_invalid_attribute(self): + """Test error handling for invalid special_use attributes.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + + with self.assertRaises(IMAPClientError) as cm: + self.client.create_folder("INBOX.TestFolder", special_use=b"\\Invalid") + + self.assertIn("Invalid special_use attribute", str(cm.exception)) + self.assertIn("\\Invalid", str(cm.exception)) + self.assertIn("Must be one of", str(cm.exception)) + + def test_create_folder_with_special_use_no_capability_error(self): + """Test CapabilityError when CREATE-SPECIAL-USE not supported.""" + # Test with different capability sets that don't include CREATE-SPECIAL-USE + capability_sets = [ + (b"IMAP4REV1",), + (b"SPECIAL-USE",), # Has SPECIAL-USE but not CREATE-SPECIAL-USE + (b"IMAP4REV1", b"SPECIAL-USE"), + ] + + for capabilities in capability_sets: + with self.subTest(capabilities=capabilities): + self.client._cached_capabilities = capabilities + + with self.assertRaises(CapabilityError) as cm: + self.client.create_folder("INBOX.TestFolder", special_use=b"\\Sent") + + self.assertIn("CREATE-SPECIAL-USE", str(cm.exception)) + + def test_create_folder_with_special_use_imap_command_construction(self): + """Test proper IMAP CREATE command construction with USE attribute.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + # Test with folder name that needs normalization + result = self.client.create_folder("TestFolder", special_use=b"\\Archive") + + # Verify the folder name was normalized and USE clause formatted correctly + self.client._imap.create.assert_called_once_with( + b'"TestFolder"', b"(USE (\\Archive))" + ) + self.assertEqual(result, "CREATE completed") + + def test_create_folder_with_special_use_server_response_handling(self): + """Test server response handling for successful CREATE command.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + + # Test different server response formats + test_responses = [ + [b"CREATE completed"], + [b"CREATE completed successfully"], + [b"OK Mailbox created"], + ] + + for response in test_responses: + with self.subTest(response=response): + self.client._imap.create.return_value = ("OK", response) + + result = self.client.create_folder( + "INBOX.TestFolder", special_use=b"\\Sent" + ) + + self.assertEqual(result, response[0].decode("ascii", "replace")) + + def test_create_folder_with_special_use_server_error_handling(self): + """Test server error handling for failed CREATE command.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ( + "NO", + [b"CREATE failed - folder exists"], + ) + + with self.assertRaises(IMAPClientError) as cm: + self.client.create_folder("INBOX.TestFolder", special_use=b"\\Sent") + + self.assertIn("CREATE command failed", str(cm.exception)) + self.assertIn("CREATE failed - folder exists", str(cm.exception)) + + def test_create_folder_with_special_use_unicode_folder_names(self): + """Test special-use folder creation with Unicode folder names.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + # Test with Unicode folder name + result = self.client.create_folder("INBOX.Боксы", special_use=b"\\Archive") + + self.client._imap.create.assert_called_once() + # Verify folder name was properly encoded + call_args = self.client._imap.create.call_args[0] + self.assertIsInstance(call_args[0], bytes) + self.assertEqual(call_args[1], b"(USE (\\Archive))") + self.assertEqual(result, "CREATE completed") + + def test_create_folder_with_special_use_empty_folder_name(self): + """Test behavior with empty folder name.""" + self.client._cached_capabilities = (b"CREATE-SPECIAL-USE",) + self.client._imap.create = Mock() + self.client._imap.create.return_value = ("OK", [b"CREATE completed"]) + + result = self.client.create_folder("", special_use=b"\\Sent") + + self.client._imap.create.assert_called_once_with(b'""', b"(USE (\\Sent))") + self.assertEqual(result, "CREATE completed") + + class TestSocket(IMAPClientTest): def test_issues_warning_for_deprecating_sock_property(self): mock_sock = Mock()