fix: drop empty emails, clear stale errors on success, harden IMAP extraction
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/d50e7f06-4c6f-4caa-b5dd-329068e8096c Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -396,14 +396,17 @@ class MailProcessor:
|
||||
email_data = line
|
||||
break
|
||||
|
||||
if email_data:
|
||||
if email_data and email_data.strip():
|
||||
emails.append(email_data)
|
||||
new_uids.append(uid_str)
|
||||
fetched_uids.append(uid)
|
||||
else:
|
||||
logger.warning(
|
||||
f"No email data extracted for UID {uid_str} on "
|
||||
f"account {self.account.id}"
|
||||
f"account {self.account.id} "
|
||||
f"(raw bytes: {len(email_data) if email_data else 0}); "
|
||||
"this may indicate a server-side error producing empty "
|
||||
"IMAP RFC822 responses"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -204,6 +204,7 @@ async def process_mail_account(account_id: int):
|
||||
return
|
||||
|
||||
successfully_forwarded_uids: list[str] = []
|
||||
skipped_empty_uids: list[str] = []
|
||||
|
||||
if len(emails) != len(new_uids):
|
||||
logger.error(
|
||||
@@ -238,6 +239,55 @@ async def process_mail_account(account_id: int):
|
||||
forwarded_ok = False
|
||||
error_msg: str | None = None
|
||||
|
||||
# ── Detect completely empty emails ───────────────────────────
|
||||
# T-Online (and potentially other servers) may return empty
|
||||
# RFC822 responses. An email with no subject, no sender and no
|
||||
# body provides no value and should not be forwarded. We log a
|
||||
# warning (it may indicate a server-side bug) and skip the
|
||||
# message while still recording its UID so it is not retried.
|
||||
if not email_subject and not email_from:
|
||||
body_has_content = False
|
||||
try:
|
||||
if msg.is_multipart(): # type: ignore[possibly-undefined]
|
||||
for part in msg.walk():
|
||||
payload = part.get_payload(decode=True)
|
||||
if isinstance(payload, bytes) and payload.strip():
|
||||
body_has_content = True
|
||||
break
|
||||
else:
|
||||
payload = msg.get_payload(decode=True) # type: ignore[possibly-undefined]
|
||||
body_has_content = isinstance(payload, bytes) and bool(
|
||||
payload.strip()
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not body_has_content:
|
||||
logger.warning(
|
||||
"Dropping completely empty email (uid=%s, account=%s, "
|
||||
"size=%d bytes) — no subject, sender, or body; "
|
||||
"this may indicate a server-side error.",
|
||||
uid,
|
||||
account.id,
|
||||
email_size_bytes,
|
||||
)
|
||||
skipped_empty_uids.append(uid)
|
||||
db.add(
|
||||
ProcessingLog(
|
||||
user_id=account.user_id,
|
||||
mail_account_id=account.id,
|
||||
processing_run_id=run.id,
|
||||
level="WARNING",
|
||||
message="Dropped empty email (no subject, sender, or body)",
|
||||
email_subject=None,
|
||||
email_from=None,
|
||||
email_size_bytes=email_size_bytes,
|
||||
success=False,
|
||||
error_details={"reason": "empty_email"},
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
if use_gmail_api and gmail_service and gmail_cred:
|
||||
# Inject via Gmail API (preferred)
|
||||
@@ -331,6 +381,17 @@ async def process_mail_account(account_id: int):
|
||||
)
|
||||
)
|
||||
|
||||
# Also persist UIDs of dropped empty emails so they are not
|
||||
# re-fetched and re-evaluated on the next run.
|
||||
for uid in skipped_empty_uids:
|
||||
if uid not in already_seen_uids:
|
||||
db.add(
|
||||
DownloadedMessageId(
|
||||
mail_account_id=account.id,
|
||||
message_uid=uid,
|
||||
)
|
||||
)
|
||||
|
||||
# If Gmail API was used, persist any refreshed access token back to
|
||||
# the DB so the next run doesn't need an extra token-refresh call.
|
||||
if use_gmail_api and gmail_service and gmail_cred:
|
||||
@@ -361,6 +422,8 @@ async def process_mail_account(account_id: int):
|
||||
if emails_failed == 0:
|
||||
account.last_successful_check_at = datetime.now(timezone.utc) # type: ignore[assignment]
|
||||
account.status = AccountStatus.ACTIVE # type: ignore[assignment]
|
||||
account.last_error_message = None # type: ignore[assignment]
|
||||
account.last_error_at = None # type: ignore[assignment]
|
||||
else:
|
||||
account.status = AccountStatus.ERROR # type: ignore[assignment]
|
||||
account.last_error_at = datetime.now(timezone.utc) # type: ignore[assignment]
|
||||
|
||||
Reference in New Issue
Block a user