Replace manual folder listing with list_selectable_folders utility

Easily exclude \NoSelect folders and folders whose status is not OK.
This commit is contained in:
Nathan Moinvaziri 2026-01-28 15:23:32 -08:00
parent 927fff17c7
commit ed52587077
6 changed files with 46 additions and 40 deletions

View File

@ -306,14 +306,10 @@ def build_labels_manifest(imap_conn, local_path):
safe_print("--- Building Gmail Labels Manifest ---")
# Get all folders
try:
typ, folders = imap_conn.list()
if typ != "OK":
safe_print("Error: Could not list folders for label mapping.")
return manifest
except Exception as e:
safe_print(f"Error listing folders: {e}")
# Get all selectable folders and filter to label folders
all_folders = imap_common.list_selectable_folders(imap_conn)
if not all_folders:
safe_print("Error: Could not list folders for label mapping.")
return manifest
# First, scan [Gmail]/All Mail to get the authoritative flags for all emails
@ -349,11 +345,7 @@ def build_labels_manifest(imap_conn, local_path):
pass
# Parse folder names and filter to label folders
label_folders = []
for f_info in folders:
name = imap_common.normalize_folder_name(f_info)
if is_gmail_label_folder(name):
label_folders.append(name)
label_folders = [f for f in all_folders if is_gmail_label_folder(f)]
total_folders = len(label_folders)
safe_print(f"Found {total_folders} label folders to scan.\n")
@ -806,11 +798,9 @@ def main():
elif args.folder:
backup_folder(src, args.folder, local_path, src_conf, args.dest_delete)
else:
typ, folders = src.list()
if typ == "OK":
for f_info in folders:
name = imap_common.normalize_folder_name(f_info)
backup_folder(src, name, local_path, src_conf, args.dest_delete)
folders = imap_common.list_selectable_folders(src)
for name in folders:
backup_folder(src, name, local_path, src_conf, args.dest_delete)
src.logout()
print("\nBackup completed successfully.")

View File

@ -114,8 +114,8 @@ def main():
# List Source Folders
print("Listing folders in Source...")
typ, folders = src.list()
if typ != "OK":
folders = imap_common.list_selectable_folders(src)
if not folders:
print("Failed to list source folders.")
return
@ -129,8 +129,7 @@ def main():
total_dest = 0
# Iterate through Source folders
for folder_info in folders:
folder_name = imap_common.normalize_folder_name(folder_info)
for folder_name in folders:
# Get Counts
src_count = get_email_count(src, folder_name)

View File

@ -36,9 +36,9 @@ def count_emails(imap_server, username, password):
# List all mailboxes
print("Listing mailboxes...")
status, folders = mail.list()
folders = imap_common.list_selectable_folders(mail)
if status != "OK":
if not folders:
print("Failed to list mailboxes.")
return
@ -46,8 +46,7 @@ def count_emails(imap_server, username, password):
print(f"{'Folder Name':<40} {'Count':>10}")
print("-" * 52)
for folder_info in folders:
folder_name = imap_common.normalize_folder_name(folder_info)
for folder_name in folders:
display_name = folder_name
try:

View File

@ -64,6 +64,27 @@ def normalize_folder_name(folder_info_str):
return folder_info_str.split()[-1].strip('"')
def list_selectable_folders(imap_conn):
"""
Lists all selectable folders (excluding \\Noselect) on the IMAP connection.
Returns a list of normalized folder name strings, or an empty list on failure.
"""
try:
status, folders = imap_conn.list()
if status != "OK" or not folders:
return []
except Exception:
return []
result = []
for f in folders:
f_str = f.decode("utf-8", errors="ignore") if isinstance(f, bytes) else str(f)
if "\\Noselect" in f_str:
continue
result.append(normalize_folder_name(f))
return result
def decode_mime_header(header_value):
"""
Decodes MIME encoded headers (Subject, etc.) to a unicode (str) string.

View File

@ -564,20 +564,17 @@ def main():
)
else:
# Migration for all folders
typ, folders = src_main.list()
if typ == "OK":
for folder_info in folders:
name = imap_common.normalize_folder_name(folder_info)
folders = imap_common.list_selectable_folders(src_main)
for name in folders:
# Auto-skip trash folder if we are utilizing it as a dump target
# This prevents re-migrating the emails we just moved to trash
if DELETE_SOURCE and trash_folder and name == trash_folder:
safe_print(f"Skipping migration of Trash folder '{name}' (preventing circular migration).")
continue
# Auto-skip trash folder if we are utilizing it as a dump target
# This prevents re-migrating the emails we just moved to trash
if DELETE_SOURCE and trash_folder and name == trash_folder:
safe_print(f"Skipping migration of Trash folder '{name}' (preventing circular migration).")
continue
migrate_folder(
src_main, dest_main, name, DELETE_SOURCE, src_conf, dest_conf, trash_folder, DEST_DELETE
)
migrate_folder(
src_main, dest_main, name, DELETE_SOURCE, src_conf, dest_conf, trash_folder, DEST_DELETE
)
src_main.logout()
dest_main.logout()

View File

@ -145,7 +145,7 @@ class TestEmailCountingErrors:
count_imap_emails.count_emails("host", "user", "pass")
captured = capsys.readouterr()
assert "IMAP Error: Crash listing" in captured.out
assert "Failed to list mailboxes" in captured.out
def test_imap_exception_during_select(self, monkeypatch, capsys):
"""Test handling of IMAP4 exception during folder selection."""