#!/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 <&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 </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 <&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 </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 </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 </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 </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 <&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='Mailcow Exporter v1.0

Mailcow Exporter v1.0

Metrics

' 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 "$@"