A webhook-based daemon that automatically purges deleted user mailboxes after a retention period.
- Listens for user deletion webhooks from userli
- Stores mailbox deletion tasks in a simple CSV file (easy to edit manually)
- Automatically purges mailboxes using a configurable command after configured retention period (default: 24h)
- Updates last login time for inactive users who have sieve scripts with unconditional redirect rules
- HMAC SHA256 webhook signature verification
- Background worker with ticker for processing tasks
- Structured logging with zap
- Configurable via environment variables
- Webhook Reception: Receives
user.deletedevents via HTTP POST to/userli - CSV Storage: Stores the email and creation timestamp in a CSV file
- Background Processing: A ticker runs periodically (configurable interval) to check for due mailboxes
- Mailbox Purging: Executes the configured
PURGER_COMMANDwith placeholders replaced for each due mailbox - Cleanup: Removes successfully purged mailboxes from the CSV file
- Fetch Inactive Users: Periodically queries the Userli API for users marked as inactive
- Sieve Script Analysis: For each inactive user, reads their sieve configuration file
- Redirect Detection: Checks if the sieve script contains an unconditional redirect rule (
if true { redirect ... }) - Touch User: If a redirect rule is found, updates the user's last login timestamp via the Userli API
- Retention Extension: This prevents the user from being marked for deletion, allowing mail forwarding to continue
git clone https://github.com/systemli/userli-mailbox-janitor.git
cd userli-mailbox-janitor
go build -o userli-mailbox-janitorConfiguration is done via environment variables:
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL |
Logging level (debug, info, warn, error) | info |
LISTEN_ADDR |
HTTP server listen address | :8080 |
WEBHOOK_SECRET |
Secret for HMAC SHA256 signature verification | required |
PURGER_DATABASE_PATH |
Path to CSV file for storing mailbox data | ./mailboxes.csv |
PURGER_RETENTION_HOURS |
Hours to wait before purging mailbox | 24 |
PURGER_TICK_INTERVAL |
Interval for checking due mailboxes (e.g., "5m", "1h") | 1h |
PURGER_COMMAND |
Command to purge mailbox, with placeholders {email}, {domain}, and {local_part} |
echo 'No PURGER_COMMAND configured; skipping purge for {domain}/{local_part}' |
TOUCHER_TICK_INTERVAL |
Interval for checking inactive users for retention (e.g., "1h", "6h") | 24h |
TOUCHER_SIEVE_LOCATION |
Path pattern for sieve files with placeholders {domain} and {local_part} |
required |
TOUCHER_USE_SUDO |
Whether to use sudo when accessing sieve files (true/false) | false |
USERLI_URL |
Base URL of the Userli API | required |
USERLI_TOKEN |
API token for Userli authentication | required |
export WEBHOOK_SECRET="your-secret-here"
export PURGER_DATABASE_PATH="/var/lib/mailbox-janitor/mailboxes.csv"
export TOUCHER_SIEVE_LOCATION="/var/vmail/{domain}/{local_part}/.dovecot.sieve"
export USERLI_URL="https://userli.example.org"
export USERLI_TOKEN="your-api-token-here"
./userli-mailbox-janitorConfigure userli to send webhooks to your janitor instance:
WEBHOOK_URL="https://mailbox-janitor.example.org/userli"
SECRET="your-secret-here"
PAYLOAD='{"type":"user.deleted","timestamp":"2025-01-01T00:00:00.000000Z","data":{"email":"[email protected]"}}'
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -i "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: $SIGNATURE" \
-d "$PAYLOAD"The Toucher feature requires access to the following Userli API endpoints:
GET /api/retention/users- Fetch list of inactive usersPUT /api/retention/{email}/touch- Update user's last login timestamp
The Toucher feature analyzes sieve scripts to identify users who have configured unconditional email forwarding. This is useful for maintaining active forwarding for users who may appear inactive but are still using the service to redirect their emails.
The system looks for sieve scripts containing the following pattern:
if true {
redirect "[email protected]";
}Will trigger touch (unconditional redirect):
# User has configured forwarding
if true {
redirect "[email protected]";
}Will NOT trigger touch (conditional redirect):
# Spam filtering with conditional redirect
if header :contains "X-Spam-Flag" "YES" {
redirect "[email protected]";
}go test -v ./...go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outgolangci-lint runThis project is licensed under the GNU Affero General Public License v3.0 - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions, please use the GitHub issue tracker.