Resolve stale failure stuck on import
This commit is contained in:
+51
-2
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
import email
|
import email
|
||||||
import imaplib
|
import imaplib
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from email.message import Message
|
from email.message import Message
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from app.config import InboxConfig
|
from app.config import InboxConfig
|
||||||
|
|
||||||
@@ -24,6 +26,48 @@ class IMAPMessage:
|
|||||||
message: Message
|
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:
|
class IMAPClient:
|
||||||
def __init__(self, inbox: InboxConfig):
|
def __init__(self, inbox: InboxConfig):
|
||||||
self.inbox = inbox
|
self.inbox = inbox
|
||||||
@@ -32,8 +76,13 @@ class IMAPClient:
|
|||||||
def __enter__(self) -> "IMAPClient":
|
def __enter__(self) -> "IMAPClient":
|
||||||
if not self.inbox.username or not self.inbox.password:
|
if not self.inbox.username or not self.inbox.password:
|
||||||
raise IMAPError(f"Missing IMAP credentials for inbox {self.inbox.id}")
|
raise IMAPError(f"Missing IMAP credentials for inbox {self.inbox.id}")
|
||||||
cls = imaplib.IMAP4_SSL if self.inbox.imap_ssl else imaplib.IMAP4
|
try:
|
||||||
self.conn = cls(self.inbox.imap_host, self.inbox.imap_port)
|
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)
|
typ, _ = self.conn.login(self.inbox.username, self.inbox.password)
|
||||||
if typ != "OK":
|
if typ != "OK":
|
||||||
raise IMAPError(f"IMAP login failed for {self.inbox.id}")
|
raise IMAPError(f"IMAP login failed for {self.inbox.id}")
|
||||||
|
|||||||
@@ -274,16 +274,40 @@
|
|||||||
result.className = "inbox-action-result is-error";
|
result.className = "inbox-action-result is-error";
|
||||||
result.textContent = job.error || "Processing failed.";
|
result.textContent = job.error || "Processing failed.";
|
||||||
renderDuplicateReports(inboxId, []);
|
renderDuplicateReports(inboxId, []);
|
||||||
|
progress.classList.remove("is-active", "is-indeterminate");
|
||||||
|
fill.style.width = "0%";
|
||||||
}
|
}
|
||||||
return running;
|
return running;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pollJob = async (jobId, inboxId) => {
|
const pollJob = async (jobId, inboxId) => {
|
||||||
const response = await fetch(`/api/admin/import-jobs/${jobId}`, { credentials: "same-origin" });
|
try {
|
||||||
const job = await response.json();
|
const response = await fetch(`/api/admin/import-jobs/${jobId}`, { credentials: "same-origin" });
|
||||||
if (renderJob(job, inboxId)) {
|
if (!response.ok) {
|
||||||
window.setTimeout(() => pollJob(jobId, inboxId), 2000);
|
throw new Error("Import job status is unavailable.");
|
||||||
} else {
|
}
|
||||||
|
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);
|
renderInboxStatus(inboxId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user