This Python script automates the download of email attachments from specific senders by connecting to an IMAP server, filtering messages, and parsing multipart MIME structures. It handles complex email formats that often break standard libraries, ensuring files are saved securely without exposing credentials or failing on nested content types.
Setting Up the Environment
Before writing the automation logic, you must ensure your Python environment has the necessary dependencies. The standard library includes
imaplib for connection and
email for parsing, but third-party tools like
beautifulsoup4 can help with HTML body extraction if needed. If you encounter missing library errors, refer to our guide on fixing platform-independent library issues in
Python Script Fails: Couldn't Find Platform-Independent Libraries. Ensure your virtual environment is active before installing packages via pip.
Connecting to the IMAP Server
The first step is establishing a secure connection to your email provider's IMAP server. Use
imaplib.IMAP4_SSL for encrypted connections, which is required by most modern providers like Gmail, Outlook, or Yahoo. You will need to generate an App Password if two-factor authentication is enabled, as using your main account password may be blocked for security reasons.
import imaplib
import email
from email.parser import BytesParser
import os
def connect_to_email(host, port, username, password):
try:
mail = imaplib.IMAP4_SSL(host, port)
mail.login(username, password)
mail.select("inbox")
return mail
except imaplib.IMAP4.error as e:
print(f"Connection failed: {e}")
return None
Always handle exceptions gracefully. If the login fails, the script should exit cleanly rather than crashing with a traceback that might expose sensitive details.
Filtering Emails by Sender
Once connected, you need to search for emails from a specific sender. The
search method allows you to filter by criteria such as
"FROM". This reduces the amount of data downloaded from the server, improving performance. Be aware that some providers may require URL-encoded characters in search queries, though most standard ASCII senders work directly.
def find_emails_from_sender(mail, sender_address):
status, messages = mail.search(None, f'(FROM "{sender_address}")')
if status != "OK":
return []
# Split the comma-separated list of message IDs
email_ids = messages[0].split()
return [eid.decode('utf-8') for eid in email_ids]
If you are dealing with large inboxes, consider adding date constraints like
(SINCE "01-Jan-2023") to narrow down the results further.
Parsing Multipart Attachments Correctly
This is where many scripts fail. Emails are not just plain text; they are multipart MIME messages. A single email can contain text, HTML, images, and multiple attachments nested within boundaries. To fix the variable scope error common in previous drafts, we must process each part individually and check its
Content-Disposition header.
def save_attachments(mail, email_id, save_folder):
status, msg_data = mail.fetch(email_id, "(RFC822)")
if status != "OK":
return
# Parse the raw bytes into an email object
msg = BytesParser().parsebytes(msg_data[0][1])
for part in msg.walk():
# Check if the part is an attachment
if part.get_content_maintype() == 'multipart':
continue
if part.get('Content-Disposition') is None:
continue
filename = part.get_filename()
if filename:
# Decode filename if it contains encoded words
from email.header import decode_header
decoded_header = decode_header(filename)[0]
if isinstance(decoded_header[0], bytes):
filename = decoded_header[0].decode(decoded_header[1] or 'utf-8', errors='ignore')
filepath = os.path.join(save_folder, filename)
# Avoid overwriting existing files
counter = 1
while os.path.exists(filepath):
name, ext = os.path.splitext(filename)
filepath = os.path.join(save_folder, f"{name}_{counter}{ext}")
counter += 1
with open(filepath, 'wb') as f:
f.write(part.get_payload(decode=True))
print(f"Saved: {filepath}")
Notice how we use
msg.walk() to iterate through all parts. This ensures that even nested multipart structures are fully traversed. We also handle filename decoding properly, which is crucial for emails containing non-ASCII characters.
Handling Security and Cleanup
After processing all emails, always close the IMAP connection to free up resources. Use
mail.logout() within a finally block to ensure this happens even if an error occurs. Never store passwords in plain text in your script; use environment variables or a configuration file with restricted permissions instead.
Scheduling the Automation
To make this script run automatically, you can use cron jobs on Linux/Mac or Task Scheduler on Windows. Schedule it to run every hour or day, depending on your needs. Ensure the script logs its actions to a file so you can monitor success or failures. For more information on managing Python environments consistently across different systems, see
How to Make Anaconda pip as the Default pip.
Frequently Asked Questions
Why does my script fail on emails with inline images?
Inline images are embedded within the HTML body and have a Content-ID header rather than a Content-Disposition attachment header. Your parser must distinguish between
attachment and
inline dispositions to avoid saving unnecessary image files.
How do I handle emails with no subject line?
Missing subjects are common in automated notifications. The script relies on sender filtering, not subject lines, so missing subjects do not affect the download process. Always check the
From field strictly.
Can I download attachments from multiple senders at once?
Yes, modify the search query to include multiple FROM conditions using OR logic, such as
'(OR FROM "[email protected]" FROM "[email protected]")'. This reduces the number of API calls needed.
What if the attachment filename is corrupted or encoded?
Use the
email.header.decode_header function to properly decode MIME-encoded words. This resolves issues with special characters and non-Latin alphabets in filenames.
Is it safe to hardcode my email password?
No, never hardcode passwords. Use environment variables or a secrets manager. Hardcoded credentials can be exposed if your code is pushed to public repositories.