From b164c4d1b2956e2a20b3e542445ff4d70a420a30 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 2 May 2026 07:26:22 +0200 Subject: [PATCH] feat: add tool to analyze deferred queue It prints all destinations with the number of recipients and all the reasons. Operator can then try to fix the problems for destinations, e.g. by manually adding reverse proxy addresses to /etc/hosts for failing domains or routing IP addresses to another interface. --- chatmaild/pyproject.toml | 1 + chatmaild/src/chatmaild/deferred.py | 37 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 chatmaild/src/chatmaild/deferred.py diff --git a/chatmaild/pyproject.toml b/chatmaild/pyproject.toml index 01ef93d2..24ac39da 100644 --- a/chatmaild/pyproject.toml +++ b/chatmaild/pyproject.toml @@ -24,6 +24,7 @@ chatmail-metadata = "chatmaild.metadata:main" chatmail-expire = "chatmaild.expire:daily_expire_main" chatmail-quota-expire = "chatmaild.expire:quota_expire_main" chatmail-fsreport = "chatmaild.fsreport:main" +chatmail-deferred = "chatmaild.deferred:main" lastlogin = "chatmaild.lastlogin:main" turnserver = "chatmaild.turnserver:main" diff --git a/chatmaild/src/chatmaild/deferred.py b/chatmaild/src/chatmaild/deferred.py new file mode 100644 index 00000000..24903dc9 --- /dev/null +++ b/chatmaild/src/chatmaild/deferred.py @@ -0,0 +1,37 @@ +""" +Analyze deferred mails and print most common failing destinations. + +Example: + + python -m chatmaild.deferred +""" + +import json +import subprocess +from collections import Counter, defaultdict + + +def main(): + p = subprocess.Popen(["postqueue", "-j"], text=True, stdout=subprocess.PIPE) + domain_reasons = defaultdict(Counter) + domain_total = Counter() + + for line in p.stdout: + item = json.loads(line) + if item["queue_name"] != "deferred": + continue + + for recipient in item["recipients"]: + _, domain = recipient["address"].rsplit("@", 1) + reason = recipient["delay_reason"] + domain_total[domain] += 1 + domain_reasons[domain][reason] += 1 + + for domain, total in reversed(domain_total.most_common()): + print(f"{domain} ({total} recipients)") + for reason, count in domain_reasons[domain].most_common(): + print(f" {count}: {reason}") + + +if __name__ == "__main__": + main()