import imaplib import os import logging import sys from dotenv import load_dotenv from datetime import datetime, timezone, timedelta from email import message_from_bytes logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) load_dotenv() # Configuration source_host = os.getenv("SOURCE_HOST") if not source_host: raise ValueError("SOURCE_HOST not set") source_port = int(os.getenv("SOURCE_PORT", "993")) source_user = os.getenv("SOURCE_USER") if not source_user: raise ValueError("SOURCE_USER not set") source_pass = os.getenv("SOURCE_PASS") if not source_pass: raise ValueError("SOURCE_PASS not set") dest_host = os.getenv("DEST_HOST") if not dest_host: raise ValueError("DEST_HOST not set") dest_port = int(os.getenv("DEST_PORT", "993")) dest_user = os.getenv("DEST_USER") if not dest_user: raise ValueError("DEST_USER not set") dest_pass = os.getenv("DEST_PASS") if not dest_pass: raise ValueError("DEST_PASS not set") folder = os.getenv("FOLDER", "INBOX") processed_file = os.getenv("PROCESSED_FILE", "processed_uids.txt") processed_msg_file = "processed_message_ids.txt" last_run_file = "last_run.txt" dry_run = os.getenv("DRY_RUN", "false").lower() == "true" def main(): # Last run if os.path.exists(last_run_file): with open(last_run_file) as f: last_run_str = f.read().strip() last_run = datetime.fromisoformat(last_run_str) else: last_run = datetime.now(timezone.utc) - timedelta(hours=1) since_date = last_run.strftime("%d-%b-%Y") logging.info(f"Using SINCE {since_date}") # Load processed Message-IDs processed_msg = set() if os.path.exists(processed_msg_file): with open(processed_msg_file, "r") as f: 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) # 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}") uids = data[0].split() logging.info(f"Found {len(uids)} emails since {since_date}") # 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] 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 # 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}") else: logging.info(f"Dry-run: Would forward Message-ID {msg_id}") 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.") except Exception as e: logging.error(f"Error during email forwarding: {e}") sys.exit(1) if __name__ == "__main__": main()