imap-migration-tools/test/utils/test_imap_common.py
Javier Callico ac3307bf1d
Align names for scripts and commands (#40)
* Add comprehensive tests for IMAP email restoration functionality

- Implement tests for loading labels manifest, including both old and new formats.
- Add tests for extracting flags from the manifest.
- Create tests for parsing .eml files, including edge cases.
- Develop tests for retrieving .eml files from a directory.
- Validate configuration settings and handle missing credentials.
- Integrate tests for restoring emails from a backup, including Gmail mode and deletion of orphaned emails.
- Test progress caching and backup folder discovery.
- Ensure robust handling of email labels and flags during restoration.
- Verify that the restore process correctly interacts with a mock IMAP server.

* Add comprehensive tests for IMAP utilities and compression support

- Introduced tests for `imap_common.py` covering environment variable verification, IMAP connection handling, folder name normalization, MIME header decoding, message details extraction, duplicate detection, filename sanitization, and trash folder detection.
- Added tests for `imap_compress.py` to validate the functionality of the `_CompressedSocket` wrapper, ensuring data compression and decompression works as expected.
- Implemented integration tests to verify that compression is enabled during IMAP connection setup.
2026-02-15 08:39:37 -05:00

842 lines
28 KiB
Python

"""
Tests for imap_common.py
Tests cover:
- Environment variable verification
- IMAP connection handling
- Folder name normalization
- MIME header decoding
- Message details extraction
- Duplicate detection
- Filename sanitization
- Trash folder detection
"""
import os
import sys
from unittest.mock import Mock, patch
import pytest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../src")))
from conftest import temp_env
from utils import imap_common
class TestVerifyEnvVars:
"""Tests for verify_env_vars function."""
def test_all_vars_present(self):
"""Test returns True when all variables are set."""
with temp_env({"VAR1": "value1", "VAR2": "value2"}):
result = imap_common.verify_env_vars(["VAR1", "VAR2"])
assert result is True
def test_missing_vars(self, capsys):
"""Test returns False and prints error when variables are missing."""
with temp_env({}):
result = imap_common.verify_env_vars(["MISSING_VAR"])
assert result is False
captured = capsys.readouterr()
assert "MISSING_VAR" in captured.err
def test_partial_vars_present(self, capsys):
"""Test with some variables present and some missing."""
with temp_env({"PRESENT": "value"}):
result = imap_common.verify_env_vars(["PRESENT", "MISSING"])
assert result is False
class TestGetImapConnection:
"""Tests for get_imap_connection function."""
def test_invalid_credentials_empty(self, capsys):
"""Test returns None when credentials are empty."""
result = imap_common.get_imap_connection("", "user", "pass")
assert result is None
result = imap_common.get_imap_connection("host", "", "pass")
assert result is None
result = imap_common.get_imap_connection("host", "user", "")
assert result is None
def test_connection_error(self, capsys):
"""Test returns None on connection error."""
# Try to connect to an invalid host
result = imap_common.get_imap_connection("invalid.nonexistent.host", "u", "p")
assert result is None
captured = capsys.readouterr()
assert "Connection error" in captured.out or "Error" in captured.out
@patch("imaplib.IMAP4")
@patch("imaplib.IMAP4_SSL")
def test_scheme_parsing(self, mock_ssl, mock_imap):
"""Test scheme parsing determines SSL usage."""
# Setup mocks
mock_conn = Mock()
mock_ssl.return_value = mock_conn
mock_imap.return_value = mock_conn
# Test SSL schemes
for scheme in ["imaps", "imap+ssl", "imapssl", "ssl"]:
host = f"{scheme}://mail.example.com"
# Note: mock_conn.login is called, so we can mock it to succeed or ignore it
imap_common.get_imap_connection(host, "user", "pass")
mock_ssl.assert_called_with("mail.example.com")
mock_imap.assert_not_called()
mock_ssl.reset_mock()
mock_imap.reset_mock()
# Test non-SSL schemes
for scheme in ["imap", "tcp"]:
host = f"{scheme}://mail.example.com"
imap_common.get_imap_connection(host, "user", "pass")
mock_imap.assert_called_with("mail.example.com")
mock_ssl.assert_not_called()
mock_ssl.reset_mock()
mock_imap.reset_mock()
# Test with port
host = "imap://mail.example.com:143"
imap_common.get_imap_connection(host, "user", "pass")
mock_imap.assert_called_with("mail.example.com", 143)
def test_invalid_schemes(self, capsys):
"""Test invalid schemes raise ValueError caught by exception handler."""
imap_common.get_imap_connection("http://mail.example.com", "user", "pass")
captured = capsys.readouterr()
assert "Unsupported IMAP scheme: http" in captured.out
def test_invalid_host_url(self, capsys):
"""Test invalid host URL parsing."""
# No hostname
imap_common.get_imap_connection("imaps://", "user", "pass")
captured = capsys.readouterr()
assert "Invalid IMAP host" in captured.out
# No scheme (urllib fails to parse scheme if missing :// or just host)
# But our code checks "://" in host. If not, it assumes basic host.
# So we test a case where "://" is present but scheme is empty?
# "://hostname" parses scheme as empty string.
imap_common.get_imap_connection("://mail.example.com", "user", "pass")
captured = capsys.readouterr()
assert "Invalid IMAP host" in captured.out
class TestNormalizeFolderName:
"""Tests for normalize_folder_name function."""
def test_standard_format(self):
"""Test parsing standard IMAP list response."""
folder_info = b'(\\HasNoChildren) "/" "INBOX"'
result = imap_common.normalize_folder_name(folder_info)
assert result == "INBOX"
def test_unquoted_name(self):
"""Test parsing unquoted folder name."""
folder_info = b'(\\HasNoChildren) "/" Drafts'
result = imap_common.normalize_folder_name(folder_info)
assert result == "Drafts"
def test_with_special_flags(self):
"""Test parsing folder with special-use flags."""
folder_info = b'(\\HasNoChildren \\Trash) "/" "Trash"'
result = imap_common.normalize_folder_name(folder_info)
assert result == "Trash"
def test_gmail_folder(self):
"""Test parsing Gmail-style folder."""
folder_info = b'(\\HasNoChildren) "/" "[Gmail]/All Mail"'
result = imap_common.normalize_folder_name(folder_info)
assert result == "[Gmail]/All Mail"
def test_string_input(self):
"""Test with string input instead of bytes."""
folder_info = '(\\HasNoChildren) "/" "Archive"'
result = imap_common.normalize_folder_name(folder_info)
assert result == "Archive"
def test_fallback_parsing(self):
"""Test fallback when regex doesn't match."""
folder_info = "simple_folder"
result = imap_common.normalize_folder_name(folder_info)
assert result == "simple_folder"
class TestDecodeMimeHeader:
"""Tests for decode_mime_header function."""
def test_plain_ascii(self):
"""Test decoding plain ASCII header."""
result = imap_common.decode_mime_header("Hello World")
assert result == "Hello World"
def test_none_input(self):
"""Test with None input."""
result = imap_common.decode_mime_header(None)
assert result == "(No Subject)"
def test_empty_string(self):
"""Test with empty string."""
result = imap_common.decode_mime_header("")
assert result == "(No Subject)"
def test_utf8_encoded(self):
"""Test decoding UTF-8 MIME encoded header."""
# =?UTF-8?B?SGVsbG8gV29ybGQ=?= is "Hello World" in base64
result = imap_common.decode_mime_header("=?UTF-8?B?SGVsbG8gV29ybGQ=?=")
assert "Hello" in result
def test_quoted_printable(self):
"""Test decoding quoted-printable header."""
# =?UTF-8?Q?Hello_World?= is "Hello World" in quoted-printable
result = imap_common.decode_mime_header("=?UTF-8?Q?Hello_World?=")
assert "Hello" in result
class TestSanitizeFilename:
"""Tests for sanitize_filename function."""
def test_valid_filename(self):
"""Test that valid filename passes through."""
result = imap_common.sanitize_filename("valid_filename")
assert result == "valid_filename"
def test_invalid_characters(self):
"""Test that invalid characters are replaced."""
result = imap_common.sanitize_filename('file<>:"/\\|?*name')
assert "<" not in result
assert ">" not in result
assert ":" not in result
assert '"' not in result
assert "/" not in result
assert "\\" not in result
assert "|" not in result
assert "?" not in result
assert "*" not in result
def test_empty_input(self):
"""Test that empty input returns 'untitled'."""
result = imap_common.sanitize_filename("")
assert result == "untitled"
def test_none_input(self):
"""Test that None input returns 'untitled'."""
result = imap_common.sanitize_filename(None)
assert result == "untitled"
def test_long_filename_truncation(self):
"""Test that long filenames are truncated."""
long_name = "a" * 300
result = imap_common.sanitize_filename(long_name)
assert len(result) <= 250
def test_strip_leading_trailing(self):
"""Test that leading/trailing whitespace and dots are stripped."""
result = imap_common.sanitize_filename(" ..filename.. ")
assert not result.startswith(" ")
assert not result.startswith(".")
assert not result.endswith(" ")
assert not result.endswith(".")
class TestDetectTrashFolder:
"""Tests for detect_trash_folder function."""
def test_detect_by_special_use_flag(self):
"""Test detection via \\Trash flag."""
mock_conn = Mock()
mock_conn.list.return_value = (
"OK",
[
b'(\\HasNoChildren) "/" "INBOX"',
b'(\\HasNoChildren \\Trash) "/" "Deleted Items"',
],
)
result = imap_common.detect_trash_folder(mock_conn)
assert result == "Deleted Items"
def test_detect_gmail_trash(self):
"""Test detection of Gmail Trash folder."""
mock_conn = Mock()
mock_conn.list.return_value = (
"OK",
[
b'(\\HasNoChildren) "/" "INBOX"',
b'(\\HasNoChildren \\Trash) "/" "[Gmail]/Trash"',
],
)
result = imap_common.detect_trash_folder(mock_conn)
assert result == "[Gmail]/Trash"
def test_detect_by_name_fallback(self):
"""Test detection by common name when no flag present."""
mock_conn = Mock()
mock_conn.list.return_value = (
"OK",
[
b'(\\HasNoChildren) "/" "INBOX"',
b'(\\HasNoChildren) "/" "Trash"',
],
)
result = imap_common.detect_trash_folder(mock_conn)
assert result == "Trash"
def test_no_trash_folder(self):
"""Test returns None when no trash folder found."""
mock_conn = Mock()
mock_conn.list.return_value = (
"OK",
[
b'(\\HasNoChildren) "/" "INBOX"',
b'(\\HasNoChildren) "/" "Sent"',
],
)
result = imap_common.detect_trash_folder(mock_conn)
assert result is None
def test_list_error(self):
"""Test returns None on list error."""
mock_conn = Mock()
mock_conn.list.return_value = ("NO", [])
result = imap_common.detect_trash_folder(mock_conn)
assert result is None
def test_exception_handling(self):
"""Test returns None on exception."""
mock_conn = Mock()
mock_conn.list.side_effect = Exception("Connection error")
result = imap_common.detect_trash_folder(mock_conn)
assert result is None
class TestExtractMessageId:
"""Tests for extract_message_id function."""
def test_standard_header(self):
"""Test extracting standard Message-ID."""
header = b"Message-ID: <test@example.com>\r\nSubject: Test"
result = imap_common.extract_message_id(header)
assert result == "<test@example.com>"
def test_lowercase_header(self):
"""Test extracting Message-ID with lowercase header name."""
header = b"message-id: <lower@example.com>\r\nSubject: Test"
result = imap_common.extract_message_id(header)
assert result == "<lower@example.com>"
def test_folded_header(self):
"""Test extracting folded Message-ID."""
# BytesParser unfolds values (replacing newline+indent with space)
header = b"Message-ID: <part1\r\n part2@example.com>\r\nSubject: Test"
result = imap_common.extract_message_id(header)
assert result == "<part1 part2@example.com>"
def test_string_input(self):
"""Test with string input."""
header = "Message-ID: <str@example.com>\nSubject: Test"
result = imap_common.extract_message_id(header)
assert result == "<str@example.com>"
def test_no_message_id(self):
"""Test header without Message-ID."""
header = b"Subject: Test\r\nFrom: sender"
result = imap_common.extract_message_id(header)
assert result is None
def test_empty_input(self):
"""Test empty input."""
assert imap_common.extract_message_id(None) is None
assert imap_common.extract_message_id(b"") is None
class TestEnsureFolderExists:
"""Tests for ensure_folder_exists function."""
def test_creates_folder_successfully(self):
"""Test folder is created when it doesn't exist."""
mock_conn = Mock()
mock_conn.create.return_value = ("OK", [])
# Should not raise any exception
imap_common.ensure_folder_exists(mock_conn, "TestFolder")
mock_conn.create.assert_called_once_with('"TestFolder"')
def test_folder_already_exists(self):
"""Test exception is suppressed when folder already exists."""
mock_conn = Mock()
mock_conn.create.side_effect = Exception("Folder already exists")
# Should not raise any exception
imap_common.ensure_folder_exists(mock_conn, "ExistingFolder")
mock_conn.create.assert_called_once_with('"ExistingFolder"')
def test_inbox_folder_skipped(self):
"""Test INBOX folder is not created."""
mock_conn = Mock()
imap_common.ensure_folder_exists(mock_conn, "INBOX")
mock_conn.create.assert_not_called()
# Also test lowercase
imap_common.ensure_folder_exists(mock_conn, "inbox")
mock_conn.create.assert_not_called()
def test_empty_folder_name(self):
"""Test empty folder name is skipped."""
mock_conn = Mock()
imap_common.ensure_folder_exists(mock_conn, "")
mock_conn.create.assert_not_called()
def test_none_folder_name(self):
"""Test None folder name is skipped."""
mock_conn = Mock()
imap_common.ensure_folder_exists(mock_conn, None)
mock_conn.create.assert_not_called()
def test_server_restriction_error_suppressed(self):
"""Test server restriction errors are suppressed."""
mock_conn = Mock()
mock_conn.create.side_effect = Exception("Permission denied")
# Should not raise any exception
imap_common.ensure_folder_exists(mock_conn, "RestrictedFolder")
mock_conn.create.assert_called_once()
class TestAppendEmail:
"""Tests for append_email function."""
def test_successful_append(self):
"""Test successful email append returns True."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
ensure_folder=False,
)
assert result is True
mock_conn.append.assert_called_once_with(
'"TestFolder"',
None,
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
def test_append_with_flags_with_parentheses(self):
"""Test flag normalization when flags already have parentheses."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
flags="(\\Seen \\Flagged)",
ensure_folder=False,
)
assert result is True
# Flags should remain unchanged
mock_conn.append.assert_called_once_with(
'"TestFolder"',
"(\\Seen \\Flagged)",
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
def test_append_with_flags_without_parentheses(self):
"""Test flag normalization when flags lack parentheses."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
flags="\\Seen",
ensure_folder=False,
)
assert result is True
# Flags should be wrapped in parentheses
mock_conn.append.assert_called_once_with(
'"TestFolder"',
"(\\Seen)",
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
def test_append_with_none_flags(self):
"""Test append with None flags."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
flags=None,
ensure_folder=False,
)
assert result is True
mock_conn.append.assert_called_once_with(
'"TestFolder"',
None,
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
def test_append_with_empty_flags(self):
"""Test append with empty string flags."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
flags="",
ensure_folder=False,
)
assert result is True
# Empty flags should result in None
mock_conn.append.assert_called_once_with(
'"TestFolder"',
None,
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
def test_append_with_whitespace_only_flags(self):
"""Test append with whitespace-only flags."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
flags=" ",
ensure_folder=False,
)
assert result is True
# Whitespace-only flags should result in None
mock_conn.append.assert_called_once_with(
'"TestFolder"',
None,
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
def test_append_with_ensure_folder_enabled(self):
"""Test append with ensure_folder enabled."""
mock_conn = Mock()
mock_conn.create.return_value = ("OK", [])
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
ensure_folder=True,
)
assert result is True
mock_conn.create.assert_called_once_with('"TestFolder"')
mock_conn.append.assert_called_once()
def test_append_with_ensure_folder_disabled(self):
"""Test append with ensure_folder disabled."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
ensure_folder=False,
)
assert result is True
mock_conn.create.assert_not_called()
mock_conn.append.assert_called_once()
def test_append_failure_returns_false(self):
"""Test append returns False on failure."""
mock_conn = Mock()
mock_conn.append.return_value = ("NO", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
ensure_folder=False,
)
assert result is False
def test_append_exception_propagates(self):
"""Test append propagates exceptions so callers can handle/log."""
mock_conn = Mock()
mock_conn.append.side_effect = Exception("Connection error")
import pytest
with pytest.raises(Exception, match="Connection error"):
imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
ensure_folder=False,
)
def test_append_with_multiple_flags(self):
"""Test append with multiple flags."""
mock_conn = Mock()
mock_conn.append.return_value = ("OK", [])
result = imap_common.append_email(
mock_conn,
"TestFolder",
b"email content",
"01-Jan-2024 12:00:00 +0000",
flags="\\Seen \\Flagged \\Answered",
ensure_folder=False,
)
assert result is True
# Multiple flags without parentheses should be wrapped
mock_conn.append.assert_called_once_with(
'"TestFolder"',
"(\\Seen \\Flagged \\Answered)",
"01-Jan-2024 12:00:00 +0000",
b"email content",
)
class TestGetMessageIdsInFolder:
"""Tests for get_message_ids_in_folder function."""
def test_empty_folder(self):
"""Test returns empty dict for folder with no messages."""
mock_conn = Mock()
mock_conn.uid.return_value = ("OK", [b""])
result = imap_common.get_message_ids_in_folder(mock_conn)
assert result == {}
def test_search_fails(self):
"""Test returns empty dict when search fails."""
mock_conn = Mock()
mock_conn.uid.return_value = ("NO", [])
result = imap_common.get_message_ids_in_folder(mock_conn)
assert result == {}
def test_search_non_auth_exception_returns_empty(self):
"""Test returns empty dict when search raises non-auth exception."""
mock_conn = Mock()
mock_conn.uid.side_effect = Exception("Connection error")
result = imap_common.get_message_ids_in_folder(mock_conn)
assert result == {}
def test_search_auth_error_reraises(self):
"""Test re-raises auth errors so callers can handle reconnection."""
mock_conn = Mock()
mock_conn.uid.side_effect = Exception("User not authenticated")
with pytest.raises(Exception, match="not authenticated"):
imap_common.get_message_ids_in_folder(mock_conn)
def test_single_message(self):
"""Test fetching single message ID."""
mock_conn = Mock()
# First call: search returns one UID
# Second call: fetch returns the Message-ID header (with UID in response)
mock_conn.uid.side_effect = [
("OK", [b"1"]),
("OK", [(b"1 (UID 1 BODY[HEADER.FIELDS (MESSAGE-ID)] {50}", b"Message-ID: <test@example.com>\r\n"), b")"]),
]
result = imap_common.get_message_ids_in_folder(mock_conn)
assert result == {b"1": "<test@example.com>"}
assert set(result.values()) == {"<test@example.com>"}
def test_multiple_messages(self):
"""Test fetching multiple message IDs."""
mock_conn = Mock()
mock_conn.uid.side_effect = [
("OK", [b"1 2 3"]),
(
"OK",
[
(b"1 (UID 1 BODY[HEADER.FIELDS (MESSAGE-ID)] {50}", b"Message-ID: <msg1@example.com>\r\n"),
b")",
(b"2 (UID 2 BODY[HEADER.FIELDS (MESSAGE-ID)] {50}", b"Message-ID: <msg2@example.com>\r\n"),
b")",
(b"3 (UID 3 BODY[HEADER.FIELDS (MESSAGE-ID)] {50}", b"Message-ID: <msg3@example.com>\r\n"),
b")",
],
),
]
result = imap_common.get_message_ids_in_folder(mock_conn)
assert set(result.values()) == {"<msg1@example.com>", "<msg2@example.com>", "<msg3@example.com>"}
def test_fetch_fails_for_batch(self):
"""Test continues when fetch fails for a batch."""
mock_conn = Mock()
mock_conn.uid.side_effect = [
("OK", [b"1"]),
("NO", []), # Fetch fails
]
result = imap_common.get_message_ids_in_folder(mock_conn)
assert result == {}
def test_fetch_non_auth_exception_continues(self):
"""Test continues when fetch raises non-auth exception for a batch."""
mock_conn = Mock()
mock_conn.uid.side_effect = [
("OK", [b"1"]),
Exception("Fetch error"),
]
result = imap_common.get_message_ids_in_folder(mock_conn)
assert result == {}
def test_fetch_auth_error_reraises(self):
"""Test re-raises auth errors during fetch so callers can handle reconnection."""
mock_conn = Mock()
mock_conn.uid.side_effect = [
("OK", [b"1"]),
Exception("AccessTokenExpired"),
]
with pytest.raises(Exception, match="AccessTokenExpired"):
imap_common.get_message_ids_in_folder(mock_conn)
def test_skips_empty_message_id(self):
"""Test that empty message IDs are not added to dict."""
mock_conn = Mock()
mock_conn.uid.side_effect = [
("OK", [b"1 2"]),
(
"OK",
[
(b"1 (UID 1 BODY[HEADER.FIELDS (MESSAGE-ID)] {50}", b"Message-ID: <valid@example.com>\r\n"),
b")",
(b"2 (UID 2 BODY[HEADER.FIELDS (MESSAGE-ID)] {50}", b"Message-ID: \r\n"), # Empty
b")",
],
),
]
result = imap_common.get_message_ids_in_folder(mock_conn)
assert set(result.values()) == {"<valid@example.com>"}
class TestLoadFolderMsgIds:
"""Tests for load_folder_msg_ids() using the mock IMAP server."""
def test_returns_none_when_disabled(self):
"""Returns None if dict or lock is None (tracking disabled)."""
mock_conn = Mock()
import threading
lock = threading.Lock()
assert imap_common.load_folder_msg_ids(mock_conn, "INBOX", None, lock) is None
assert imap_common.load_folder_msg_ids(mock_conn, "INBOX", {}, None) is None
assert imap_common.load_folder_msg_ids(mock_conn, "INBOX", None, None) is None
def test_fetches_message_ids_from_server(self, single_mock_server):
"""Fetches Message-IDs from server and caches them."""
import imaplib
import threading
server_data = {
"TestFolder": [
b"Subject: A\r\nMessage-ID: <a@test.com>\r\n\r\nBody A",
b"Subject: B\r\nMessage-ID: <b@test.com>\r\n\r\nBody B",
]
}
_server, port = single_mock_server(server_data)
conn = imaplib.IMAP4("localhost", port)
conn.login("user", "pass")
lock = threading.Lock()
by_folder: dict[str, set[str]] = {}
result = imap_common.load_folder_msg_ids(conn, "TestFolder", by_folder, lock)
assert result == {"<a@test.com>", "<b@test.com>"}
assert "TestFolder" in by_folder
# Second call returns the same cached set object
result2 = imap_common.load_folder_msg_ids(conn, "TestFolder", by_folder, lock)
assert result2 is result
conn.logout()
def test_empty_folder(self, single_mock_server):
"""Returns empty set for a folder with no messages."""
import imaplib
import threading
_server, port = single_mock_server({"INBOX": []})
conn = imaplib.IMAP4("localhost", port)
conn.login("user", "pass")
lock = threading.Lock()
by_folder: dict[str, set[str]] = {}
result = imap_common.load_folder_msg_ids(conn, "INBOX", by_folder, lock)
assert result == set()
conn.logout()
def test_creates_folder_if_missing(self, single_mock_server):
"""Creates folder on first access if it doesn't exist."""
import imaplib
import threading
server, port = single_mock_server({"INBOX": []})
conn = imaplib.IMAP4("localhost", port)
conn.login("user", "pass")
lock = threading.Lock()
by_folder: dict[str, set[str]] = {}
result = imap_common.load_folder_msg_ids(conn, "NewFolder", by_folder, lock)
assert result == set()
assert "NewFolder" in server.folders
conn.logout()