Compare commits

...

4 Commits

Author SHA1 Message Date
Alessandro Zarrilli
b66ac1fecf
Merge f52f125097d1f17b4221b65dca11567c6f0bdfa4 into 796e131c3af59fb36714818b2e03cbf5f60d9e0c 2025-10-01 16:11:34 +02:00
milkmaker
796e131c3a
update postscreen_access.cidr (#6801) 2025-10-01 11:14:57 +02:00
Alessandro Zarrilli
f52f125097
Address review comments: use lowercase 'mailcow' 2025-05-11 11:10:17 +02:00
Alessandro Zarrilli
6d3d7a0294
feat: Add mailbox_cleaner.py script for automated cleanup
Adds a Python script to expunge old emails from specified mailboxes (e.g., Trash, Junk) and remove empty subfolders. Supports single-user, all-user, and dry-run modes.
2025-04-21 19:39:43 +02:00
2 changed files with 159 additions and 23 deletions

View File

@ -1,12 +1,13 @@
# Whitelist generated by Postwhite v3.4 on Mon Sep 1 00:23:07 UTC 2025
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
# https://github.com/stevejenkins/postwhite/
# 2165 total rules
# 2216 total rules
2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit
2a01:111:f403::/49 permit
2a01:111:f403:8000::/50 permit
2a01:111:f403:2800::/53 permit
2a01:111:f403:8000::/51 permit
2a01:111:f403::/49 permit
2a01:111:f403:c000::/51 permit
2a01:111:f403:d000::/53 permit
2a01:111:f403:f000::/52 permit
2a01:238:20a:202:5370::1 permit
2a01:238:20a:202:5372::1 permit
@ -55,7 +56,8 @@
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit
13.107.246.40 permit
13.107.213.41 permit
13.107.246.41 permit
13.110.208.0/21 permit
13.110.209.0/24 permit
13.110.216.0/22 permit
@ -174,6 +176,7 @@
35.161.32.253 permit
35.162.73.231 permit
35.167.93.243 permit
35.174.145.124 permit
35.176.132.251 permit
35.205.92.9 permit
35.228.216.85 permit
@ -183,7 +186,6 @@
37.218.249.47 permit
37.218.251.62 permit
39.156.163.64/29 permit
40.90.65.81 permit
40.92.0.0/15 permit
40.92.0.0/16 permit
40.107.0.0/16 permit
@ -271,9 +273,6 @@
50.56.130.221 permit
50.56.130.222 permit
50.112.246.219 permit
51.77.79.158 permit
51.83.17.38 permit
51.89.119.103 permit
52.1.14.157 permit
52.5.230.59 permit
52.6.74.205 permit
@ -324,8 +323,6 @@
52.234.172.96/28 permit
52.235.253.128 permit
52.236.28.240/28 permit
54.36.149.183 permit
54.38.221.122 permit
54.90.148.255 permit
54.165.19.38 permit
54.174.52.0/24 permit
@ -686,6 +683,8 @@
82.165.159.45 permit
82.165.159.130 permit
82.165.159.131 permit
85.9.206.169 permit
85.9.210.45 permit
85.158.136.0/21 permit
85.215.255.39 permit
85.215.255.40 permit
@ -1234,16 +1233,14 @@
99.83.190.102 permit
103.9.96.0/22 permit
103.28.42.0/24 permit
103.122.78.238 permit
103.84.217.238 permit
103.89.75.238 permit
103.151.192.0/23 permit
103.168.172.128/27 permit
103.237.104.0/22 permit
104.43.243.237 permit
104.44.112.128/25 permit
104.47.0.0/17 permit
104.47.20.0/23 permit
104.47.75.0/24 permit
104.47.108.0/23 permit
104.130.96.0/28 permit
104.130.122.0/23 permit
106.10.144.64/27 permit
@ -1378,7 +1375,6 @@
108.174.6.215 permit
108.175.18.45 permit
108.175.30.45 permit
108.177.96.0/20 permit
108.179.144.0/20 permit
109.224.244.0/24 permit
109.237.142.0/24 permit
@ -1544,6 +1540,7 @@
148.105.0.0/16 permit
148.105.8.0/21 permit
149.72.0.0/16 permit
149.72.234.184 permit
149.72.248.236 permit
149.97.173.180 permit
150.230.98.160 permit
@ -1599,6 +1596,7 @@
159.183.0.0/16 permit
159.183.68.71 permit
159.183.79.38 permit
159.183.129.172 permit
160.1.62.192 permit
161.38.192.0/20 permit
161.38.204.0/22 permit
@ -1616,6 +1614,7 @@
163.114.134.16 permit
163.114.135.16 permit
163.116.128.0/17 permit
163.192.116.87 permit
164.152.23.32 permit
164.152.25.241 permit
164.177.132.168/30 permit
@ -1655,6 +1654,7 @@
169.148.131.0/24 permit
169.148.138.0/24 permit
169.148.142.10 permit
169.148.142.33 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
@ -1666,11 +1666,7 @@
170.10.132.56/29 permit
170.10.132.64/29 permit
170.10.133.0/24 permit
172.217.0.0/20 permit
172.217.32.0/20 permit
172.217.128.0/19 permit
172.217.160.0/20 permit
172.217.192.0/19 permit
172.253.56.0/21 permit
172.253.112.0/20 permit
173.0.84.0/29 permit
@ -2209,17 +2205,17 @@
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
2607:f8b0:4000::/36 permit
2620:109:c003:104::215 permit
2620:109:c003:104::/64 permit
2620:109:c006:104::215 permit
2620:109:c003:104::215 permit
2620:109:c006:104::/64 permit
2620:109:c006:104::215 permit
2620:109:c00d:104::/64 permit
2620:10d:c090:400::8:1 permit
2620:10d:c091:400::8:1 permit
2620:10d:c09b:400::8:1 permit
2620:10d:c09c:400::8:1 permit
2620:119:50c0:207::215 permit
2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit
2800:3f0:4000::/36 permit
49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email

140
helper-scripts/mailbox_cleaner.py Executable file
View File

@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
This script cleans up old messages from specified mailboxes (e.g., Trash, Junk)
in a mailcow environment. It can process a single user or all users, and it
supports dry-run mode.
Ideally, this script should be run daily via cron.
"""
import argparse
import logging
import os
import re
import subprocess
import sys
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
DEFAULT_DAYS_BACK: int = 30
DEFAULT_MAILCOW_DIR: str = "/opt/mailcow-dockerized"
DEFAULT_MAILBOXES: list[str] = ["Trash", "Junk"]
def _run_doveadm_command(mailcow_dir: str, user: str | None, command: list[str]) -> str:
"""
Runs a doveadm command within the dovecot-mailcow container.
Args:
mailcow_dir: The path to the mailcow-dockerized directory.
user: The email address of the user to run the command for, or None for all users.
command: The doveadm command to run as a list of strings.
Returns:
The standard output of the command.
"""
command = ["docker", "compose", "--project-directory", mailcow_dir, "exec", "-T", "dovecot-mailcow",
"doveadm"] + command
if user:
command.extend(["-u", user])
logging.debug(f"Executing command: {' '.join(command)}")
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
logging.error(f"Command execution failed: {' '.join(command)} (return code: {e.returncode})")
logging.error(f"Stderr: {e.stderr}")
logging.error(f"Stdout: {e.stdout}")
raise
def main() -> None:
"""
Main function to parse arguments and execute the cleanup process.
"""
parser = argparse.ArgumentParser(
description="Clean up old messages from specified mailboxes in a mailcow environment.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--user", help="Email address of the single user to process.")
group.add_argument("--all", action="store_true", help="Process all users found via doveadm.")
parser.add_argument("--days-back", type=int, default=DEFAULT_DAYS_BACK,
help="Number of days back to consider for message deletion.")
parser.add_argument("--mailcow-directory", default=DEFAULT_MAILCOW_DIR,
help="Path to the mailcow-dockerized directory.")
parser.add_argument("--mailboxes", nargs='+', default=DEFAULT_MAILBOXES,
help="List of top-level mailboxes (and their subfolders) to process (e.g., Trash Junk).")
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
parser.add_argument("--dry-run", action="store_true", help="Perform a dry run without deleting anything.")
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
if not os.path.isdir(args.mailcow_directory):
raise FileNotFoundError(
f"mailcow directory '{args.mailcow_directory}' does not exist or is not a directory.")
# If --all is specified, get all users
if args.all:
doveadm_output = _run_doveadm_command(args.mailcow_directory, None, ["user", "*"])
users = [line.strip() for line in doveadm_output.splitlines() if line.strip()]
# Otherwise, use the specified user
else:
try:
_run_doveadm_command(args.mailcow_directory, None, ["user", args.user])
except subprocess.CalledProcessError:
logging.error(f"User '{args.user}' not found.")
sys.exit(1)
users = [args.user]
logging.info(f"Starting processing for {len(users)} users.")
logging.debug(f"Users to process: {', '.join(users)}")
# Iterate over each user
for user in users:
# Get all mailboxes for the current user
logging.info(f"Processing user: '{user}'.")
doveadm_output = _run_doveadm_command(args.mailcow_directory, user, ["mailbox", "list"])
# get all user mailboxes, sorted in reverse order
mailboxes = sorted([line.strip() for line in doveadm_output.splitlines() if line.strip()], reverse=True)
logging.info(f"User '{user}' has {len(mailboxes)} mailboxes.")
logging.debug(f"Mailboxes for user '{user}': {', '.join(mailboxes)}")
for mailbox in mailboxes:
# Iterate over each mailbox
logging.debug(f"Processing mailbox '{mailbox}' for user '{user}'.")
# Check if the mailbox is a target mailbox
if not any(re.match(rf"{re.escape(tmb)}(/|$)", mailbox, re.IGNORECASE) for tmb in args.mailboxes):
logging.debug(f"Skipping mailbox '{mailbox}' for user '{user}' as it is not a target mailbox.")
continue
# Expunge old messages from the mailbox
logging.info(
f"Expunging messages older than {args.days_back} days from mailbox '{mailbox}' for user '{user}'.")
if args.dry_run:
logging.info(f"[DRY-RUN] Skipping expunge command for mailbox '{mailbox}' of user '{user}'.")
else:
# Run the expunge command
_run_doveadm_command(args.mailcow_directory, user,
["expunge", "mailbox", mailbox, "savedbefore", f"{args.days_back}d"])
# Check if the mailbox is a sub-mailbox
if "/" not in mailbox:
logging.debug(
f"Skipping deletion check for top-level mailbox '{mailbox}' to preserve standard folders.")
continue
# Check if the mailbox is empty
doveadm_output = _run_doveadm_command(args.mailcow_directory, user, ["mailbox", "status", "messages", mailbox])
messages_count = int(doveadm_output.split("=")[1])
logging.debug(f"Mailbox '{mailbox}' for user '{user}' contains {messages_count} messages.")
if messages_count > 0:
logging.info(f"Skipping deletion of mailbox '{mailbox}' for user '{user}' as it is not empty.")
continue
# Delete the mailbox if it's empty
logging.info(f"Deleting mailbox '{mailbox}' for user '{user}' (only if empty).")
if args.dry_run:
logging.info(f"[DRY-RUN] Skipping delete command for mailbox '{mailbox}' of user '{user}'.")
else:
# As a safeguard, -e flag prevents mailbox deletion in case it's not empty
_run_doveadm_command(args.mailcow_directory, user, ["mailbox", "delete", "-e", "-s", mailbox])
if __name__ == "__main__":
main()