a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
427 lines
16 KiB
Bash
Executable File
427 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
|
################################################################################
|
|
# Script Name: mailcow-exporter.sh
|
|
# Version: 1.0
|
|
# Description: Prometheus exporter for Mailcow — exposes container health,
|
|
# mailbox statistics, domain info, rspamd action counts,
|
|
# quarantine status, and version information as Prometheus metrics.
|
|
# Supports stdout, node_exporter textfile collector, and HTTP
|
|
# server modes.
|
|
#
|
|
# Author: Phil Connor
|
|
# Contact: contact@mylinux.work
|
|
# Website: https://mylinux.work
|
|
# License: MIT
|
|
#
|
|
# Prerequisites:
|
|
# - curl and jq
|
|
# - Mailcow API key (read-only recommended)
|
|
# - netcat (nc) for HTTP mode
|
|
#
|
|
# Usage:
|
|
# ./mailcow-exporter.sh # stdout
|
|
# ./mailcow-exporter.sh --textfile # node_exporter textfile
|
|
# ./mailcow-exporter.sh --http -p 9201 # HTTP server
|
|
# ./mailcow-exporter.sh --url https://mail.example.com --api-key KEY
|
|
#
|
|
################################################################################
|
|
|
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
|
|
|
TEXTFILE_DIR="/var/lib/node_exporter"
|
|
OUTPUT_FILE=""
|
|
HTTP_MODE=false
|
|
HTTP_PORT=9201
|
|
|
|
# Source environment file if present (for cron deployments)
|
|
[ -f /etc/default/mailcow-exporter ] && . /etc/default/mailcow-exporter
|
|
|
|
MAILCOW_URL="${MAILCOW_URL:-https://localhost}"
|
|
MAILCOW_API_KEY="${MAILCOW_API_KEY:-}"
|
|
|
|
# ── Helper functions ──────────────────────────────────────────────────────────
|
|
|
|
prom_escape() {
|
|
local s="$1"
|
|
s=${s//\\/\\\\}
|
|
s=${s//\"/\\\"}
|
|
s=${s//$'\n'/\\n}
|
|
printf '%s\n' "$s"
|
|
}
|
|
|
|
show_usage() {
|
|
cat <<EOF
|
|
Usage: $0 [OPTIONS]
|
|
|
|
Prometheus exporter for Mailcow (v1.0).
|
|
|
|
Modes: --textfile Write to node_exporter textfile collector
|
|
--http Run HTTP server on port $HTTP_PORT
|
|
|
|
Options:
|
|
-p, --port PORT HTTP port (default: 9201)
|
|
-o, --output FILE Output file path
|
|
--url URL Mailcow base URL (default: $MAILCOW_URL)
|
|
--api-key KEY Mailcow API key
|
|
|
|
Environment: MAILCOW_URL, MAILCOW_API_KEY
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help) show_usage ;;
|
|
--textfile) OUTPUT_FILE="$TEXTFILE_DIR/mailcow_exporter.prom"; shift ;;
|
|
--http) HTTP_MODE=true; shift ;;
|
|
-p|--port) HTTP_PORT="$2"; shift 2 ;;
|
|
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
|
|
--url) MAILCOW_URL="$2"; shift 2 ;;
|
|
--api-key) MAILCOW_API_KEY="$2"; shift 2 ;;
|
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ── Metrics generation ────────────────────────────────────────────────────────
|
|
|
|
generate_metrics() {
|
|
local script_start
|
|
script_start=$(date +%s)
|
|
|
|
# Prerequisite checks
|
|
local prereq_fail=""
|
|
command -v curl >/dev/null 2>&1 || prereq_fail="curl is required but not found"
|
|
[ -z "$prereq_fail" ] && ! command -v jq >/dev/null 2>&1 && prereq_fail="jq is required but not found"
|
|
[ -z "$prereq_fail" ] && [ -z "$MAILCOW_API_KEY" ] && prereq_fail="MAILCOW_API_KEY is required but not set"
|
|
|
|
if [ -n "$prereq_fail" ]; then
|
|
echo "# ERROR: $prereq_fail" >&2
|
|
cat <<EOF
|
|
# HELP mailcow_exporter_up Exporter status (1=up, 0=down)
|
|
# TYPE mailcow_exporter_up gauge
|
|
mailcow_exporter_up 0
|
|
# HELP mailcow_exporter_info Exporter version information
|
|
# TYPE mailcow_exporter_info gauge
|
|
mailcow_exporter_info{version="1.0"} 1
|
|
EOF
|
|
return
|
|
fi
|
|
|
|
local exporter_up=1
|
|
local api_curl="curl -sf --max-time 10 -k -H X-API-Key:${MAILCOW_API_KEY}"
|
|
|
|
cat <<EOF
|
|
# HELP mailcow_exporter_info Exporter version information
|
|
# TYPE mailcow_exporter_info gauge
|
|
mailcow_exporter_info{version="1.0"} 1
|
|
|
|
EOF
|
|
|
|
# ── Version / Health ─────────────────────────────────────────────
|
|
local version_json
|
|
version_json=$($api_curl "${MAILCOW_URL}/api/v1/get/status/version" 2>/dev/null)
|
|
if [ -n "$version_json" ] && [ "$version_json" != "null" ]; then
|
|
local mc_version
|
|
mc_version=$(echo "$version_json" | jq -r '.version // "unknown"' 2>/dev/null)
|
|
mc_version=$(prom_escape "$mc_version")
|
|
cat <<EOF
|
|
# HELP mailcow_version_info Mailcow version information
|
|
# TYPE mailcow_version_info gauge
|
|
mailcow_version_info{version="${mc_version}"} 1
|
|
|
|
EOF
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/status/version" >&2
|
|
exporter_up=0
|
|
fi
|
|
|
|
# ── Container Health ─────────────────────────────────────────────
|
|
local containers_json container_lines=""
|
|
containers_json=$($api_curl "${MAILCOW_URL}/api/v1/get/status/containers" 2>/dev/null)
|
|
if [ -n "$containers_json" ] && [ "$containers_json" != "null" ]; then
|
|
local container_data
|
|
container_data=$(echo "$containers_json" | jq -r '
|
|
to_entries[] | "\(.key)\t\(.value.state // "unknown")"
|
|
' 2>/dev/null)
|
|
if [ -n "$container_data" ]; then
|
|
while IFS=$'\t' read -r c_name c_state; do
|
|
[ -z "$c_name" ] && continue
|
|
local esc_name up_val
|
|
esc_name=$(prom_escape "$c_name")
|
|
[ "$c_state" = "running" ] && up_val=1 || up_val=0
|
|
container_lines="${container_lines}mailcow_container_up{name=\"${esc_name}\"} ${up_val}
|
|
"
|
|
done <<< "$container_data"
|
|
fi
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/status/containers" >&2
|
|
exporter_up=0
|
|
fi
|
|
|
|
if [ -n "$container_lines" ]; then
|
|
echo "# HELP mailcow_container_up Container running status (1=running, 0=down)"
|
|
echo "# TYPE mailcow_container_up gauge"
|
|
printf '%s' "$container_lines"
|
|
echo ""
|
|
fi
|
|
|
|
# ── Domain Metrics ───────────────────────────────────────────────
|
|
local domains_json domain_count=0 domain_info_lines=""
|
|
domains_json=$($api_curl "${MAILCOW_URL}/api/v1/get/domain/all" 2>/dev/null)
|
|
if [ -n "$domains_json" ] && [ "$domains_json" != "null" ]; then
|
|
domain_count=$(echo "$domains_json" | jq 'length' 2>/dev/null)
|
|
domain_count=${domain_count:-0}
|
|
local domain_data
|
|
domain_data=$(echo "$domains_json" | jq -r '
|
|
.[] | "\(.domain_name)\t\(.active // 0)"
|
|
' 2>/dev/null)
|
|
if [ -n "$domain_data" ]; then
|
|
while IFS=$'\t' read -r d_name d_active; do
|
|
[ -z "$d_name" ] && continue
|
|
local esc_domain esc_active
|
|
esc_domain=$(prom_escape "$d_name")
|
|
esc_active=$(prom_escape "$d_active")
|
|
domain_info_lines="${domain_info_lines}mailcow_domain_info{domain=\"${esc_domain}\",active=\"${esc_active}\"} 1
|
|
"
|
|
done <<< "$domain_data"
|
|
fi
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/domain/all" >&2
|
|
fi
|
|
|
|
cat <<EOF
|
|
# HELP mailcow_domain_count Total number of domains
|
|
# TYPE mailcow_domain_count gauge
|
|
mailcow_domain_count $domain_count
|
|
|
|
EOF
|
|
|
|
if [ -n "$domain_info_lines" ]; then
|
|
echo "# HELP mailcow_domain_info Domain inventory (always 1)"
|
|
echo "# TYPE mailcow_domain_info gauge"
|
|
printf '%s' "$domain_info_lines"
|
|
echo ""
|
|
fi
|
|
|
|
# ── Mailbox Metrics ──────────────────────────────────────────────
|
|
local mailboxes_json mailbox_count=0
|
|
local quota_used_lines="" quota_total_lines="" messages_lines="" active_lines=""
|
|
mailboxes_json=$($api_curl "${MAILCOW_URL}/api/v1/get/mailbox/all" 2>/dev/null)
|
|
if [ -n "$mailboxes_json" ] && [ "$mailboxes_json" != "null" ]; then
|
|
mailbox_count=$(echo "$mailboxes_json" | jq 'length' 2>/dev/null)
|
|
mailbox_count=${mailbox_count:-0}
|
|
local mailbox_data
|
|
mailbox_data=$(echo "$mailboxes_json" | jq -r '
|
|
.[] | "\(.username)\t\(.domain)\t\(.quota_used // 0)\t\(.quota // 0)\t\(.messages // 0)\t\(.active // 0)"
|
|
' 2>/dev/null)
|
|
if [ -n "$mailbox_data" ]; then
|
|
while IFS=$'\t' read -r mb_user mb_domain mb_quota_used mb_quota_total mb_messages mb_active; do
|
|
[ -z "$mb_user" ] && continue
|
|
local esc_mb esc_mbd
|
|
esc_mb=$(prom_escape "$mb_user")
|
|
esc_mbd=$(prom_escape "$mb_domain")
|
|
local labels="mailbox=\"${esc_mb}\",domain=\"${esc_mbd}\""
|
|
quota_used_lines="${quota_used_lines}mailcow_mailbox_quota_used_bytes{${labels}} ${mb_quota_used}
|
|
"
|
|
quota_total_lines="${quota_total_lines}mailcow_mailbox_quota_total_bytes{${labels}} ${mb_quota_total}
|
|
"
|
|
messages_lines="${messages_lines}mailcow_mailbox_messages{${labels}} ${mb_messages}
|
|
"
|
|
active_lines="${active_lines}mailcow_mailbox_active{${labels}} ${mb_active}
|
|
"
|
|
done <<< "$mailbox_data"
|
|
fi
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/mailbox/all" >&2
|
|
fi
|
|
|
|
cat <<EOF
|
|
# HELP mailcow_mailbox_count Total number of mailboxes
|
|
# TYPE mailcow_mailbox_count gauge
|
|
mailcow_mailbox_count $mailbox_count
|
|
|
|
EOF
|
|
|
|
if [ -n "$quota_used_lines" ]; then
|
|
echo "# HELP mailcow_mailbox_quota_used_bytes Quota used per mailbox in bytes"
|
|
echo "# TYPE mailcow_mailbox_quota_used_bytes gauge"
|
|
printf '%s' "$quota_used_lines"
|
|
echo ""
|
|
fi
|
|
if [ -n "$quota_total_lines" ]; then
|
|
echo "# HELP mailcow_mailbox_quota_total_bytes Total quota per mailbox in bytes"
|
|
echo "# TYPE mailcow_mailbox_quota_total_bytes gauge"
|
|
printf '%s' "$quota_total_lines"
|
|
echo ""
|
|
fi
|
|
if [ -n "$messages_lines" ]; then
|
|
echo "# HELP mailcow_mailbox_messages Message count per mailbox"
|
|
echo "# TYPE mailcow_mailbox_messages gauge"
|
|
printf '%s' "$messages_lines"
|
|
echo ""
|
|
fi
|
|
if [ -n "$active_lines" ]; then
|
|
echo "# HELP mailcow_mailbox_active Mailbox active status (1=active, 0=inactive)"
|
|
echo "# TYPE mailcow_mailbox_active gauge"
|
|
printf '%s' "$active_lines"
|
|
echo ""
|
|
fi
|
|
|
|
# ── Rspamd Metrics ───────────────────────────────────────────────
|
|
local rspamd_json rspamd_scanned=0 action_lines=""
|
|
rspamd_json=$($api_curl "${MAILCOW_URL}/api/v1/get/logs/rspamd-history/100" 2>/dev/null)
|
|
if [ -n "$rspamd_json" ] && [ "$rspamd_json" != "null" ]; then
|
|
rspamd_scanned=$(echo "$rspamd_json" | jq 'length' 2>/dev/null)
|
|
rspamd_scanned=${rspamd_scanned:-0}
|
|
local action_data
|
|
action_data=$(echo "$rspamd_json" | jq -r '
|
|
group_by(.action) | .[] | "\(.[0].action)\t\(length)"
|
|
' 2>/dev/null)
|
|
if [ -n "$action_data" ]; then
|
|
while IFS=$'\t' read -r act_name act_count; do
|
|
[ -z "$act_name" ] && continue
|
|
local esc_action
|
|
esc_action=$(prom_escape "$act_name")
|
|
action_lines="${action_lines}mailcow_rspamd_action_total{action=\"${esc_action}\"} ${act_count}
|
|
"
|
|
done <<< "$action_data"
|
|
fi
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/logs/rspamd-history/100" >&2
|
|
fi
|
|
|
|
cat <<EOF
|
|
# HELP mailcow_rspamd_scanned_total Total messages scanned (from recent history)
|
|
# TYPE mailcow_rspamd_scanned_total gauge
|
|
mailcow_rspamd_scanned_total $rspamd_scanned
|
|
|
|
EOF
|
|
|
|
if [ -n "$action_lines" ]; then
|
|
echo "# HELP mailcow_rspamd_action_total Count of rspamd actions by type"
|
|
echo "# TYPE mailcow_rspamd_action_total gauge"
|
|
printf '%s' "$action_lines"
|
|
echo ""
|
|
fi
|
|
|
|
# ── Quarantine ────────────────────────────────────────────────────
|
|
local quarantine_json quarantine_count=0
|
|
quarantine_json=$($api_curl "${MAILCOW_URL}/api/v1/get/quarantine/all" 2>/dev/null)
|
|
if [ -n "$quarantine_json" ] && [ "$quarantine_json" != "null" ]; then
|
|
quarantine_count=$(echo "$quarantine_json" | jq 'length' 2>/dev/null)
|
|
quarantine_count=${quarantine_count:-0}
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/quarantine/all" >&2
|
|
fi
|
|
|
|
cat <<EOF
|
|
# HELP mailcow_quarantine_count Messages currently in quarantine
|
|
# TYPE mailcow_quarantine_count gauge
|
|
mailcow_quarantine_count $quarantine_count
|
|
|
|
EOF
|
|
|
|
# ── Rate Limit Status ────────────────────────────────────────────
|
|
local ratelimit_json
|
|
ratelimit_json=$($api_curl "${MAILCOW_URL}/api/v1/get/rl-mbox" 2>/dev/null)
|
|
if [ -n "$ratelimit_json" ] && [ "$ratelimit_json" != "null" ]; then
|
|
echo "# Rate limit data retrieved successfully"
|
|
else
|
|
echo "# WARNING: could not read /api/v1/get/rl-mbox" >&2
|
|
fi
|
|
|
|
# ── Exporter Runtime ─────────────────────────────────────────────
|
|
local script_end script_duration
|
|
script_end=$(date +%s)
|
|
script_duration=$((script_end - script_start))
|
|
|
|
cat <<EOF
|
|
# HELP mailcow_exporter_up Exporter status (1=up, 0=down)
|
|
# TYPE mailcow_exporter_up gauge
|
|
mailcow_exporter_up $exporter_up
|
|
|
|
# HELP mailcow_exporter_duration_seconds Time to generate all metrics
|
|
# TYPE mailcow_exporter_duration_seconds gauge
|
|
mailcow_exporter_duration_seconds $script_duration
|
|
|
|
# HELP mailcow_exporter_last_run_timestamp Unix timestamp of last successful run
|
|
# TYPE mailcow_exporter_last_run_timestamp gauge
|
|
mailcow_exporter_last_run_timestamp $script_end
|
|
EOF
|
|
echo ""
|
|
}
|
|
|
|
# ── HTTP server mode ──────────────────────────────────────────────────────────
|
|
|
|
run_http_server() {
|
|
echo "# Starting mailcow exporter on port $HTTP_PORT..." >&2
|
|
|
|
if ! command -v nc >/dev/null 2>&1; then
|
|
echo "# ERROR: netcat (nc) required for HTTP mode" >&2
|
|
exit 1
|
|
fi
|
|
|
|
trap 'echo "# Shutting down mailcow exporter..." >&2; exit 0' INT TERM
|
|
|
|
while true; do
|
|
{
|
|
read -r request
|
|
local body
|
|
if [[ "$request" =~ ^GET\ /metrics ]]; then
|
|
body=$(generate_metrics)
|
|
printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" "${#body}" "$body"
|
|
else
|
|
body='<html><head><title>Mailcow Exporter v1.0</title></head><body><h1>Mailcow Exporter v1.0</h1><p><a href="/metrics">Metrics</a></p></body></html>'
|
|
printf "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" "${#body}" "$body"
|
|
fi
|
|
} | if nc -h 2>&1 | grep -q 'GNU\|traditional'; then
|
|
nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
|
|
else
|
|
nc -l "$HTTP_PORT" 2>/dev/null
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ── Main execution ────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
|
|
if [ "$HTTP_MODE" = true ]; then
|
|
run_http_server
|
|
elif [ -n "$OUTPUT_FILE" ]; then
|
|
local output_dir
|
|
output_dir="$(dirname "$OUTPUT_FILE")"
|
|
mkdir -p "$output_dir"
|
|
|
|
local temp_file
|
|
temp_file=$(mktemp "${output_dir}/.mailcow_metrics.XXXXXX")
|
|
|
|
if ! generate_metrics > "$temp_file" 2>/dev/null; then
|
|
rm -f "$temp_file"
|
|
echo "# ERROR: Failed to generate metrics" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local file_lines
|
|
file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0)
|
|
|
|
if [ "$file_lines" -lt 3 ]; then
|
|
rm -f "$temp_file"
|
|
echo "# ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2
|
|
exit 1
|
|
fi
|
|
|
|
chmod 644 "$temp_file"
|
|
mv -f "$temp_file" "$OUTPUT_FILE"
|
|
|
|
echo "# Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2
|
|
else
|
|
generate_metrics
|
|
fi
|
|
}
|
|
|
|
main "$@"
|