Created
March 20, 2026 04:03
-
-
Save englishm/b1e32d61e7f88fe4d048067ff824eedf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # requires-python = ">=3.10" | |
| # /// | |
| """ | |
| Import a .eml file into your Gmail inbox via IMAP APPEND. | |
| Useful for replying to IETF mailing list messages from the web archive — | |
| the original Message-ID, References, and In-Reply-To headers are preserved, | |
| so Gmail will thread your reply correctly. | |
| Usage: | |
| chmod +x gmail-import-eml.py | |
| ./gmail-import-eml.py message.eml | |
| Or without making it executable: | |
| uv run gmail-import-eml.py message.eml | |
| Authentication: | |
| You need a Gmail "App Password" (NOT your regular password). | |
| Generate one here — you'll need 2FA enabled first: | |
| https://myaccount.google.com/apppasswords | |
| Select app type "Mail", give it a name like "eml-import", and copy | |
| the 16-character password it gives you. | |
| You can either: | |
| - Export env vars to skip the prompts: | |
| export GMAIL_USER="you@gmail.com" | |
| export GMAIL_APP_PASSWORD="xxxx xxxx xxxx xxxx" | |
| - Or just run the script and it will prompt you interactively. | |
| """ | |
| import argparse | |
| import email.utils | |
| import getpass | |
| import imaplib | |
| import os | |
| import sys | |
| import time | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Import a .eml file into your Gmail inbox via IMAP.", | |
| epilog="Get an app password at: https://myaccount.google.com/apppasswords", | |
| ) | |
| parser.add_argument("eml", help="Path to the .eml file to import") | |
| parser.add_argument( | |
| "--label", | |
| default="INBOX", | |
| help="Gmail label/folder to import into (default: INBOX)", | |
| ) | |
| parser.add_argument( | |
| "--unseen", | |
| action="store_true", | |
| help="Mark the message as unread so it's easy to find", | |
| ) | |
| args = parser.parse_args() | |
| # Read the .eml file | |
| try: | |
| with open(args.eml, "rb") as f: | |
| eml_data = f.read() | |
| except FileNotFoundError: | |
| print(f"Error: file not found: {args.eml}", file=sys.stderr) | |
| sys.exit(1) | |
| # Get credentials from env or prompt | |
| user = os.environ.get("GMAIL_USER") | |
| if not user: | |
| user = input("Gmail address: ") | |
| password = os.environ.get("GMAIL_APP_PASSWORD") | |
| if not password: | |
| print( | |
| "\nYou need a Gmail App Password (not your regular password)." | |
| "\nGenerate one at: https://myaccount.google.com/apppasswords" | |
| "\n" | |
| ) | |
| password = getpass.getpass("App password: ") | |
| # Parse the Date header so Gmail gets the original timestamp | |
| internal_date = None | |
| try: | |
| import email | |
| msg = email.message_from_bytes(eml_data) | |
| date_str = msg.get("Date") | |
| if date_str: | |
| parsed = email.utils.parsedate_to_datetime(date_str) | |
| internal_date = imaplib.Time2Internaldate(parsed) | |
| except Exception: | |
| pass # Fall back to None (current time) if parsing fails | |
| # Connect and append | |
| flags = "()" if args.unseen else "(\\Seen)" | |
| try: | |
| imap = imaplib.IMAP4_SSL("imap.gmail.com") | |
| imap.login(user, password) | |
| status, response = imap.append(args.label, flags, internal_date, eml_data) | |
| imap.logout() | |
| except imaplib.IMAP4.error as e: | |
| print(f"IMAP error: {e}", file=sys.stderr) | |
| print( | |
| "\nIf you got 'AUTHENTICATIONFAILED', double-check that you're" | |
| "\nusing an App Password, not your regular Google password." | |
| "\nGenerate one at: https://myaccount.google.com/apppasswords", | |
| file=sys.stderr, | |
| ) | |
| sys.exit(1) | |
| if status == "OK": | |
| print(f"Imported into {args.label}: {args.eml}") | |
| print("Open Gmail and find the message to reply with proper threading.") | |
| else: | |
| print(f"Failed: {response}", file=sys.stderr) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment