Resolve stale failure stuck on import
This commit is contained in:
+51
-2
@@ -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}")
|
||||
|
||||
@@ -274,18 +274,42 @@
|
||||
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) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll(".inbox-action-result[data-job-id]").forEach((result) => {
|
||||
|
||||
Reference in New Issue
Block a user