Supplier invoices, order confirmations, monthly statements, scans sent by the multifunction printer: many documents reach the office as email attachments. Opening each message, downloading the file and moving it to the right folder is a task repeated dozens of times a week. With the IMAP protocol and Python's standard library you can hand it all over to a script that reads the mailbox, saves the attachments and keeps track of what it has already done.
IMAP in brief
IMAP is the protocol email clients use to read messages while leaving them on the server. A script can use it exactly as Outlook or Thunderbird do: it connects, chooses a folder, searches for messages by criteria and downloads them. You need the IMAP server address, normally encrypted on port 993, and the credentials. For accounts with two-step verification you generate an app password, as for sending via SMTP; some business services instead require token-based authentication, to be configured according to the provider's documentation.
Connecting and searching for messages
import imaplib
with imaplib.IMAP4_SSL("imap.provider.com") as imap:
imap.login(user, password)
imap.select("INBOX")
status, data = imap.uid("search", None, '(UNSEEN FROM "invoices@supplier.com")')
found_uids = data[0].split()
print(len(found_uids), "messages to process")
The search criterion follows IMAP syntax. Some useful examples:
| Criterion | Meaning |
|---|---|
UNSEEN |
Unread messages |
FROM "address" |
Specific sender |
SUBJECT "invoice" |
Word in the subject |
SINCE 01-Mar-2025 |
Received from that date onwards |
(UNSEEN SUBJECT "order") |
Several conditions together |
The date format uses the abbreviated English month, whatever the account's language. It is best to use UID identifiers, through imap.uid(...), rather than sequence numbers: sequence numbers change when messages are moved or deleted, while UIDs stay stable.
Extracting attachments
Each message is downloaded in raw form and parsed with the email module, which knows how to break down the parts of a complex message:
import email
from email import policy
from pathlib import Path
destination = Path("C:/Accounts/Incoming invoices")
for uid in found_uids:
status, parts = imap.uid("fetch", uid, "(RFC822)")
message = email.message_from_bytes(parts[0][1], policy=policy.default)
for attachment in message.iter_attachments():
name = attachment.get_filename()
if not name or not name.lower().endswith(".pdf"):
continue
path = destination / safe_name(name)
path.write_bytes(attachment.get_content())
With policy=policy.default the module returns modern objects, automatically decodes file names containing accented characters and offers iter_attachments(), which lists only the parts marked as attachments.
Safe file names
An attachment's name is chosen by whoever sends the message and must never be used as it is. It may contain characters that are invalid on Windows, be identical to that of a file already saved or, in malicious cases, include sequences such as ..\ to write outside the intended folder. A cleaning function solves all three problems:
import re
from datetime import date
def safe_name(name):
name = Path(name).name # strips any path
name = re.sub(r'[\\/:*?"<>|]', "_", name).strip(" .")
return f"{date.today():%Y-%m-%d}_{name}"
Prefixing the date helps sorting and reduces collisions. To eliminate them entirely, check before saving whether the file exists and, if so, add a numeric suffix.
Marking processed messages
The script must remember which messages it has already handled, otherwise on the next run it will save the same attachments again. There are three strategies:
- mark as read: fetching with
RFC822already does this automatically on many servers; it is simple, but if a person opens the message before the script does, the script will no longer see it; - move to a dedicated folder, for example "Processed": this is the clearest choice, because the mailbox shows at a glance what remains to be done. It is done with
imap.uid("copy", uid, "Processed")followed by marking the message as deleted; many servers also support theMOVEcommand in a single step; - record the UIDs in a local file: independent of the messages' state, but it needs careful upkeep.
In my work I prefer the second: it leaves a trace visible even to those who don't know the script, and lets you reprocess a message simply by moving it back into the inbox.
Attachment security
Automatically saving files received from the Internet calls for caution. Limit the script to expected senders and expected file types, such as PDFs, ignoring executables, compressed archives and documents with macros. Remember too that an email's sender can be forged: filtering by address reduces noise but does not guarantee authenticity. Saved files must never be opened automatically; they should be made available to people, who will treat them like any other attachment, with the company antivirus active on the destination folder.
In short
imapliblets you search for and download messages; use UIDs and an encrypted connection.- The
emailmodule withpolicy.defaultextracts attachments and decodes their names. - Always clean file names and prefix the date to avoid collisions.
- Move processed messages to a dedicated folder so they aren't handled twice.
- Filter senders and file types, and never open attachments automatically.