From ed5258707746d21b6208059d50a8a3f546ba1125 Mon Sep 17 00:00:00 2001 From: Nathan Moinvaziri Date: Wed, 28 Jan 2026 15:23:32 -0800 Subject: [PATCH] Replace manual folder listing with list_selectable_folders utility Easily exclude \NoSelect folders and folders whose status is not OK. --- src/backup_imap_emails.py | 26 ++++++++------------------ src/compare_imap_folders.py | 7 +++---- src/count_imap_emails.py | 7 +++---- src/imap_common.py | 21 +++++++++++++++++++++ src/migrate_imap_emails.py | 23 ++++++++++------------- test/test_count_imap_emails.py | 2 +- 6 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/backup_imap_emails.py b/src/backup_imap_emails.py index 472bfb9..9350712 100644 --- a/src/backup_imap_emails.py +++ b/src/backup_imap_emails.py @@ -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.") diff --git a/src/compare_imap_folders.py b/src/compare_imap_folders.py index e7bc364..f0de0de 100644 --- a/src/compare_imap_folders.py +++ b/src/compare_imap_folders.py @@ -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) diff --git a/src/count_imap_emails.py b/src/count_imap_emails.py index 59253da..2953f8a 100644 --- a/src/count_imap_emails.py +++ b/src/count_imap_emails.py @@ -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: diff --git a/src/imap_common.py b/src/imap_common.py index c9a97c6..f31ec26 100644 --- a/src/imap_common.py +++ b/src/imap_common.py @@ -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. diff --git a/src/migrate_imap_emails.py b/src/migrate_imap_emails.py index 79ff545..30d5c27 100644 --- a/src/migrate_imap_emails.py +++ b/src/migrate_imap_emails.py @@ -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() diff --git a/test/test_count_imap_emails.py b/test/test_count_imap_emails.py index 6f0c26d..1cd38a6 100644 --- a/test/test_count_imap_emails.py +++ b/test/test_count_imap_emails.py @@ -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."""