#!/bin/bash ################################################################################ # Script Name: dovecot-metrics-exporter.sh # Description: Prometheus exporter for Dovecot IMAP/POP3 server metrics # # Collects connection counts, authentication stats, mailbox operations, # process info, and protocol-level metrics from doveadm and Dovecot stats. # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # Version: 1.0 # # Usage: # # Output to stdout # ./dovecot-metrics-exporter.sh # # # Textfile collector mode (atomic write) # ./dovecot-metrics-exporter.sh --textfile # # # Custom output file # ./dovecot-metrics-exporter.sh -o /path/to/metrics.prom # ################################################################################ # ============================================================================ # CONFIGURATION VARIABLES # ============================================================================ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HOSTNAME=$(hostname) # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2; exit 1 ;; esac done } # Safe integer extraction — returns 0 on failure safe_int() { local val="$1" if [[ "$val" =~ ^[0-9]+$ ]]; then echo "$val" else echo 0 fi } # ============================================================================ # METRIC GENERATION # ============================================================================ generate_metrics() { local START_TIME START_TIME=$(date +%s.%N) # --- Exporter info --- echo "# HELP dovecot_up Exporter status (1=up, 0=down)" echo "# TYPE dovecot_up gauge" # Check if Dovecot is running if systemctl is-active --quiet dovecot 2>/dev/null; then echo "dovecot_up 1" else echo "dovecot_up 0" fi echo "" echo "# HELP dovecot_exporter_info Exporter version information" echo "# TYPE dovecot_exporter_info gauge" echo 'dovecot_exporter_info{version="1.0"} 1' echo "" # --- Dovecot version --- echo "# HELP dovecot_version_info Dovecot version information" echo "# TYPE dovecot_version_info gauge" local dovecot_version dovecot_version=$(dovecot --version 2>/dev/null | awk '{print $1}') || dovecot_version="unknown" echo "dovecot_version_info{version=\"${dovecot_version}\"} 1" echo "" # --- Process counts --- echo "# HELP dovecot_processes Number of running Dovecot processes by type" echo "# TYPE dovecot_processes gauge" for proc_type in imap pop3 lmtp managesieve submission auth anvil; do count=$(pgrep -c "dovecot/${proc_type}" 2>/dev/null) || count=0 echo "dovecot_processes{type=\"${proc_type}\"} ${count}" done local total_procs total_procs=$(pgrep -c dovecot 2>/dev/null) || total_procs=0 echo "dovecot_processes{type=\"total\"} ${total_procs}" echo "" # --- Connected users (from doveadm) --- echo "# HELP dovecot_connected_users Number of currently connected users by protocol" echo "# TYPE dovecot_connected_users gauge" local imap_users=0 pop3_users=0 lmtp_users=0 managesieve_users=0 if command -v doveadm >/dev/null 2>&1; then imap_users=$(doveadm who -1 2>/dev/null | grep -c 'imap' 2>/dev/null) || imap_users=0 pop3_users=$(doveadm who -1 2>/dev/null | grep -c 'pop3' 2>/dev/null) || pop3_users=0 lmtp_users=$(doveadm who -1 2>/dev/null | grep -c 'lmtp' 2>/dev/null) || lmtp_users=0 managesieve_users=$(doveadm who -1 2>/dev/null | grep -c 'managesieve' 2>/dev/null) || managesieve_users=0 fi echo "dovecot_connected_users{protocol=\"imap\"} ${imap_users}" echo "dovecot_connected_users{protocol=\"pop3\"} ${pop3_users}" echo "dovecot_connected_users{protocol=\"lmtp\"} ${lmtp_users}" echo "dovecot_connected_users{protocol=\"managesieve\"} ${managesieve_users}" echo "" # --- Total connections (from doveadm who) --- echo "# HELP dovecot_connections_total Total active connections by protocol" echo "# TYPE dovecot_connections_total gauge" local imap_conns=0 pop3_conns=0 if command -v doveadm >/dev/null 2>&1; then imap_conns=$(doveadm who -1 2>/dev/null | grep 'imap' | awk '{sum+=$3} END {print sum+0}' 2>/dev/null) || imap_conns=0 pop3_conns=$(doveadm who -1 2>/dev/null | grep 'pop3' | awk '{sum+=$3} END {print sum+0}' 2>/dev/null) || pop3_conns=0 fi echo "dovecot_connections_total{protocol=\"imap\"} ${imap_conns}" echo "dovecot_connections_total{protocol=\"pop3\"} ${pop3_conns}" echo "" # --- Authentication stats from mail.log --- local LOG_FILE="/var/log/mail.log" if [[ ! -f "$LOG_FILE" ]]; then LOG_FILE="/var/log/maillog" fi echo "# HELP dovecot_auth_success_total Successful authentication attempts by protocol" echo "# TYPE dovecot_auth_success_total counter" local imap_auth_ok=0 pop3_auth_ok=0 if [[ -f "$LOG_FILE" ]]; then imap_auth_ok=$(grep -c 'imap-login: Info: Login:' "$LOG_FILE" 2>/dev/null) || imap_auth_ok=0 pop3_auth_ok=$(grep -c 'pop3-login: Info: Login:' "$LOG_FILE" 2>/dev/null) || pop3_auth_ok=0 fi echo "dovecot_auth_success_total{protocol=\"imap\"} ${imap_auth_ok}" echo "dovecot_auth_success_total{protocol=\"pop3\"} ${pop3_auth_ok}" echo "" echo "# HELP dovecot_auth_failed_total Failed authentication attempts by protocol" echo "# TYPE dovecot_auth_failed_total counter" local imap_auth_fail=0 pop3_auth_fail=0 if [[ -f "$LOG_FILE" ]]; then imap_auth_fail=$(grep -c 'imap-login:.*auth failed\|imap-login: Info: Aborted login' "$LOG_FILE" 2>/dev/null) || imap_auth_fail=0 pop3_auth_fail=$(grep -c 'pop3-login:.*auth failed\|pop3-login: Info: Aborted login' "$LOG_FILE" 2>/dev/null) || pop3_auth_fail=0 fi echo "dovecot_auth_failed_total{protocol=\"imap\"} ${imap_auth_fail}" echo "dovecot_auth_failed_total{protocol=\"pop3\"} ${pop3_auth_fail}" echo "" # --- TLS connections --- echo "# HELP dovecot_tls_connections_total TLS connections by status" echo "# TYPE dovecot_tls_connections_total counter" local tls_yes=0 tls_no=0 if [[ -f "$LOG_FILE" ]]; then tls_yes=$(grep -c 'Login:.*TLS' "$LOG_FILE" 2>/dev/null) || tls_yes=0 tls_no=$(grep 'Login:' "$LOG_FILE" 2>/dev/null | grep -cv 'TLS' 2>/dev/null) || tls_no=0 fi echo "dovecot_tls_connections_total{tls=\"yes\"} ${tls_yes}" echo "dovecot_tls_connections_total{tls=\"no\"} ${tls_no}" echo "" # --- Authentication methods --- echo "# HELP dovecot_auth_method_total Logins by authentication method" echo "# TYPE dovecot_auth_method_total counter" if [[ -f "$LOG_FILE" ]]; then for method in PLAIN LOGIN CRAM-MD5 DIGEST-MD5; do count=$(grep -c "Login:.*method=${method}" "$LOG_FILE" 2>/dev/null) || count=0 echo "dovecot_auth_method_total{method=\"${method}\"} ${count}" done else for method in PLAIN LOGIN CRAM-MD5 DIGEST-MD5; do echo "dovecot_auth_method_total{method=\"${method}\"} 0" done fi echo "" # --- Disconnections --- echo "# HELP dovecot_disconnections_total Client disconnections by reason" echo "# TYPE dovecot_disconnections_total counter" local dc_logout=0 dc_timeout=0 dc_closed=0 dc_internal=0 if [[ -f "$LOG_FILE" ]]; then dc_logout=$(grep -c 'Logged out' "$LOG_FILE" 2>/dev/null) || dc_logout=0 dc_timeout=$(grep -c 'Disconnected.*Timed out\|Connection timed out' "$LOG_FILE" 2>/dev/null) || dc_timeout=0 dc_closed=$(grep -c 'Disconnected.*Connection closed' "$LOG_FILE" 2>/dev/null) || dc_closed=0 dc_internal=$(grep -c 'Disconnected.*Internal error' "$LOG_FILE" 2>/dev/null) || dc_internal=0 fi echo "dovecot_disconnections_total{reason=\"logout\"} ${dc_logout}" echo "dovecot_disconnections_total{reason=\"timeout\"} ${dc_timeout}" echo "dovecot_disconnections_total{reason=\"connection_closed\"} ${dc_closed}" echo "dovecot_disconnections_total{reason=\"internal_error\"} ${dc_internal}" echo "" # --- LMTP delivery stats --- echo "# HELP dovecot_lmtp_deliveries_total LMTP deliveries by status" echo "# TYPE dovecot_lmtp_deliveries_total counter" local lmtp_ok=0 lmtp_reject=0 lmtp_tempfail=0 if [[ -f "$LOG_FILE" ]]; then lmtp_ok=$(grep -c 'lmtp.*saved mail' "$LOG_FILE" 2>/dev/null) || lmtp_ok=0 lmtp_reject=$(grep -c 'lmtp.*rejected' "$LOG_FILE" 2>/dev/null) || lmtp_reject=0 lmtp_tempfail=$(grep -c 'lmtp.*temporary failure\|lmtp.*temp-fail' "$LOG_FILE" 2>/dev/null) || lmtp_tempfail=0 fi echo "dovecot_lmtp_deliveries_total{status=\"delivered\"} ${lmtp_ok}" echo "dovecot_lmtp_deliveries_total{status=\"rejected\"} ${lmtp_reject}" echo "dovecot_lmtp_deliveries_total{status=\"tempfail\"} ${lmtp_tempfail}" echo "" # --- Sieve stats --- echo "# HELP dovecot_sieve_actions_total Sieve filter actions" echo "# TYPE dovecot_sieve_actions_total counter" local sieve_filed=0 sieve_discard=0 sieve_redirect=0 sieve_reject=0 if [[ -f "$LOG_FILE" ]]; then sieve_filed=$(grep -c 'sieve:.*stored mail\|sieve:.*fileinto' "$LOG_FILE" 2>/dev/null) || sieve_filed=0 sieve_discard=$(grep -c 'sieve:.*discard' "$LOG_FILE" 2>/dev/null) || sieve_discard=0 sieve_redirect=$(grep -c 'sieve:.*redirect' "$LOG_FILE" 2>/dev/null) || sieve_redirect=0 sieve_reject=$(grep -c 'sieve:.*reject' "$LOG_FILE" 2>/dev/null) || sieve_reject=0 fi echo "dovecot_sieve_actions_total{action=\"filed\"} ${sieve_filed}" echo "dovecot_sieve_actions_total{action=\"discard\"} ${sieve_discard}" echo "dovecot_sieve_actions_total{action=\"redirect\"} ${sieve_redirect}" echo "dovecot_sieve_actions_total{action=\"reject\"} ${sieve_reject}" echo "" # --- Dovecot stats (if old_stats or stats plugin enabled) --- # Try doveadm stats dump for Dovecot 2.3+ echo "# HELP dovecot_mail_commands_total Mail commands executed" echo "# TYPE dovecot_mail_commands_total counter" local cmds_select=0 cmds_fetch=0 cmds_store=0 cmds_search=0 cmds_copy=0 cmds_expunge=0 if command -v doveadm >/dev/null 2>&1; then local stats_output stats_output=$(doveadm stats dump session 2>/dev/null | head -20) if [[ -n "$stats_output" ]]; then cmds_select=$(echo "$stats_output" | awk '{sum+=$4} END {print sum+0}') || cmds_select=0 cmds_fetch=$(echo "$stats_output" | awk '{sum+=$5} END {print sum+0}') || cmds_fetch=0 fi fi # Fallback: count from logs if [[ -f "$LOG_FILE" ]]; then cmds_copy=$(grep -c 'Copy\|copy' "$LOG_FILE" 2>/dev/null | head -1) || cmds_copy=0 cmds_expunge=$(grep -c 'Expunged' "$LOG_FILE" 2>/dev/null) || cmds_expunge=0 fi echo "dovecot_mail_commands_total{command=\"copy\"} ${cmds_copy}" echo "dovecot_mail_commands_total{command=\"expunge\"} ${cmds_expunge}" echo "" # --- Mail storage quota (top users if doveadm quota available) --- echo "# HELP dovecot_quota_usage_bytes User quota usage in bytes (top users)" echo "# TYPE dovecot_quota_usage_bytes gauge" echo "# HELP dovecot_quota_limit_bytes User quota limit in bytes" echo "# TYPE dovecot_quota_limit_bytes gauge" if command -v doveadm >/dev/null 2>&1; then doveadm quota get -A 2>/dev/null | grep 'STORAGE' | head -20 | while IFS=$'\t' read -r user type value limit _; do local usage_bytes=$((value * 1024)) local limit_bytes=$((limit * 1024)) echo "dovecot_quota_usage_bytes{user=\"${user}\"} ${usage_bytes}" echo "dovecot_quota_limit_bytes{user=\"${user}\"} ${limit_bytes}" done 2>/dev/null fi echo "" # --- Dovecot uptime --- echo "# HELP dovecot_uptime_seconds Dovecot process uptime in seconds" echo "# TYPE dovecot_uptime_seconds gauge" local dovecot_pid uptime_seconds=0 dovecot_pid=$(pgrep -o dovecot 2>/dev/null) || dovecot_pid="" if [[ -n "$dovecot_pid" ]] && [[ -d "/proc/${dovecot_pid}" ]]; then local start_time start_time=$(stat -c %Y "/proc/${dovecot_pid}" 2>/dev/null) || start_time=0 if [[ "$start_time" -gt 0 ]]; then uptime_seconds=$(( $(date +%s) - start_time )) fi fi echo "dovecot_uptime_seconds ${uptime_seconds}" echo "" # --- Memory usage --- echo "# HELP dovecot_memory_bytes Total memory usage of all Dovecot processes" echo "# TYPE dovecot_memory_bytes gauge" local total_mem=0 total_mem=$(pgrep dovecot 2>/dev/null | xargs -I {} cat /proc/{}/status 2>/dev/null | awk '/VmRSS/{sum+=$2} END {print sum*1024+0}') || total_mem=0 echo "dovecot_memory_bytes ${total_mem}" echo "" # --- Script execution time --- local END_TIME END_TIME=$(date +%s.%N) local DURATION DURATION=$(echo "$END_TIME - $START_TIME" | bc) echo "# HELP dovecot_exporter_duration_seconds Time to generate all metrics" echo "# TYPE dovecot_exporter_duration_seconds gauge" echo "dovecot_exporter_duration_seconds ${DURATION}" echo "" echo "# HELP dovecot_exporter_last_run_timestamp Unix timestamp of last successful run" echo "# TYPE dovecot_exporter_last_run_timestamp gauge" echo "dovecot_exporter_last_run_timestamp $(date +%s)" } # ============================================================================ # MAIN EXECUTION # ============================================================================ main() { parse_args "$@" if [ -n "$OUTPUT_FILE" ]; then local output_dir output_dir="$(dirname "$OUTPUT_FILE")" mkdir -p "$output_dir" local temp_file temp_file=$(mktemp "${output_dir}/.dovecot_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 10 ]; 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 "$@"