Update imap_tasks.py
Refactor IMAP Tasks: Filter Allowed Attachment Types & Improve Code Quality - Filter attachments to process only office files, PDFs, plain text, CSV, and RTF, explicitly skipping common image MIME types. - Use an explicit elif for clarity in attachment processing logic. - Remove dead code by not reassigning the result of fetch_attachments_and_enqueue. - Reformat log messages, docstrings, and break long lines to comply with PEP 8. - Add clarifying comments for excluded image MIME types.
This commit is contained in:
committed by
GitHub
parent
473ae3a20a
commit
4c95e6ec6c
+45
-33
@@ -141,7 +141,8 @@ def check_and_pull_mailbox(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_after_process):
|
def pull_inbox(mailbox_key, host, port, username, password, use_ssl,
|
||||||
|
delete_after_process):
|
||||||
"""
|
"""
|
||||||
Connects to the IMAP inbox, fetches new unread emails from the last 3 days,
|
Connects to the IMAP inbox, fetches new unread emails from the last 3 days,
|
||||||
and processes attachments while preserving the original unread status.
|
and processes attachments while preserving the original unread status.
|
||||||
@@ -152,7 +153,8 @@ def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_afte
|
|||||||
|
|
||||||
For non-Gmail mailboxes, it falls back to selecting the INBOX with a SINCE/UNSEEN filter.
|
For non-Gmail mailboxes, it falls back to selecting the INBOX with a SINCE/UNSEEN filter.
|
||||||
"""
|
"""
|
||||||
logger.info(f"Connecting to {mailbox_key} at {host}:{port} (SSL={use_ssl})")
|
logger.info("Connecting to %s at %s:%s (SSL=%s)",
|
||||||
|
mailbox_key, host, port, use_ssl)
|
||||||
processed_emails = load_processed_emails()
|
processed_emails = load_processed_emails()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -164,7 +166,7 @@ def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_afte
|
|||||||
# For Gmail, try to select the localized All Mail folder.
|
# For Gmail, try to select the localized All Mail folder.
|
||||||
all_mail_folder = find_all_mail_folder(mail)
|
all_mail_folder = find_all_mail_folder(mail)
|
||||||
if all_mail_folder:
|
if all_mail_folder:
|
||||||
logger.info(f"Using Gmail All Mail folder: {all_mail_folder}")
|
logger.info("Using Gmail All Mail folder: %s", all_mail_folder)
|
||||||
mail.select(f'"{all_mail_folder}"')
|
mail.select(f'"{all_mail_folder}"')
|
||||||
else:
|
else:
|
||||||
logger.warning("Gmail All Mail folder not found, falling back to INBOX.")
|
logger.warning("Gmail All Mail folder not found, falling back to INBOX.")
|
||||||
@@ -175,22 +177,25 @@ def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_afte
|
|||||||
else:
|
else:
|
||||||
# For non-Gmail, select INBOX and use SINCE/UNSEEN query.
|
# For non-Gmail, select INBOX and use SINCE/UNSEEN query.
|
||||||
mail.select("INBOX")
|
mail.select("INBOX")
|
||||||
since_date = (datetime.now(timezone.utc) - timedelta(days=3)).strftime("%d-%b-%Y")
|
since_date = (datetime.now(timezone.utc) - timedelta(days=3)
|
||||||
|
).strftime("%d-%b-%Y")
|
||||||
status, search_data = mail.search(None, f'(SINCE {since_date} UNSEEN)')
|
status, search_data = mail.search(None, f'(SINCE {since_date} UNSEEN)')
|
||||||
|
|
||||||
if status != "OK":
|
if status != "OK":
|
||||||
logger.warning(f"Search failed on mailbox {mailbox_key}. Status={status}")
|
logger.warning("Search failed on mailbox %s. Status=%s",
|
||||||
|
mailbox_key, status)
|
||||||
mail.close()
|
mail.close()
|
||||||
mail.logout()
|
mail.logout()
|
||||||
return
|
return
|
||||||
|
|
||||||
msg_numbers = search_data[0].split()
|
msg_numbers = search_data[0].split()
|
||||||
logger.info(f"Found {len(msg_numbers)} unread emails in {mailbox_key}.")
|
logger.info("Found %d unread emails in %s.", len(msg_numbers), mailbox_key)
|
||||||
|
|
||||||
for num in msg_numbers:
|
for num in msg_numbers:
|
||||||
status, msg_data = mail.fetch(num, "(RFC822)")
|
status, msg_data = mail.fetch(num, "(RFC822)")
|
||||||
if status != "OK":
|
if status != "OK":
|
||||||
logger.warning(f"Failed to fetch message {num} in {mailbox_key}. Status={status}")
|
logger.warning("Failed to fetch message %s in %s. Status=%s",
|
||||||
|
num, mailbox_key, status)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw_email = msg_data[0][1]
|
raw_email = msg_data[0][1]
|
||||||
@@ -198,21 +203,23 @@ def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_afte
|
|||||||
msg_id = email_message.get("Message-ID")
|
msg_id = email_message.get("Message-ID")
|
||||||
|
|
||||||
if not msg_id:
|
if not msg_id:
|
||||||
logger.warning(f"Skipping email without Message-ID in {mailbox_key}")
|
logger.warning("Skipping email without Message-ID in %s", mailbox_key)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if msg_id in processed_emails:
|
if msg_id in processed_emails:
|
||||||
logger.info(f"Skipping already processed email {msg_id} in {mailbox_key}")
|
logger.info("Skipping already processed email %s in %s", msg_id, mailbox_key)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# For Gmail, check if the email already has the "Ingested" label.
|
# For Gmail, check if the email already has the "Ingested" label.
|
||||||
if is_gmail_host:
|
if is_gmail_host:
|
||||||
if email_already_has_label(mail, num, "Ingested"):
|
if email_already_has_label(mail, num, "Ingested"):
|
||||||
logger.info(f"Skipping email {msg_id} in {mailbox_key}, already labeled 'Ingested'.")
|
logger.info("Skipping email %s in %s, already labeled 'Ingested'.",
|
||||||
|
msg_id, mailbox_key)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Process attachments (and convert non-PDF files)
|
# Process attachments (and convert non-PDF files).
|
||||||
has_attachment = fetch_attachments_and_enqueue(email_message)
|
# We call the function without assigning its return value since it is not used.
|
||||||
|
fetch_attachments_and_enqueue(email_message)
|
||||||
|
|
||||||
if is_gmail_host:
|
if is_gmail_host:
|
||||||
mark_as_processed_with_star(mail, num)
|
mark_as_processed_with_star(mail, num)
|
||||||
@@ -222,7 +229,7 @@ def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_afte
|
|||||||
save_processed_emails(processed_emails)
|
save_processed_emails(processed_emails)
|
||||||
|
|
||||||
if delete_after_process:
|
if delete_after_process:
|
||||||
logger.info(f"Deleting message {num.decode()} from {mailbox_key}")
|
logger.info("Deleting message %s from %s", num.decode(), mailbox_key)
|
||||||
mail.store(num, "+FLAGS", "\\Deleted")
|
mail.store(num, "+FLAGS", "\\Deleted")
|
||||||
else:
|
else:
|
||||||
mail.store(num, "-FLAGS", "\\Seen")
|
mail.store(num, "-FLAGS", "\\Seen")
|
||||||
@@ -232,10 +239,10 @@ def pull_inbox(mailbox_key, host, port, username, password, use_ssl, delete_afte
|
|||||||
|
|
||||||
mail.close()
|
mail.close()
|
||||||
mail.logout()
|
mail.logout()
|
||||||
logger.info(f"Finished processing mailbox {mailbox_key}")
|
logger.info("Finished processing mailbox %s", mailbox_key)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error pulling mailbox {mailbox_key}: {e}")
|
logger.exception("Error pulling mailbox %s: %s", mailbox_key, e)
|
||||||
|
|
||||||
|
|
||||||
def fetch_attachments_and_enqueue(email_message):
|
def fetch_attachments_and_enqueue(email_message):
|
||||||
@@ -245,20 +252,26 @@ def fetch_attachments_and_enqueue(email_message):
|
|||||||
Allowed file types include:
|
Allowed file types include:
|
||||||
- PDF: application/pdf
|
- PDF: application/pdf
|
||||||
- Microsoft Office files:
|
- Microsoft Office files:
|
||||||
- Word: application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
- Word: application/msword,
|
||||||
- Excel: application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
- PowerPoint: application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation
|
- Excel: application/vnd.ms-excel,
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
- PowerPoint: application/vnd.ms-powerpoint,
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
- Other meaningful attachments:
|
- Other meaningful attachments:
|
||||||
- Plain text: text/plain
|
- Plain text: text/plain
|
||||||
- CSV: text/csv
|
- CSV: text/csv
|
||||||
- Rich Text Format: application/rtf, text/rtf
|
- Rich Text Format: application/rtf, text/rtf
|
||||||
|
|
||||||
- If the attachment is a PDF, it is enqueued for upload.
|
Attachments not in this list are skipped. Common image MIME types such as
|
||||||
- For any other allowed file, a conversion task is enqueued to convert it to PDF.
|
image/jpeg, image/png, image/gif, image/bmp, image/tiff, and image/webp are
|
||||||
|
intentionally excluded.
|
||||||
|
|
||||||
|
If the attachment is a PDF, it is enqueued for upload; any other allowed file
|
||||||
|
is enqueued for conversion to PDF.
|
||||||
|
|
||||||
Returns True if at least one allowed attachment was processed.
|
Returns True if at least one allowed attachment was processed.
|
||||||
"""
|
"""
|
||||||
# Define allowed MIME types for office files and other meaningful attachments.
|
|
||||||
ALLOWED_MIME_TYPES = {
|
ALLOWED_MIME_TYPES = {
|
||||||
"application/pdf",
|
"application/pdf",
|
||||||
"application/msword",
|
"application/msword",
|
||||||
@@ -282,10 +295,10 @@ def fetch_attachments_and_enqueue(email_message):
|
|||||||
if not filename:
|
if not filename:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check the MIME type of the attachment.
|
|
||||||
mime_type = part.get_content_type()
|
mime_type = part.get_content_type()
|
||||||
if mime_type not in ALLOWED_MIME_TYPES:
|
if mime_type not in ALLOWED_MIME_TYPES:
|
||||||
logger.info(f"Skipping attachment {filename} with MIME type {mime_type}")
|
logger.info("Skipping attachment %s with MIME type %s",
|
||||||
|
filename, mime_type)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(settings.workdir, filename)
|
file_path = os.path.join(settings.workdir, filename)
|
||||||
@@ -294,17 +307,16 @@ def fetch_attachments_and_enqueue(email_message):
|
|||||||
|
|
||||||
if mime_type == "application/pdf":
|
if mime_type == "application/pdf":
|
||||||
upload_to_s3.delay(file_path)
|
upload_to_s3.delay(file_path)
|
||||||
logger.info(f"Enqueued PDF for upload: {filename}")
|
logger.info("Enqueued PDF for upload: %s", filename)
|
||||||
else:
|
elif mime_type in ALLOWED_MIME_TYPES:
|
||||||
# Enqueue conversion to PDF using the Gotenberg service.
|
# Enqueue conversion to PDF using the Gotenberg service.
|
||||||
convert_to_pdf.delay(file_path)
|
convert_to_pdf.delay(file_path)
|
||||||
logger.info(f"Enqueued file for conversion to PDF: {filename}")
|
logger.info("Enqueued file for conversion to PDF: %s", filename)
|
||||||
|
|
||||||
has_attachment = True
|
has_attachment = True
|
||||||
return has_attachment
|
return has_attachment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def email_already_has_label(mail, msg_id, label="Ingested"):
|
def email_already_has_label(mail, msg_id, label="Ingested"):
|
||||||
"""
|
"""
|
||||||
Checks if the given message (msg_id) has the specified Gmail label.
|
Checks if the given message (msg_id) has the specified Gmail label.
|
||||||
@@ -317,7 +329,7 @@ def email_already_has_label(mail, msg_id, label="Ingested"):
|
|||||||
if label in raw_labels:
|
if label in raw_labels:
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to fetch labels for msg_id={msg_id}: {e}")
|
logger.error("Failed to fetch labels for msg_id=%s: %s", msg_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -325,18 +337,18 @@ def mark_as_processed_with_star(mail, msg_id):
|
|||||||
"""Stars the email in Gmail."""
|
"""Stars the email in Gmail."""
|
||||||
try:
|
try:
|
||||||
mail.store(msg_id, "+FLAGS", "\\Flagged")
|
mail.store(msg_id, "+FLAGS", "\\Flagged")
|
||||||
logger.info(f"Email {msg_id} starred in Gmail.")
|
logger.info("Email %s starred in Gmail.", msg_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to star email {msg_id}: {e}")
|
logger.error("Failed to star email %s: %s", msg_id, e)
|
||||||
|
|
||||||
|
|
||||||
def mark_as_processed_with_label(mail, msg_id, label="Ingested"):
|
def mark_as_processed_with_label(mail, msg_id, label="Ingested"):
|
||||||
"""Adds a custom label to the email in Gmail."""
|
"""Adds a custom label to the email in Gmail."""
|
||||||
try:
|
try:
|
||||||
mail.store(msg_id, "+X-GM-LABELS", label)
|
mail.store(msg_id, "+X-GM-LABELS", label)
|
||||||
logger.info(f"Email {msg_id} labeled '{label}' in Gmail.")
|
logger.info("Email %s labeled '%s' in Gmail.", msg_id, label)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to label email {msg_id} with {label}: {e}")
|
logger.error("Failed to label email %s with %s: %s", msg_id, label, e)
|
||||||
|
|
||||||
|
|
||||||
def find_all_mail_folder(mail):
|
def find_all_mail_folder(mail):
|
||||||
@@ -393,7 +405,7 @@ def find_all_mail_xlist(mail):
|
|||||||
match = re.search(r'"([^"]+)"$', line_str)
|
match = re.search(r'"([^"]+)"$', line_str)
|
||||||
if match:
|
if match:
|
||||||
candidate = match.group(1)
|
candidate = match.group(1)
|
||||||
logger.info(f"Found All Mail folder via XLIST: {candidate}")
|
logger.info("Found All Mail folder via XLIST: %s", candidate)
|
||||||
all_mail_folder = candidate
|
all_mail_folder = candidate
|
||||||
if line_str.startswith(tag):
|
if line_str.startswith(tag):
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user