Keeping a consistent, up-to-date company directory on every desk phone can be a pain—especially when you’re managing multiple Yealink devices across a business. In this guide, we’ll generate a Yealink-compatible XML directory directly from the FreePBX users table, place it in /tftpboot, and enable it across all Yealink phones using EPM > Base File Edit—so your remote phonebook stays current automatically with no per-phone configuration.
This guide shows how to:
- Generate a Yealink-compatible directory XML from FreePBX users.
- Place it in
/tftpboot. - Make it available at your provisioning URL.
- Enable it on all Yealink phones using a few lines added to the Yealink Base File.
What We’re Building
We want Yealink phones to successfully download a remote phonebook from:
__provisionAddress__/company_directory.xml
Using a Yealink-compatible XML structure like:
<?xml version="1.0" encoding="UTF-8"?>
<CompanyIPPhoneDirectory>
<DirectoryEntry>
<Name>Aaron Atwood</Name>
<Telephone>5584</Telephone>
</DirectoryEntry>
</CompanyIPPhoneDirectory>
1) Create the XML Generator Script
Create this script on your FreePBX v17 system:
sudo nano /usr/local/sbin/xml_directory.sh
Paste the full script below.
xml_directory.sh (Yealink-compatible output)
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
# Build an XML Company Directory for Yealink IP phones
# Pulls extension + name from FreePBX "users" table.
#
# Output structure:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <CompanyIPPhoneDirectory>
# <DirectoryEntry>
# <Name>...</Name>
# <Telephone>...</Telephone>
# </DirectoryEntry>
# </CompanyIPPhoneDirectory>
#
# FreePBX 17 notes:
# - /etc/freepbx.conf is PHP and often the authoritative place for AMPDB*.
# - This script reads AMPDB* from amportal.conf if present, otherwise
# pulls them from freepbx.conf via php.
# -----------------------------------------------------------------------------
# --- TFTP directory ---
TFTP_DIR="/tftpboot"
FILE="${TFTP_DIR}/company_directory.xml"
TMP="$(mktemp)"
# --- Defaults ---
DBUSER=""
DBPASS=""
DBNAME="asterisk"
DBHOST="localhost"
# --- Helper: read value from /etc/freepbx.conf (PHP) ---
get_php_amp_conf() {
local key="$1"
if command -v php >/dev/null 2>&1 && [[ -f /etc/freepbx.conf ]]; then
php -r "
error_reporting(0);
\$amp_conf = [];
include '/etc/freepbx.conf';
if (isset(\$amp_conf['$key'])) { echo \$amp_conf['$key']; }
" 2>/dev/null || true
fi
}
# --- First choice: amportal.conf (older style) ---
if [[ -f /etc/asterisk/amportal.conf ]]; then
# shellcheck disable=SC1091
source /etc/asterisk/amportal.conf || true
DBUSER="${AMPDBUSER:-}"
DBPASS="${AMPDBPASS:-}"
DBNAME="${AMPDBNAME:-asterisk}"
DBHOST="${AMPDBHOST:-localhost}"
fi
# --- Second choice: /etc/freepbx.conf (PHP amp_conf array) ---
if [[ -z "$DBUSER" ]]; then
DBUSER="$(get_php_amp_conf "AMPDBUSER")"
DBPASS="$(get_php_amp_conf "AMPDBPASS")"
DBNAME="$(get_php_amp_conf "AMPDBNAME")"
DBHOST="$(get_php_amp_conf "AMPDBHOST")"
DBNAME="${DBNAME:-asterisk}"
DBHOST="${DBHOST:-localhost}"
fi
# --- Last-ditch: grep patterns just in case ---
if [[ -z "$DBUSER" && -f /etc/freepbx.conf ]]; then
DBUSER="$(grep -E "AMPDBUSER" /etc/freepbx.conf | sed -n "s/.*\['AMPDBUSER'\][[:space:]]*=[[:space:]]*'\([^']*\)'.*/\1/p" | head -n1 || true)"
DBPASS="$(grep -E "AMPDBPASS" /etc/freepbx.conf | sed -n "s/.*\['AMPDBPASS'\][[:space:]]*=[[:space:]]*'\([^']*\)'.*/\1/p" | head -n1 || true)"
DBNAME="$(grep -E "AMPDBNAME" /etc/freepbx.conf | sed -n "s/.*\['AMPDBNAME'\][[:space:]]*=[[:space:]]*'\([^']*\)'.*/\1/p" | head -n1 || true)"
DBHOST="$(grep -E "AMPDBHOST" /etc/freepbx.conf | sed -n "s/.*\['AMPDBHOST'\][[:space:]]*=[[:space:]]*'\([^']*\)'.*/\1/p" | head -n1 || true)"
DBNAME="${DBNAME:-asterisk}"
DBHOST="${DBHOST:-localhost}"
fi
if [[ -z "$DBUSER" ]]; then
echo "❌ ERROR: Could not determine FreePBX DB credentials."
echo " Checked:"
echo " - /etc/asterisk/amportal.conf"
echo " - /etc/freepbx.conf (PHP \$amp_conf array)"
exit 1
fi
# --- XML escape helper ---
xml_escape() {
local s="${1:-}"
s="${s//&/&}"
s="${s//</<}"
s="${s//>/>}"
s="${s//\"/"}"
s="${s//\'/'}"
printf '%s' "$s"
}
# --- Build XML header ---
{
echo '<?xml version="1.0" encoding="UTF-8"?>'
echo '<CompanyIPPhoneDirectory>'
} > "$TMP"
# --- Query DB safely ---
QUERY="SELECT extension, name FROM users WHERE extension IS NOT NULL AND extension <> '' ORDER BY name, extension;"
MYSQL_CMD=(mysql -N -B -h "$DBHOST" -u "$DBUSER")
if [[ -n "${DBPASS:-}" ]]; then
MYSQL_CMD+=(-p"$DBPASS")
fi
MYSQL_CMD+=("$DBNAME" -e "$QUERY")
# Read rows (extension<TAB>name)
while IFS=$'\t' read -r exten name; do
[[ -z "${exten:-}" ]] && continue
name="${name:-}"
# If name is missing, fall back to extension
display_name="$name"
if [[ -z "$display_name" ]]; then
display_name="$exten"
fi
# Escape for XML
esc_name="$(xml_escape "$display_name")"
esc_ext="$(xml_escape "$exten")"
{
echo ' <DirectoryEntry>'
echo " <Name>${esc_name}</Name>"
echo " <Telephone>${esc_ext}</Telephone>"
echo ' </DirectoryEntry>'
} >> "$TMP"
done < <("${MYSQL_CMD[@]}")
# --- Close XML ---
echo '</CompanyIPPhoneDirectory>' >> "$TMP"
# --- Install final file atomically ---
mkdir -p "$TFTP_DIR"
mv "$TMP" "$FILE"
# Ownership/permissions
chown asterisk:asterisk "$FILE" 2>/dev/null || true
chmod 0644 "$FILE"
echo "✅ OK - wrote $FILE"
Make it executable:
sudo chmod +x /usr/local/sbin/xml_directory.sh
2) Run It Once Manually
sudo -u asterisk /usr/local/sbin/xml_directory.sh
Confirm the output:
ls -l /tftpboot/company_directory.xml
head -n 20 /tftpboot/company_directory.xml
3) Ensure the File is Served by the Provisioning Web Server
Most EPM setups already serve the provisioning directory via the PBX’s web server.
To confirm the phone can reach it, test from another machine:
curl -i http://YOUR_PBX_FQDN/company_directory.xml
or if your provisioning is IP-based:
curl -i http://YOUR_PBX_IP/company_directory.xml
You should see:
HTTP/1.1 200 OKContent-Type: application/xml- Your XML content
If your provisioning URL is different, match whatever your phones use for provisioning. The goal is that the same base provisioning address can also serve
company_directory.xml.
4) Automate It via Cron
Edit the system crontab:
sudo nano /etc/crontab
Add:
0 * * * * asterisk /usr/local/sbin/xml_directory.sh
This rebuilds the directory every hour so it stays in sync with user changes.
5) Enable the Phonebook via EPM > Base File Edit (Yealink)
This is the key step that makes this scalable.
In FreePBX:
- Go to:
Admin → Endpoint Manager - Select:
Brands → Yealink - Open:
Base File Edit - Add these lines:
remote_phonebook.data.1.name = Phonebook remote_phonebook.data.1.url = __provisionAddress__/company_directory.xml features.remote_phonebook.enable = 1
- Save
- Rebuild configs
- Reboot or reprovision the phones
Why This Method Is Better
Instead of manually configuring each phone’s remote phonebook URL, using the Yealink Base File:
- Pushes the config to every Yealink model in EPM
- Uses the dynamic
__provisionAddress__token - Keeps your directory centralized and consistent
This is especially helpful when you manage multiple sites or VLANs, or when phones are re-provisioned often.
Troubleshooting Tips
Phone still says “Cannot download remote phonebook”
Check these quickly:
- XML formatting
- Yealink is picky.
- Your root must be:
<CompanyIPPhoneDirectory>
And entries must be:
<DirectoryEntry>
Provisioning address accessibility
- The phone must be able to reach the PBX’s provisioning URL.
HTTP vs HTTPS
- If you’re using HTTPS with a self-signed cert, test HTTP first.
Permissions
- Confirm:
-
ls -l /tftpboot/company_directory.xml0644is typically perfect.
Final Result
Once applied, your Yealink phones will:
- Auto-enable Remote Phonebook
- Pull:
__provisionAddress__/company_directory.xml- Display a live-updated company directory based on FreePBX users