diff --git a/.env.example b/.env.example index 0d59d47..0ed032e 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ DEST_HOST=yourserver.mxrouting.net DEST_PORT=993 DEST_USER=your@mxroute.com DEST_PASS=password -FOLDER=INBOX +FOLDERS=INBOX DRY_RUN=false UPTIME_SUCCESS_URL=https://your-uptime-instance.com/api/push/your-token?status=up&msg=OK&ping= UPTIME_FAIL_URL=https://your-uptime-instance.com/api/push/your-token?status=down&msg=Failed \ No newline at end of file diff --git a/README.md b/README.md index f1825ca..7310edc 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This will set up the virtual environment, systemd services, and provide post-ins - `DEST_PORT`: Port (default 993) - `DEST_USER`: Destination email/username - `DEST_PASS`: Destination password -- `FOLDER`: Mailbox folder (default INBOX) +- `FOLDERS`: Folders to sync (default INBOX; use "all" for all folders or comma-separated list like "INBOX,Sent") - `DRY_RUN`: Set to `true` for testing without forwarding (default false) - `UPTIME_SUCCESS_URL`: URL for success ping - `UPTIME_FAIL_URL`: URL for failure ping diff --git a/main.py b/main.py index 4585634..b1c3a23 100644 --- a/main.py +++ b/main.py @@ -33,7 +33,7 @@ if not dest_user: dest_pass = os.getenv("DEST_PASS") if not dest_pass: raise ValueError("DEST_PASS not set") -folder = os.getenv("FOLDER", "INBOX") +folders_config = os.getenv("FOLDERS", "INBOX") processed_msg_file = "processed_message_ids.txt" last_run_file = "last_run.txt" dry_run = os.getenv("DRY_RUN", "false").lower() == "true" @@ -57,71 +57,129 @@ def main(): processed_msg = set(line.strip() for line in f if line.strip()) try: - # Connect to source IMAP - source_imap = imaplib.IMAP4_SSL(source_host, source_port) - source_imap.login(source_user, source_pass) - source_imap.select(folder) + # Connect to source IMAP for listing + source_imap_list = imaplib.IMAP4_SSL(source_host, source_port) + source_imap_list.login(source_user, source_pass) - # Search for emails since last run - typ, data = source_imap.search(None, f'SINCE "{since_date}"') - if typ != "OK": - raise Exception(f"Search failed: {typ}") + # Determine folders to sync + if folders_config == "all": + # List all folders + typ, folder_list = source_imap_list.list() + if typ != "OK": + raise Exception("Failed to list folders") + folders = [] + for f in folder_list: + if f: + # Parse folder name, e.g., '(\\HasNoChildren) "/" "INBOX"' + parts = f.decode().split(' "/" ') + if len(parts) > 1: + folder_name = parts[1].strip('"') + # Skip system folders if desired (optional) + if folder_name not in ["Trash", "Junk", "Spam"]: + folders.append(folder_name) + else: + folders = [f.strip() for f in folders_config.split(",")] - uids = data[0].split() - logging.info(f"Found {len(uids)} emails since {since_date}") + source_imap_list.logout() + + logging.info(f"Syncing folders: {folders}") # Connect to destination IMAP dest_imap = imaplib.IMAP4_SSL(dest_host, dest_port) dest_imap.login(dest_user, dest_pass) - dest_imap.select(folder) - forwarded = 0 - for uid in uids: - uid_str = uid.decode() - # Fetch raw message - typ, msg_data = source_imap.fetch(uid, "(RFC822)") - if typ != "OK": - logging.error(f"Failed to fetch UID {uid_str}") - continue - raw_msg = msg_data[0][1] + total_forwarded = 0 - msg = message_from_bytes(raw_msg) - msg_id = msg["Message-ID"] - if not msg_id: - logging.warning(f"No Message-ID for UID {uid_str}, skipping") - continue - if msg_id in processed_msg: - logging.info(f"Skipping already processed Message-ID {msg_id}") - continue + for folder in folders: + try: + # Connect to source IMAP per folder + source_imap = imaplib.IMAP4_SSL(source_host, source_port) + source_imap.login(source_user, source_pass) + source_imap.select(folder) - # Check dest for duplicate - typ, dest_data = dest_imap.search(None, f'HEADER Message-ID "{msg_id}"') - if typ == "OK" and dest_data[0]: - logging.info(f"Skipping duplicate Message-ID {msg_id} in dest") - continue + # Search for emails since last run + typ, data = source_imap.search(None, f'SINCE "{since_date}"') + if typ != "OK": + logging.error(f"Search failed for {folder}: {typ}") + source_imap.logout() + continue - if not dry_run: - # Append to destination - dest_imap.append(folder, "", None, raw_msg) + uids = data[0].split() + logging.info(f"Found {len(uids)} emails in {folder} since {since_date}") - # Mark as processed - with open(processed_msg_file, "a") as f: - f.write(msg_id + "\n") - processed_msg.add(msg_id) - forwarded += 1 - logging.info(f"Forwarded email Message-ID {msg_id}") - else: - logging.info(f"Dry-run: Would forward Message-ID {msg_id}") + # Ensure dest folder exists + try: + dest_imap.select(folder) + except: + logging.info(f"Creating folder {folder} in dest") + dest_imap.create(folder) + dest_imap.select(folder) + + forwarded = 0 + for uid in uids: + uid_str = uid.decode() + # Fetch raw message + typ, msg_data = source_imap.fetch(uid, "(RFC822)") + if typ != "OK": + logging.error(f"Failed to fetch UID {uid_str} in {folder}") + continue + raw_msg = msg_data[0][1] + + msg = message_from_bytes(raw_msg) + msg_id = msg["Message-ID"] + if not msg_id: + logging.warning( + f"No Message-ID for UID {uid_str} in {folder}, skipping" + ) + continue + if msg_id in processed_msg: + logging.info(f"Skipping already processed Message-ID {msg_id}") + continue + + # Check dest for duplicate + typ, dest_data = dest_imap.search( + None, f'HEADER Message-ID "{msg_id}"' + ) + if typ == "OK" and dest_data[0]: + logging.info(f"Skipping duplicate Message-ID {msg_id} in dest") + continue + + if not dry_run: + # Append to destination + dest_imap.append(folder, "", None, raw_msg) + + # Mark as processed + with open(processed_msg_file, "a") as f: + f.write(msg_id + "\n") + processed_msg.add(msg_id) + forwarded += 1 + logging.info( + f"Forwarded email Message-ID {msg_id} from {folder}" + ) + else: + logging.info( + f"Dry-run: Would forward Message-ID {msg_id} from {folder}" + ) + + source_imap.logout() + total_forwarded += forwarded + logging.info( + f"Completed syncing {folder}: forwarded {forwarded} emails" + ) + + except Exception as e: + logging.error(f"Error syncing folder {folder}: {e}") dest_imap.logout() - source_imap.logout() if not dry_run: # Update last run with open(last_run_file, "w") as f: f.write(datetime.now(timezone.utc).isoformat()) - logging.info(f"Email forwarding completed. Forwarded {forwarded} new emails.") + logging.info( + f"Email forwarding completed. Total forwarded {total_forwarded} new emails." + ) except Exception as e: logging.error(f"Error during email forwarding: {e}")