Membersihkan Deferred Queue Zimbra yang Menumpuk Karena "Recipient's Inbox is Out of Storage Space"

6 min read
Membersihkan Deferred Queue Zimbra yang Menumpuk Karena "Recipient's Inbox is Out of Storage Space"

Kalau kamu admin Zimbra Mail Server, pasti pernah lihat monitoring mailq atau dashboard Zimbra menampilkan angka deferred email yang tidak kunjung habis, bahkan terus bertambah tiap hari. Setelah dicek satu-satu, penyebabnya ternyata sama: mailbox tujuan (kebanyakan Gmail) sudah penuh, jadi Google membalas dengan bounce message:

The recipient's inbox is out of storage space.

Kabar baiknya: ini bukan masalah di server Zimbra kamu. Kabar kurang enaknya: kalau dibiarkan, Postfix (mesin pengirim email di balik Zimbra) akan terus mencoba mengirim ulang email tersebut selama beberapa hari (default maximal queue lifetime Postfix adalah 5 hari), sehingga antrian deferred menumpuk dan bikin laporan queue jadi berantakan.

Di artikel ini saya akan share script bash yang saya pakai untuk otomatis menghapus email deferred yang bouncenya karena mailbox penuh dan sudah nyangkut lebih dari 24 jam, lengkap dengan error-error yang saya temui saat development-nya (biar kalau kamu ketemu error yang sama, tidak perlu pusing lagi).


Kenapa Tidak Dibiarkan Saja?

Beberapa alasan kenapa email seperti ini sebaiknya dibersihkan lebih cepat daripada menunggu Postfix menyerah sendiri:

  • Antrian jadi bersih — memudahkan monitoring, jadi kamu bisa lebih cepat notice kalau ada masalah pengiriman yang beneran perlu ditindak (misal domain di-blacklist, DNS salah, dsb), bukan tenggelam di antara ratusan email "mailbox penuh".
  • Resource server lebih hemat — Postfix tetap melakukan percobaan ulang (retry) untuk tiap pesan deferred secara berkala, yang berarti ada beban tambahan walau kecil.
  • Realistis — mailbox Gmail yang penuh biasanya tidak akan tiba-tiba kosong dalam hitungan jam. Kalau setelah 24 jam masih gagal dengan alasan yang sama, kemungkinan besar akan tetap gagal sampai limit hari ke-5 juga.

Persiapan

Sebelum mulai, pastikan:

  1. Kamu punya akses root atau sudo ke server Zimbra.
  2. Tahu di mana lokasi binary postqueue dan postsuper di server kamu. Default-nya ada di:
    /opt/zimbra/common/sbin/postqueue
    /opt/zimbra/common/sbin/postsuper
    
  3. Backup / catat dulu kalau perlu — walaupun script ini sudah ada mode "cek dulu" sebelum hapus beneran.

Kenalan Dulu dengan Istilahnya

Biar tidak bingung, ini penjelasan singkat istilah yang dipakai:

Istilah Penjelasan
Queue Antrian email yang sedang diproses Postfix
Deferred Status email yang gagal terkirim dan akan dicoba lagi nanti
Bounce Pesan error balasan dari server tujuan kenapa email gagal masuk
postqueue -p Perintah untuk melihat isi antrian email (mirip mailq)
postsuper -d Perintah untuk menghapus email tertentu dari antrian

Script-nya

Simpan kode berikut sebagai deferred-cleanup.sh di server Zimbra kamu, misalnya di /opt/zimbra/scripts/.

#!/bin/bash
#
# deferred-cleanup.sh
#
# Menghapus pesan di deferred queue Zimbra/Postfix yang:
#   1. Alasan bounce-nya mengandung frasa tertentu (default: "out of storage space")
#   2. Sudah berada di queue lebih dari N jam (default: 24 jam)
#
# Jalankan sebagai root:
#   sudo ./deferred-cleanup.sh            # mode interaktif (tanya konfirmasi)
#   sudo ./deferred-cleanup.sh -y          # auto-confirm (cocok untuk cron)
#   sudo ./deferred-cleanup.sh -n          # dry-run, cuma tampilkan tanpa hapus
#
# Bisa juga dijadwalkan lewat cron root, misal tiap jam:
#   0 * * * * /opt/zimbra/scripts/deferred-cleanup.sh -y >> /opt/zimbra/log/bersihkan-deferred.log 2>&1

set -euo pipefail

# ---------- KONFIGURASI ----------
POSTQUEUE_BIN="/opt/zimbra/common/sbin/postqueue"
POSTSUPER_BIN="/opt/zimbra/common/sbin/postsuper"
REASON_PATTERN="out of storage space"   # ubah/tambah kalau mau cocokkan pola lain (case-insensitive)
MAX_AGE_HOURS=24
AUTO_YES=0
DRY_RUN=0
LOG_DIR="/opt/zimbra/log"   # ubah lewat -L kalau perlu
# ----------------------------------

while getopts "ynL:" opt; do
  case $opt in
    y) AUTO_YES=1 ;;
    n) DRY_RUN=1 ;;
    L) LOG_DIR="$OPTARG" ;;
    *) echo "Usage: $0 [-y] [-n] [-L log_dir]"; exit 1 ;;
  esac
done

mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/deferred-cleanup-$(date +%Y%m%d-%H%M%S).log"

if [[ ! -x "$POSTQUEUE_BIN" ]]; then
  echo "postqueue tidak ditemukan di $POSTQUEUE_BIN, sesuaikan path-nya." >&2
  exit 1
fi

NOW_EPOCH=$(date +%s)
MAX_AGE_SECONDS=$((MAX_AGE_HOURS * 3600))

DELETE_LIST=()
DELETE_INFO=()   # baris ringkasan "ID | tanggal | sender -> recipient(s)" untuk ditampilkan ke user

CURRENT_ID=""
CURRENT_DATE=""
CURRENT_SENDER=""
CURRENT_STATUS=""
CURRENT_RECIPIENTS=()
MATCH_REASON=0

EMAIL_REGEX='^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'

process_block() {
  if [[ -n "$CURRENT_ID" && "$CURRENT_STATUS" == "deferred" && $MATCH_REASON -eq 1 ]]; then
    local ARR_EPOCH
    ARR_EPOCH=$(date -d "$CURRENT_DATE" +%s 2>/dev/null || echo "")
    if [[ -n "$ARR_EPOCH" ]]; then
      local AGE=$((NOW_EPOCH - ARR_EPOCH))
      if [[ $AGE -gt $MAX_AGE_SECONDS ]]; then
        DELETE_LIST+=("$CURRENT_ID")
        local RCPT_JOINED
        RCPT_JOINED=$(IFS=,; echo "${CURRENT_RECIPIENTS[*]}")
        DELETE_INFO+=("$CURRENT_ID | $CURRENT_DATE | $CURRENT_SENDER -> $RCPT_JOINED")
      fi
    fi
  fi
  CURRENT_ID=""
  CURRENT_DATE=""
  CURRENT_SENDER=""
  CURRENT_STATUS=""
  CURRENT_RECIPIENTS=()
  MATCH_REASON=0
}

# Baris header tiap pesan di output "postqueue -p" contohnya:
# A1B2C3D4E5F6      1234 Mon Aug 24 09:12:34  sender@domain.com
# (kalau ada "!" atau "*" setelah queue id, artinya statusnya bukan deferred biasa)
HEADER_REGEX='^([A-F0-9]{6,})(\*|!)?[[:space:]]+([0-9]+)[[:space:]]+([A-Za-z]{3}[[:space:]]+[A-Za-z]{3}[[:space:]]+[0-9]+[[:space:]]+[0-9:]+)[[:space:]]+(.*)$'

while IFS= read -r line; do
  if [[ -z "$line" ]]; then
    process_block
    continue
  fi

  if [[ "$line" =~ $HEADER_REGEX ]]; then
    process_block
    CURRENT_ID="${BASH_REMATCH[1]}"
    CURRENT_DATE="${BASH_REMATCH[4]}"
    CURRENT_SENDER="${BASH_REMATCH[5]}"
    if [[ "${BASH_REMATCH[2]}" == "!" ]]; then
      CURRENT_STATUS="hold"
    else
      CURRENT_STATUS="deferred"
    fi
    continue
  fi

  TRIMMED_LINE="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"

  if echo "$line" | grep -qi "$REASON_PATTERN"; then
    MATCH_REASON=1
  fi

  # baris recipient: tidak diawali "(" dan berbentuk alamat email
  if [[ "$TRIMMED_LINE" =~ $EMAIL_REGEX ]]; then
    CURRENT_RECIPIENTS+=("$TRIMMED_LINE")
  fi
done < <("$POSTQUEUE_BIN" -p)
process_block

{
  echo "=== Ringkasan ==="
  echo "Waktu jalan        : $(date '+%Y-%m-%d %H:%M:%S')"
  echo "Pola alasan bounce : \"$REASON_PATTERN\""
  echo "Batas umur         : > $MAX_AGE_HOURS jam"
  echo "Jumlah ditemukan   : ${#DELETE_LIST[@]}"
  echo
} | tee "$LOG_FILE"

if [[ ${#DELETE_LIST[@]} -eq 0 ]]; then
  echo "Tidak ada pesan yang cocok kriteria. Selesai." | tee -a "$LOG_FILE"
  exit 0
fi

{
  echo "QUEUE_ID | ARRIVAL_DATE | SENDER -> RECIPIENT(S)"
  printf '%s\n' "${DELETE_INFO[@]}"
  echo
} | tee -a "$LOG_FILE"

if [[ $DRY_RUN -eq 1 ]]; then
  echo "(dry-run) Tidak ada yang dihapus." | tee -a "$LOG_FILE"
  echo "Log tersimpan di: $LOG_FILE"
  exit 0
fi

if [[ $AUTO_YES -eq 0 ]]; then
  read -r -p "Hapus ${#DELETE_LIST[@]} pesan di atas dari queue? (y/n) " CONFIRM
  if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
    echo "Dibatalkan." | tee -a "$LOG_FILE"
    echo "Log tersimpan di: $LOG_FILE"
    exit 0
  fi
fi

# postsuper -d hanya menerima SATU queue id per flag -d.
# Kalau banyak id ditaruh setelah satu -d, id ke-2 dst akan dianggap sebagai
# nama direktori queue -> muncul error "invalid directory name: <id>".
# Jadi flag -d harus diulang untuk tiap id.
POSTSUPER_ARGS=()
for qid in "${DELETE_LIST[@]}"; do
  POSTSUPER_ARGS+=("-d" "$qid")
done

"$POSTSUPER_BIN" "${POSTSUPER_ARGS[@]}"

{
  echo "Status              : BERHASIL DIHAPUS"
  echo "Selesai             : $(date '+%Y-%m-%d %H:%M:%S')"
  echo "Berhasil menghapus ${#DELETE_LIST[@]} pesan dari deferred queue."
} | tee -a "$LOG_FILE"

echo
echo "Log lengkap tersimpan di: $LOG_FILE"

Kasih izin eksekusi:

chmod +x deferred-cleanup.sh

Cara Pakai

1. Cek dulu sebelum menghapus (mode dry-run)

Selalu mulai dengan mode -n supaya kamu bisa lihat dulu email mana saja yang akan terhapus — lengkap dengan pengirim dan penerimanya — sebelum ada yang benar-benar dihapus:

sudo ./deferred-cleanup.sh -n

Contoh outputnya:

=== Ringkasan ===
Waktu jalan        : 2026-08-25 14:02:11
Pola alasan bounce : "out of storage space"
Batas umur         : > 24 jam
Jumlah ditemukan   : 268

QUEUE_ID | ARRIVAL_DATE | SENDER -> RECIPIENT(S)
F20803F98AAB | Thu Aug 20 20:28:56 | sender@domainku.com -> receiver1@gmail.com
3DBA13FC0D27 | Sat Aug 22 10:01:02 | sender@domainku.com -> receiver2@gmail.com
...
(dry-run) Tidak ada yang dihapus.

2. Kalau sudah yakin, jalankan mode hapus

sudo ./deferred-cleanup.sh -y

Flag -y artinya "auto-confirm", cocok kalau nanti mau dijadwalkan otomatis lewat cron. Kalau mau tetap ditanya konfirmasi dulu, jalankan tanpa flag apa pun.

3. Semua hasilnya otomatis tersimpan sebagai log

Setiap kali dijalankan (baik -n maupun -y), script otomatis membuat file log baru berisi ringkasan dan daftar email yang diproses, jadi kamu tidak perlu repot redirect manual (> log.txt) lagi. Default-nya tersimpan di:

/opt/zimbra/log/deferred-cleanup-<tanggal-jam>.log

Mau simpan di lokasi lain? Tinggal tambahkan flag -L:

sudo ./deferred-cleanup.sh -y -L /backup/zimbra/logs

Biar Otomatis Jalan Sendiri (Cron)

Kalau sudah yakin script-nya bekerja dengan baik, jadwalkan supaya jalan otomatis, misalnya tiap jam:

sudo crontab -e

Tambahkan baris berikut:

0 * * * * /opt/zimbra/scripts/deferred-cleanup.sh -y

Dengan begini, tiap jam server akan otomatis membersihkan deferred queue yang bouncenya karena mailbox penuh dan sudah lebih dari 24 jam, tanpa perlu kamu jalankan manual lagi.


Error yang Mungkin Kamu Temui (dan Solusinya)

Waktu develop script ini, saya sendiri sempat ketemu dua error berikut. Saya tulis di sini biar kalau kamu ketemu error yang sama, sudah tahu solusinya.

❌ Error 1: postsuper: fatal: use of this command is reserved for the superuser

Penyebab: postsuper -d (perintah untuk menghapus dari queue) hanya boleh dijalankan oleh root, atau user yang terdaftar sebagai mail_owner di konfigurasi Postfix (biasanya postfix, bukan zimbra). Kalau kamu menjalankan script ini lewat su - zimbra, perintah postqueue -p (baca antrian) memang bisa jalan, tapi postsuper -d akan gagal.

Solusi: Jalankan script langsung sebagai root/sudo, bukan lewat user zimbra:

sudo ./deferred-cleanup.sh -y

❌ Error 2: postsuper: fatal: invalid directory name: <queue_id>

Penyebab: Ternyata postsuper -d cuma menerima satu ID per flag -d. Kalau kita kasih banyak ID sekaligus setelah satu -d (misalnya postsuper -d ID1 ID2 ID3), Postfix akan menganggap ID kedua dan seterusnya sebagai nama direktori queue — bukan ID pesan — makanya muncul error "invalid directory name".

Solusi: Ulangi flag -d untuk setiap ID (postsuper -d ID1 -d ID2 -d ID3 ...). Script di atas sudah menangani ini otomatis lewat bagian:

POSTSUPER_ARGS=()
for qid in "${DELETE_LIST[@]}"; do
  POSTSUPER_ARGS+=("-d" "$qid")
done
"$POSTSUPER_BIN" "${POSTSUPER_ARGS[@]}"

Tips Tambahan

  • Ubah kata kunci bounce: kalau selain "out of storage space" kamu juga mau cover kasus lain seperti "quota" atau "mailbox is full", cukup ubah variabel REASON_PATTERN di bagian atas script.
  • Ubah batas waktu: mau lebih agresif (misal 12 jam) atau lebih longgar (48 jam)? Tinggal ubah MAX_AGE_HOURS.
  • Penghapusan ini permanen — email yang dihapus dengan postsuper -d tidak akan mengirim notifikasi bounce balik ke pengirim aslinya (beda dengan menunggu Postfix menyerah sendiri setelah maximal_queue_lifetime habis, yang otomatis mengirim NDR/bounce ke pengirim). Kalau pengirim perlu tahu emailnya gagal terkirim, pertimbangkan untuk menurunkan maximal_queue_lifetime di Zimbra saja, daripada menghapus paksa.

Penutup

Dengan script sederhana ini, deferred queue Zimbra yang tadinya menumpuk ratusan email gara-gara mailbox Gmail penuh bisa otomatis dibersihkan setiap jam, lengkap dengan log untuk audit. Semoga tutorial ini membantu kamu yang lagi struggle sama masalah yang sama ya!

Kalau ada pertanyaan atau ketemu error lain, tulis di kolom komentar 👇