import msal import imaplib import os import csv import re import time import socket # 🚨 Force Python to drop the connection if Microsoft stops responding (tarpitting) socket.setdefaulttimeout(30) # ========================================== # 1. CONFIGURATION # ========================================== CLIENT_ID = "05332268-8149-449f-a1f8-1efadd17166f" EMAIL_ADDRESS = "guillem@agile611.com" AUTHORITY = "https://login.microsoftonline.com/884a3c53-8a5a-4d79-b0e0-a62ab5a794a1" # MSAL automatically requests offline_access, so we only list the IMAP scope here SCOPES = ["https://outlook.office.com/IMAP.AccessAsUser.All"] # ========================================== # 2. HELPER FUNCTION: GENERATE CSV # ========================================== def generate_user_csv(email_address, first_name="Guillem", last_name="Hernandez Sola"): username = email_address.split("@")[0] headers = [ "originUsername", "targetUsername", "password", "pop3enabled", "pop3password", "aliases", "forwards", "filters", "forename", "surname", "mailboxStatus" ] row_data = { "originUsername": username, "targetUsername": username, "password": "TempWebmailPassword123!", "pop3enabled": "true", "pop3password": "", "aliases": "", "forwards": "", "filters": "", "forename": first_name, "surname": last_name, "mailboxStatus": "premium" } csv_filename = f"{username}_import.csv" with open(csv_filename, mode="w", newline="", encoding="utf-8") as file: writer = csv.DictWriter(file, fieldnames=headers, delimiter=";") writer.writeheader() writer.writerow(row_data) print(f"šŸ“ Migration CSV generated: {csv_filename}") # ========================================== # 3. TOKEN & CONNECTION MANAGERS # ========================================== def get_valid_token(app): """Checks the cache for a valid token, refreshes silently if needed, or prompts user.""" accounts = app.get_accounts() if accounts: result = app.acquire_token_silent(SCOPES, account=accounts[0]) if result and "access_token" in result: return result["access_token"] flow = app.initiate_device_flow(scopes=SCOPES) if "user_code" not in flow: raise ValueError("Failed to create device flow. Check your Client ID and Azure settings.") print("\n🚨 ACTION REQUIRED 🚨") print(flow["message"]) print("ā³ Waiting for browser authentication...") result = app.acquire_token_by_device_flow(flow) if "access_token" not in result: raise Exception(f"Failed to get token: {result.get('error_description')}") return result["access_token"] def connect_to_imap(email, token): """Creates a fresh, authenticated connection to the IMAP server.""" auth_string = f"user={email}\x01auth=Bearer {token}\x01\x01" mail = imaplib.IMAP4_SSL("outlook.office365.com", 993) mail.authenticate("XOAUTH2", lambda x: auth_string.encode("utf-8")) return mail # ========================================== # 4. MAIN EXECUTION # ========================================== def main(): print("šŸ”„ Initializing Microsoft Authentication...") app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY) try: access_token = get_valid_token(app) print("\nāœ… Access Token Acquired Successfully!") print("\nšŸ”Œ Connecting to Outlook IMAP server...") mail = connect_to_imap(EMAIL_ADDRESS, access_token) print("āœ… Successfully logged into IMAP via OAuth2!") except Exception as e: print(f"\nāŒ Authentication failed: {e}") return try: username = EMAIL_ADDRESS.split("@")[0] base_download_dir = f"downloaded_emails_{username}" os.makedirs(base_download_dir, exist_ok=True) status, folders = mail.list() if status == "OK": print(f"šŸ“‚ Found {len(folders)} folders. Starting full account backup...\n") for folder_data in folders: folder_string = folder_data.decode('utf-8') if "\\Noselect" in folder_string: continue match = re.search(r'\"([^\"]+)\"$', folder_string) folder_name = match.group(1) if match else folder_string.split()[-1].strip('"') print(f"šŸ“ Scanning folder: {folder_name}") status, _ = mail.select(f'"{folder_name}"', readonly=True) if status != "OK": print(f" āš ļø Could not open {folder_name}. Skipping.") continue status, data = mail.search(None, "ALL") email_ids = data[0].split() if not email_ids: print(" ↳ Folder is empty.") continue print(f" ↳ Found {len(email_ids)} emails. Downloading...") safe_folder_name = "".join([c for c in folder_name if c.isalnum() or c in (' ', '-', '_')]).strip() folder_dir = os.path.join(base_download_dir, safe_folder_name) os.makedirs(folder_dir, exist_ok=True) # Download loop with Reconnect & Timeout Logic for e_id in email_ids: file_path = os.path.join(folder_dir, f"email_{e_id.decode('utf-8')}.eml") # šŸš€ SKIP EXISTING: Don't re-download emails we already have! if os.path.exists(file_path): continue success = False while not success: try: status, msg_data = mail.fetch(e_id, "(RFC822)") for response_part in msg_data: if isinstance(response_part, tuple): raw_email = response_part[1] with open(file_path, "wb") as f: f.write(raw_email) success = True # Catch token expiration, forced disconnects, AND silent timeouts except (imaplib.IMAP4.abort, imaplib.IMAP4.error, ConnectionResetError, socket.timeout, TimeoutError) as e: print(f"\n āš ļø Connection lost or timed out. Refreshing token and reconnecting...") try: access_token = get_valid_token(app) mail = connect_to_imap(EMAIL_ADDRESS, access_token) mail.select(f'"{folder_name}"', readonly=True) print(" āœ… Reconnected! Resuming download...") except Exception as reconnect_error: print(f" āŒ Reconnection failed: {reconnect_error}. Retrying in 5 seconds...") time.sleep(5) print(f"\nšŸŽ‰ All folders successfully downloaded to '{base_download_dir}'!") mail.logout() print("šŸ‘‹ Logged out successfully.\n") print("āš™ļø Generating configuration files...") generate_user_csv(EMAIL_ADDRESS) print("šŸŽ‰ Migration prep complete!") except Exception as e: print(f"\nāŒ A critical error occurred: {e}") if __name__ == "__main__": main()