Resolve stale failure stuck on import

This commit is contained in:
2026-05-16 16:23:15 -03:00
parent b70ac9bc6f
commit 636d3b73cb
2 changed files with 80 additions and 7 deletions
+51 -2
View File
@@ -3,9 +3,11 @@ from __future__ import annotations
import email
import imaplib
import logging
import socket
from dataclasses import dataclass
from datetime import date
from email.message import Message
from typing import Any
from app.config import InboxConfig
@@ -24,6 +26,48 @@ class IMAPMessage:
message: Message
def _resolved_addresses(host: str, port: int) -> list[tuple[Any, tuple]]:
return [(family, sockaddr) for family, _, _, _, sockaddr in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)]
class _ResolvedIMAP4(imaplib.IMAP4):
def __init__(self, host: str, port: int, addresses: list[tuple[Any, tuple]]):
self._resolved_addresses = addresses
super().__init__(host, port)
def _create_socket(self, timeout):
last_error: Exception | None = None
for family, sockaddr in self._resolved_addresses:
try:
if timeout is not None:
return socket.create_connection(sockaddr, timeout)
return socket.create_connection(sockaddr)
except OSError as exc:
last_error = exc
logger.warning("IMAP connect failed for %s via %s: %s", self.host, sockaddr[0], exc)
raise IMAPError(f"Could not connect to IMAP host {self.host}: {last_error}")
class _ResolvedIMAP4SSL(imaplib.IMAP4_SSL):
def __init__(self, host: str, port: int, addresses: list[tuple[Any, tuple]]):
self._resolved_addresses = addresses
super().__init__(host, port)
def _create_socket(self, timeout):
last_error: Exception | None = None
for family, sockaddr in self._resolved_addresses:
try:
if timeout is not None:
sock = socket.create_connection(sockaddr, timeout)
else:
sock = socket.create_connection(sockaddr)
return self.ssl_context.wrap_socket(sock, server_hostname=self.host)
except OSError as exc:
last_error = exc
logger.warning("IMAP SSL connect failed for %s via %s: %s", self.host, sockaddr[0], exc)
raise IMAPError(f"Could not connect to IMAP host {self.host}: {last_error}")
class IMAPClient:
def __init__(self, inbox: InboxConfig):
self.inbox = inbox
@@ -32,8 +76,13 @@ class IMAPClient:
def __enter__(self) -> "IMAPClient":
if not self.inbox.username or not self.inbox.password:
raise IMAPError(f"Missing IMAP credentials for inbox {self.inbox.id}")
cls = imaplib.IMAP4_SSL if self.inbox.imap_ssl else imaplib.IMAP4
self.conn = cls(self.inbox.imap_host, self.inbox.imap_port)
try:
addresses = _resolved_addresses(self.inbox.imap_host, self.inbox.imap_port)
except socket.gaierror as exc:
raise IMAPError(f"Could not resolve IMAP host {self.inbox.imap_host!r} for inbox {self.inbox.id}: {exc}") from exc
logger.info("IMAP host resolved for inbox %s: %r -> %s", self.inbox.id, self.inbox.imap_host, [item[1][0] for item in addresses])
cls = _ResolvedIMAP4SSL if self.inbox.imap_ssl else _ResolvedIMAP4
self.conn = cls(self.inbox.imap_host, self.inbox.imap_port, addresses)
typ, _ = self.conn.login(self.inbox.username, self.inbox.password)
if typ != "OK":
raise IMAPError(f"IMAP login failed for {self.inbox.id}")
+29 -5
View File
@@ -274,16 +274,40 @@
result.className = "inbox-action-result is-error";
result.textContent = job.error || "Processing failed.";
renderDuplicateReports(inboxId, []);
progress.classList.remove("is-active", "is-indeterminate");
fill.style.width = "0%";
}
return running;
};
const pollJob = async (jobId, inboxId) => {
const response = await fetch(`/api/admin/import-jobs/${jobId}`, { credentials: "same-origin" });
const job = await response.json();
if (renderJob(job, inboxId)) {
window.setTimeout(() => pollJob(jobId, inboxId), 2000);
} else {
try {
const response = await fetch(`/api/admin/import-jobs/${jobId}`, { credentials: "same-origin" });
if (!response.ok) {
throw new Error("Import job status is unavailable.");
}
const job = await response.json();
if (renderJob(job, inboxId)) {
window.setTimeout(() => pollJob(jobId, inboxId), 2000);
} else {
renderInboxStatus(inboxId);
}
} catch (error) {
const result = document.getElementById(`inbox-action-result-${inboxId}`);
const progress = document.getElementById(`inbox-progress-${inboxId}`);
const fill = progress?.querySelector("span");
const buttons = document.querySelectorAll(`.js-inbox-action[data-inbox-id="${inboxId}"]`);
buttons.forEach((button) => { button.disabled = false; });
if (progress) {
progress.classList.remove("is-active", "is-indeterminate");
}
if (fill) {
fill.style.width = "0%";
}
if (result) {
result.className = "inbox-action-result is-error";
result.textContent = error.message || "Import job status is unavailable.";
}
renderInboxStatus(inboxId);
}
};