Starting from June 4, 2025 Let’s Encrypt stops sending their expiration notification emails and that is why I have faced with the need to find a replacement.

Let’s Encrypt suggested both payed and free-to-use 3rd-party services, but since I have my own e-mail server and it is running 24/7 I’ve decided to write an automation script.

First try was very straightforward and not actually usable: I need to schedule a job running at the specific date, calculated from certificates expiration date, extracted manually. Of course it is needed to be replaced with fully automated one, with date calculation directly from certificate files and scheduled only once (to run every day, but do an action only as needed).

The script has comments and in this way it is a self-explained. Feel free to use and update it for your needs.

#!/bin/bash
set -euo pipefail

# User settings
CERTS=(example.com pimpl.dev)
REPORT_TO="admin"
CERTS_DIR="/etc/letsencrypt/live/"

# Script tools
MAIL="/usr/bin/mail"

# Global script vars
issues=()

# Format message and send mail
send_report() {
  local days=$1
  # Calc expiration date
  local date="$(date -u -d "+$1 days" +"%Y-%m-%d")"
  # Convert domains list to indented list
  local domains="$(printf '\t%s\n' "${issues[@]}")"
  # Prepare subject and message body
  local subject="Certificates expire in $days days."
  local message=$(cat <<EOF
Dear administrator,

Your certificates expire on $date (in $days days)
for the following domains:
$domains

Please renew them in time.
Also, don't forget to update certificate for router
and copy all certificates to the sky server.

Regards,
Your Script.

EOF
)

  # Send mail report
  echo "$message" | $MAIL -s "$subject" $REPORT_TO

}

# Returns 0 if certificate $2 expires within $1 seconds
will_expire_within() {
  ! openssl x509 -checkend "$1" -noout -in "$2" >/dev/null 2>&1
}

# Check certificate expiration
check_cert() {
  local cert_file="$CERTS_DIR/$1/cert.pem"
  local seconds_before=$(($2*86400))
  local seconds_not_before=$((($2-1)*86400))

  if will_expire_within $seconds_before "$cert_file" &&
    ! will_expire_within $seconds_not_before "$cert_file"
  then
    issues+=($1)
  fi
}

# Check all certificates and send mail if needed
main() {
  for cert in "${CERTS[@]}"; do
    check_cert "$cert" $1
  done
  
  if (( ${#issues[@]} )); then
    send_report $1
  fi
}

main $@

Configuration is pretty easy. Keep in mind that an user must has access to your certificates. In general, you just need to setup a cron job like the following:

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * command to be executed
# ...
# certificate expirity report
  15 10 *  *  * sh /usr/local/bin/cert-report.sh 3
  16 10 *  *  * sh /usr/local/bin/cert-report.sh 7