Zimbra Password Expired Reminder: 5 Easy Steps, Fix 3 Bugs

A Zimbra password expired reminder is a small script that mails each user a few days before the password expires. The version in the original article looked fine, but when it was tested its day counter was off by one and it never sent a single reminder. This rewrite shows the test, explains the three problems found, and gives a corrected script.

How this was tested: there is no Zimbra server in the author's lab. The script ran in a debian:13 container where the Zimbra tools (zmprov, zmlocalconfig, ldapsearch, sendmail) were replaced by small stand-ins that print the same kind of lines, and faketime fixed "today" at 2026-11-01 08:00, a typical cron time. So the date arithmetic and the mail logic are real test results, while the Zimbra tools are simulated. The Zimbra facts come from the Zimbra Administrator Guide.

How the Zimbra password expired policy works

Zimbra handles a password expired state itself. In the Admin Console, under the class of service (COS), the Password options include a minimum and maximum password age: "users can change their passwords at any time between the minimum and maximum. They must change it when the maximum password age is reached." The script does not enforce anything about a password expired condition. It only warns users so they are not surprised by a forced change, so the POLICY number in the script must equal the maximum age you set in the COS (120 days in the original post).

Test the original Zimbra password expired script

The test used six accounts whose passwords were set 113, 117, 119, 50 and 130 days ago, and one account with no password date. With a 120 day policy, the first three should get reminders with 7, 3 and 1 days left (the values FIRST=7, LAST=3 and the final 1 in the script). The result:

$ faketime "2026-11-01 08:00:00" bash orig.sh
Account: alice@example.com reports; 6 days on Password policy
Account: bob@example.com reports; 2 days on Password policy
Account: carol@example.com reports; 0 days on Password policy
Account: dave@example.com reports; 69 days on Password policy
Account: erin@example.com reports; -10 days on Password policy
Account: frank@example.com reports; 120 days on Password policy

$ grep -E "^(--- sendmail|Subject)" /tmp/mail.log
grep: /tmp/mail.log: No such file or directory

No mail was sent, and three things are wrong:

  • Off by one day. The script subtracts an expiry date at midnight from the current time with hours included. At 08:00, a user with 7 days left shows 6, because the shell drops the fraction. The checks for 7, 3 and 1 therefore fire a day early, and the user with exactly 1 day left shows 0 and gets nothing.
  • Missing date means 120 days. Frank has no zimbraPasswordModifiedTime, the empty value makes date -d " 120 days" count from today, and the script reports a comfortable 120 days.
  • Expired accounts are ignored. Erin's password expired 10 days ago and the report line only says -10 days; nobody is told.
Zimbra password expired script test: the original script reports 6, 2 and 0 days and sends no mail
The original script counts 6, 2 and 0 days instead of 7, 3 and 1.

Keep the LDAP password out of the password expired script's command line

The original script reads zimbra_ldap_password with zmlocalconfig -s (the Admin Guide shows the same key = value output) and hands it to ldapsearch -w. Any user on the server can then read it from the process list while the command runs:

$ ps -eo user,args | grep [l]dapsearch -H
root     /bin/bash /opt/zimbra/common/bin/ldapsearch -H ldap://ldap.example.com:389 -w S3cretLdapPw -D uid=zimbra,cn=admins,cn=zimbra -x (mail=a@b)

The fix is to ask Zimbra instead of LDAP: zmprov -l ga user zimbraPasswordModifiedTime needs no password in the command, because it runs as the zimbra user. The corrected script below uses it.

A corrected Zimbra password expired reminder script

The new password expired check counts whole days from midnight, skips accounts without a password date, reports expired accounts, and builds a mail with real headers. Mail is sent with the sendmail binary that Zimbra ships; check its path on your version with the find command in the comment. The original post told readers to install ldap-utils and sendmail with apt, which is not needed because the script itself looks for Zimbra's own copies under /opt/zimbra.

#!/bin/bash
# Zimbra password expiry reminder - daily cron job, run as the zimbra user.
POLICY=120                      # days; must match the COS maximum password age
WARN_DAYS="7 3 1"               # send a reminder when this many days are left
FROM="admin@example.com"
SENDMAIL=/opt/zimbra/common/sbin/sendmail   # check: find /opt/zimbra -name sendmail -type f
TODAY=$(date -d "$(date +%F)" +%s)          # midnight, so whole days are counted

for user in $(zmprov -l gaa | grep -Ev '^(spam|ham|virus-quarantine|galsync)\.'); do
  set_date=$(zmprov -l ga "$user" zimbraPasswordModifiedTime | awk '/^zimbraPasswordModifiedTime:/ {print substr($2,1,8)}')
  if [ -z "$set_date" ]; then
    echo "SKIP $user: no zimbraPasswordModifiedTime"
    continue
  fi
  expires=$(date -d "$set_date $POLICY days" +%s)
  left=$(( (expires - TODAY) / 86400 ))
  if [ "$left" -lt 0 ]; then
    echo "EXPIRED $user: $(( -left )) days ago"
  elif [[ " $WARN_DAYS " == *" $left "* ]]; then
    printf 'From: %s\nTo: %s\nSubject: Your mail password expires in %s days\n\nPlease change your password in Preferences > General.\n' \
      "$FROM" "$user" "$left" | "$SENDMAIL" -f "$FROM" "$user"
    echo "SENT $user: $left days left"
  else
    echo "OK $user: $left days left"
  fi
done

The relative date date -d "$set_date $POLICY days" is standard GNU date syntax, described in the coreutils manual. The same fake date and the same six accounts, now with the corrected script:

$ su - zimbra -c "faketime \"2026-11-01 08:00:00\" bash /root/new.sh"
SENT alice@example.com: 7 days left
SENT bob@example.com: 3 days left
SENT carol@example.com: 1 days left
OK dave@example.com: 70 days left
EXPIRED erin@example.com: 10 days ago
SKIP frank@example.com: no zimbraPasswordModifiedTime

$ cat /tmp/mail.log | head -12
--- sendmail -f admin@example.com alice@example.com
From: admin@example.com
To: alice@example.com
Subject: Your mail password expires in 7 days

Please change your password in Preferences > General.
--- sendmail -f admin@example.com bob@example.com
From: admin@example.com
To: bob@example.com
Subject: Your mail password expires in 3 days

Please change your password in Preferences > General.

Now the password expired warnings reach the three accounts on the right days, the account without a date is reported instead of hidden, and the expired account is visible in the log.

Schedule the reminder and check the log

Install the script as the zimbra user and let cron run it once a day, for example 0 8 * * * /opt/zimbra/scripts/password_reminder.sh >> /opt/zimbra/log/password_reminder.log 2>&1 (create the folder first and keep the log). Test it by hand first, and read the log after the first run. If the reminder mentions accounts that cannot log in at all, the guides on locking and unlocking a Zimbra account and checking account status help you decide whether those accounts should be skipped.

Common password expired reminder mistakes

  • Setting POLICY to a number that differs from the COS maximum password age, so a password expired in Zimbra is never announced.
  • Running the script at a different time of day without counting whole days, which moves the day when mails are sent.
  • Sending to system accounts such as spam, ham and virus-quarantine users (the script filters them with grep).
  • Keeping the LDAP password in a script or on a command line.

FAQ

Where do I set the limit that makes a password expired? In the Admin Console under Configure, Class of Service, your COS, Advanced, Password. The Administrator Guide lists the minimum and maximum password age there.

Does the password expired script work for accounts in several domains? Yes; it takes accounts from zmprov -l gaa, which lists all accounts.

Comments

  1. Hi,
    everything works well except the admin account doesn’t receive expired account information. any suggestion?
    Thanks,
    Ritz

    ReplyDelete
  2. Hi Team,

    I got this below error while running the above script,

    ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
    Account: myuser@mpradeep.cf reports; 120 days on Password policy

    ReplyDelete

Post a Comment

Popular posts from this blog

How to Play Minecraft Bedrock Edition on Linux: A Comprehensive Guide for Tech Professionals

Linux desktop frozen? My 5 go-to tricks to try - before forcing a hard reboot

The Ultimate Guide: How to Set Up DXVK in Wine on Linux for Enhanced Gaming Performance