Skip to content
View in the app

A better way to browse. Learn more.

AlphaGNU

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

CWP: Persistent PHP intl Fix and Safe Roundcube 1.5.15 → 1.6.20 LTS Upgrade

Featured Replies

I encountered a recurring PHP startup warning on CWP servers:

PHP Warning: PHP Startup: Unable to load dynamic library 'intl'

The affected CWP PHP binary was:

/usr/local/cwp/php71/bin/php

Despite the directory name, this was actually PHP 7.4.33:

PHP 7.4.33
PHP API: 20190902

The important diagnostic result was:

/usr/local/cwp/php71/bin/php -n -r \
    'var_dump(PHP_VERSION, extension_loaded("intl"), phpversion("intl"));'

Output:

string(6) "7.4.33"
bool(true)
string(6) "7.4.33"

This proved that intl was already compiled into the CWP PHP binary. However, CWP repeatedly recreated:

/usr/local/cwp/php71/php.d/intl.ini

with:

extension=intl

PHP then tried to load a separate intl.so, although no compatible dynamic module was required.

The source of the recurring configuration was:

/scripts/update_roundcube

The old updater also contained outdated PHP API paths, downloaded third-party intl.so binaries, installed separate ICU packages, and recreated intl.ini. Simply renaming or deleting intl.ini was therefore not permanent: a later CWP update could recreate it.

Why upgrade Roundcube 1.5.15?

Roundcube 1.5.15 was the final security release in the 1.5 branch. New security corrections are now released for the 1.6 LTS and 1.7 branches.

Roundcube 1.6.20 contains later security fixes that are not present in 1.5.15, including fixes related to:

  • IMAP command injection;

  • stored and zero-click XSS;

  • SSRF filter bypasses;

  • e-mail header injection;

  • remote-content blocking bypasses;

  • cross-user address-book access;

  • Sieve script injection;

  • denial-of-service conditions.

For existing CWP installations using the internal PHP 7.4 binary, Roundcube 1.6.20 is the appropriate target:

Roundcube 1.6.20 LTS
PHP requirement: PHP 7.3 or newer
CWP PHP tested: PHP 7.4.33

Roundcube 1.7 is not a drop-in replacement in this environment because it requires PHP 8.1 or newer and introduces additional web-root changes.

Idempotent fleet upgrade script

I created an idempotent migration script for CWP servers:

roundcube-cwp-1.5.15-to-1.6.20.sh

Its behavior is:

Current version

Result with --apply

Roundcube 1.5.15

Installs the protection and upgrades to 1.6.20

Roundcube 1.6.20

Verifies/reinstalls the protection and skips migration

Any other version

Stops without performing an uncertain migration

The script:

  • verifies the installed Roundcube version;

  • checks the CWP PHP version and required extensions;

  • verifies whether intl is compiled into PHP;

  • downloads the official Roundcube 1.6.20 complete package over HTTPS;

  • verifies the official SHA-256 checksum;

  • verifies the Roundcube PGP signature and full signing-key fingerprint;

  • creates a complete Roundcube filesystem backup;

  • creates a MariaDB/MySQL database backup;

  • executes the official Roundcube installto.sh;

  • ensures that both installto.sh and its child php bin/... commands use:

/usr/local/cwp/php71/bin/php
  • stops and restarts cwpsrv during the migration;

  • verifies the resulting Roundcube version;

  • checks PHP syntax;

  • preserves the previous /scripts/update_roundcube file for reference.

It does not modify the verified Roundcube source package with sed. Instead, it starts installto.sh explicitly with CWP PHP and provides a temporary php PATH shim for any child migration commands.

Preventing CWP from restoring the old updater

The script replaces:

/scripts/update_roundcube

with a harmless wrapper that logs the attempted invocation and exits successfully with status 0.

This avoids disrupting the normal CWP nightly process while preventing the obsolete updater from:

  • reinstalling an old Roundcube release;

  • downloading incompatible intl.so files;

  • reinstalling unnecessary ICU packages;

  • recreating the faulty intl.ini configuration.

A systemd path unit monitors:

/scripts/update_roundcube
/usr/local/cwp/php71/php.d/intl.ini

If a later CWP update overwrites either file, the guard:

  1. restores the safe /scripts/update_roundcube wrapper;

  2. removes the redundant dynamic intl loading when intl is proven to be compiled into the PHP binary.

A systemd timer also performs a fallback check every five minutes.

The solution intentionally does not use:

chattr +i

An immutable file could cause a CWP update to fail when it attempts to replace the file.

Usage

Copy the script to the server and make it executable:

chmod 750 roundcube-cwp-1.5.15-to-1.6.20.sh

Run the read-only preflight first:

./roundcube-cwp-1.5.15-to-1.6.20.sh

This performs checks without changing the server.

If the preflight succeeds:

./roundcube-cwp-1.5.15-to-1.6.20.sh --apply

If Roundcube 1.6.20 is already installed, the expected message is:

Roundcube 1.6.20 is already installed. Migration skipped; guards verified.

Verification

Check the systemd protection:

systemctl is-active \
    cwp-roundcube-guard.path \
    cwp-roundcube-guard.timer

Expected output:

active
active

Verify that the protected updater is identical to the safe copy:

cmp \
    /usr/local/libexec/cwp-roundcube-guard/update_roundcube.disabled \
    /scripts/update_roundcube

echo $?

Expected exit status:

0

Verify PHP and intl:

/usr/local/cwp/php71/bin/php -v

/usr/local/cwp/php71/bin/php -m \
    | grep -i '^intl$'

The PHP command should no longer emit an intl startup warning, and the module list should contain:

intl

Check all relevant services:

systemctl is-active \
    cwpsrv \
    cwp-roundcube-guard.path \
    cwp-roundcube-guard.timer

Backups and logs

Backups of replaced legacy scripts:

/root/roundcube-upgrade-backups/legacy-script-backups/

Full filesystem and database backups:

/root/roundcube-upgrade-backups/YYYYMMDD-HHMMSS/

Migration log:

/var/log/cwp-roundcube-1.6.20-upgrade.log

Guard activity:

journalctl \
    -u cwp-roundcube-guard.service \
    --since today \
    --no-pager

Functional tests after upgrading

I recommend testing:

  1. direct login through webmail.example.com;

  2. CWP administrator e-mail auto-login;

  3. opening plain-text and HTML messages;

  4. sending a test message;

  5. sending and downloading an attachment;

  6. address-book access;

  7. logout and a new login.

On the first upgraded production server, all of the following worked:

  • Roundcube 1.5.15 → 1.6.20 migration;

  • direct webmail access;

  • CWP administrator auto-login;

  • database migration;

  • PHP 7.4 compatibility;

  • sending and receiving mail;

  • removal of the recurring intl warning.

A temporary proxy error appeared during the first auto-login attempt immediately after the update, but it disappeared after the services and session state settled. It was not a persistent compatibility problem.

What if the server still has Roundcube 1.5.8?

Roundcube 1.5.8 is older and should not remain exposed as a production webmail installation.

The current fleet script intentionally refuses to modify unknown source versions. It accepts only:

1.5.15
1.6.20

This is a safety measure. Do not simply remove the version check on a production server.

For a server still running 1.5.8, use this process:

  1. Create a complete backup of:

    • the Roundcube installation directory;

    • the Roundcube database;

    • config/config.inc.php;

    • installed plugins and custom CWP integration files.

  2. Download the official Roundcube 1.5.15 complete release package.

  3. Verify its PGP signature before extracting it.

  4. Upgrade 1.5.8 to 1.5.15 with the official bin/installto.sh, ensuring that the CWP PHP binary is used:

/usr/local/cwp/php71/bin/php
  1. Verify:

    • direct webmail login;

    • CWP auto-login;

    • IMAP access;

    • SMTP sending;

    • attachments;

    • address book;

    • logout and login.

  2. Run the fleet script in preflight mode:

./roundcube-cwp-1.5.15-to-1.6.20.sh
  1. If the preflight succeeds, perform the protected 1.5.15 → 1.6.20 migration:

./roundcube-cwp-1.5.15-to-1.6.20.sh --apply

This two-stage approach is deliberately conservative. It provides a tested checkpoint at 1.5.15 and avoids turning an old, possibly customized CWP Roundcube installation into an unverified direct migration.

Do not use an old updater that downloads PHP modules or RPM packages from unrelated repositories. Roundcube source updates and PHP-extension repairs should remain separate operations, and downloaded release artifacts should always be authenticated before installation.

And here is the script, save as `roundcube-cwp-1.5.15-to-1.6.20.sh`

Controlled CWP Roundcube migration: 1.5.15 -> 1.6.20 LTS

#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

# Controlled CWP Roundcube migration: 1.5.15 -> 1.6.20 LTS
#
# This script deliberately does not install PHP extensions, ICU packages, or
# third-party binaries. Run without --apply for a read-only preflight.

readonly SOURCE_VERSION="1.5.15"
readonly TARGET_VERSION="1.6.20"
readonly TARGET_SHA256="46919c70c85609b57038903c4ecc4e1d7fa4e14c535fe5b14192d778dcd4a1ee"
readonly RELEASE_BASE="https://github.com/roundcube/roundcubemail/releases/download/${TARGET_VERSION}"
readonly ARCHIVE_NAME="roundcubemail-${TARGET_VERSION}-complete.tar.gz"
readonly SIGNATURE_NAME="${ARCHIVE_NAME}.asc"
readonly RELEASE_URL="${RELEASE_BASE}/${ARCHIVE_NAME}"
readonly SIGNATURE_URL="${RELEASE_BASE}/${SIGNATURE_NAME}"
readonly PUBKEY_URL="https://roundcube.net/download/pubkey.asc"
readonly EXPECTED_KEY_FINGERPRINT="F3E4C04BB3DB5D4215C45F7F5AB2BAA141C4F7D5"

readonly TARGET_DIR="/usr/local/cwpsrv/var/services/roundcube"
readonly PHP_BIN="/usr/local/cwp/php71/bin/php"
readonly RESTART_SCRIPT="/scripts/restart_cwpsrv"
readonly LOG_FILE="/var/log/cwp-roundcube-1.6.20-upgrade.log"
readonly BACKUP_ROOT="/root/roundcube-upgrade-backups"
readonly CONFIG_FILE="${TARGET_DIR}/config/config.inc.php"
readonly VERSION_FILE="${TARGET_DIR}/program/include/iniset.php"
readonly INTL_INI="/usr/local/cwp/php71/php.d/intl.ini"
readonly LEGACY_UPDATE_SCRIPT="/scripts/update_roundcube"
readonly GUARD_DIR="/usr/local/libexec/cwp-roundcube-guard"
readonly GUARD_SCRIPT="/usr/local/sbin/cwp-roundcube-guard"
readonly GUARD_SERVICE="/etc/systemd/system/cwp-roundcube-guard.service"
readonly GUARD_PATH="/etc/systemd/system/cwp-roundcube-guard.path"
readonly GUARD_TIMER="/etc/systemd/system/cwp-roundcube-guard.timer"

APPLY=0
KEEP_WORKDIR=0
WORK_DIR=""
BACKUP_DIR=""
CWPSRV_WAS_ACTIVE=0

usage() {
    cat <<'EOF'
Usage:
  roundcube-cwp-1.5.15-to-1.6.20.sh
  roundcube-cwp-1.5.15-to-1.6.20.sh --apply
  roundcube-cwp-1.5.15-to-1.6.20.sh --apply --keep-workdir

Without --apply, only preflight checks are performed.
EOF
}

log() {
    printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S%z')" "$*" | tee -a "$LOG_FILE"
}

die() {
    log "ERROR: $*"
    exit 1
}

cleanup() {
    local exit_code=$?

    if (( CWPSRV_WAS_ACTIVE == 1 )) && ! systemctl is-active --quiet cwpsrv; then
        log "Starting cwpsrv after interrupted or failed migration."
        "$RESTART_SCRIPT" >>"$LOG_FILE" 2>&1 || true
    fi

    if [[ -n "$WORK_DIR" && -d "$WORK_DIR" && $KEEP_WORKDIR -eq 0 ]]; then
        rm -rf -- "$WORK_DIR"
    elif [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
        log "Working directory retained: $WORK_DIR"
    fi

    exit "$exit_code"
}

install_cwp_guards() {
    local legacy_backup_dir

    legacy_backup_dir="${BACKUP_ROOT}/legacy-script-backups"
    mkdir -p "$GUARD_DIR" "$legacy_backup_dir"

    if [[ -f "$LEGACY_UPDATE_SCRIPT" ]] && \
       ! grep -q 'Managed by cwp-roundcube-guard' "$LEGACY_UPDATE_SCRIPT"; then
        cp -a "$LEGACY_UPDATE_SCRIPT" \
            "${legacy_backup_dir}/update_roundcube.$(date '+%Y%m%d-%H%M%S')"
    fi

    cat >"${GUARD_DIR}/update_roundcube.disabled" <<'EOF'
#!/bin/sh
# Managed by cwp-roundcube-guard. The legacy CWP updater is intentionally disabled.
logger -t cwp-roundcube-guard \
    "Blocked obsolete /scripts/update_roundcube invocation; managed Roundcube updater is required."
exit 0
EOF
    chmod 0755 "${GUARD_DIR}/update_roundcube.disabled"

    cat >"$GUARD_SCRIPT" <<'EOF'
#!/usr/bin/env bash

set -Eeuo pipefail

readonly PHP_BIN="/usr/local/cwp/php71/bin/php"
readonly INTL_INI="/usr/local/cwp/php71/php.d/intl.ini"
readonly LEGACY_SCRIPT="/scripts/update_roundcube"
readonly SAFE_SCRIPT="/usr/local/libexec/cwp-roundcube-guard/update_roundcube.disabled"
readonly LOG_TAG="cwp-roundcube-guard"

changed=0

if [[ -f "$SAFE_SCRIPT" ]] && ! cmp --silent "$SAFE_SCRIPT" "$LEGACY_SCRIPT" 2>/dev/null; then
    install -o root -g root -m 0755 "$SAFE_SCRIPT" "$LEGACY_SCRIPT"
    logger -t "$LOG_TAG" "Restored the safe /scripts/update_roundcube wrapper."
    changed=1
fi

if [[ -x "$PHP_BIN" ]] && \
   "$PHP_BIN" -n -r 'exit(extension_loaded("intl") ? 0 : 1);'; then
    if [[ -f "$INTL_INI" ]] && \
       grep -Eq '^[[:space:]]*extension[[:space:]]*=[[:space:]]*"?intl(\.so)?"?[[:space:]]*$' "$INTL_INI"; then
        printf '%s\n' \
            '; Disabled automatically: intl is compiled into the CWP PHP binary.' \
            >"$INTL_INI"
        chmod 0644 "$INTL_INI"
        logger -t "$LOG_TAG" "Disabled redundant dynamic intl loading."
        changed=1
    fi
fi

exit 0
EOF
    chmod 0750 "$GUARD_SCRIPT"

    cat >"$GUARD_SERVICE" <<EOF
[Unit]
Description=Protect managed CWP Roundcube and PHP intl configuration
After=local-fs.target

[Service]
Type=oneshot
ExecStart=$GUARD_SCRIPT
EOF

    cat >"$GUARD_PATH" <<EOF
[Unit]
Description=Watch CWP Roundcube updater and PHP intl configuration

[Path]
PathChanged=$LEGACY_UPDATE_SCRIPT
PathChanged=$INTL_INI
Unit=cwp-roundcube-guard.service

[Install]
WantedBy=multi-user.target
EOF

    cat >"$GUARD_TIMER" <<'EOF'
[Unit]
Description=Periodic fallback check for CWP Roundcube guard

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true
Unit=cwp-roundcube-guard.service

[Install]
WantedBy=timers.target
EOF

    systemctl daemon-reload
    systemctl enable --now cwp-roundcube-guard.path cwp-roundcube-guard.timer \
        >>"$LOG_FILE" 2>&1
    systemctl start cwp-roundcube-guard.service

    cmp --silent "${GUARD_DIR}/update_roundcube.disabled" "$LEGACY_UPDATE_SCRIPT" || \
        die "Failed to replace the legacy Roundcube updater."
    systemctl is-active --quiet cwp-roundcube-guard.path || \
        die "Roundcube guard path unit is not active."
    systemctl is-active --quiet cwp-roundcube-guard.timer || \
        die "Roundcube guard timer is not active."

    log "Legacy Roundcube updater disabled; persistent CWP/intl guard installed."
}

trap cleanup EXIT
trap 'die "Failure at line $LINENO."' ERR

for argument in "$@"; do
    case "$argument" in
        --apply) APPLY=1 ;;
        --keep-workdir) KEEP_WORKDIR=1 ;;
        -h|--help) usage; exit 0 ;;
        *) usage; die "Unknown argument: $argument" ;;
    esac
done

[[ $EUID -eq 0 ]] || die "This script must be run as root."

for command_name in base64 curl gpg gzip sha256sum tar awk grep sed stat systemctl; do
    command -v "$command_name" >/dev/null 2>&1 || die "Required command not found: $command_name"
done

[[ -x "$PHP_BIN" ]] || die "CWP PHP binary not found: $PHP_BIN"
[[ -d "$TARGET_DIR" ]] || die "Roundcube directory not found: $TARGET_DIR"
[[ -f "$CONFIG_FILE" ]] || die "Roundcube configuration not found: $CONFIG_FILE"
[[ -f "$VERSION_FILE" ]] || die "Roundcube version file not found: $VERSION_FILE"
[[ -x "$RESTART_SCRIPT" ]] || die "CWP restart script not found: $RESTART_SCRIPT"

INSTALLED_VERSION="$({
    grep "define('RCMAIL_VERSION'" "$VERSION_FILE" || true
} | awk -F"'" 'NR == 1 {print $4}')"

if [[ "$INSTALLED_VERSION" != "$SOURCE_VERSION" && \
      "$INSTALLED_VERSION" != "$TARGET_VERSION" ]]; then
    die "Expected Roundcube ${SOURCE_VERSION} or ${TARGET_VERSION}, found '${INSTALLED_VERSION:-unknown}'."
fi

PHP_VERSION="$($PHP_BIN -n -r 'echo PHP_VERSION;')"
PHP_VERSION_ID="$($PHP_BIN -n -r 'echo PHP_VERSION_ID;')"
(( PHP_VERSION_ID >= 70300 )) || die "Roundcube 1.6 requires PHP 7.3 or newer; found $PHP_VERSION."

if "$PHP_BIN" -n -r 'exit(extension_loaded("intl") ? 0 : 1);'; then
    INTL_MODE="compiled-in"
else
    INTL_MODE="configured extension"
fi

for extension_name in dom fileinfo intl json mbstring openssl pcre pdo session sockets; do
    "$PHP_BIN" -r 'exit(extension_loaded($argv[1]) ? 0 : 1);' "$extension_name" 2>/dev/null || \
        die "Required PHP extension is not loaded: $extension_name"
done

log "Preflight: Roundcube $INSTALLED_VERSION, PHP $PHP_VERSION, intl mode: $INTL_MODE."

if [[ "$INTL_MODE" == "compiled-in" && -f "$INTL_INI" ]] && \
   grep -Eq '^[[:space:]]*extension[[:space:]]*=[[:space:]]*"?intl(\.so)?"?[[:space:]]*$' "$INTL_INI"; then
    if (( APPLY == 1 )); then
        cp -a "$INTL_INI" "${INTL_INI}.before-roundcube-1.6.20"
        printf '%s\n' '; Disabled: intl is compiled into the CWP PHP binary.' >"$INTL_INI"
        log "Disabled redundant dynamic intl loading."
    else
        log "Preflight warning: $INTL_INI contains redundant dynamic intl loading."
    fi
fi

if (( APPLY == 0 )); then
    if [[ "$INSTALLED_VERSION" == "$TARGET_VERSION" ]]; then
        log "Roundcube ${TARGET_VERSION} is already installed; migration would be skipped."
    fi
    log "Preflight completed. No changes were made. Re-run with --apply to install guards and migrate if needed."
    exit 0
fi

mkdir -p "$BACKUP_ROOT"
install_cwp_guards

if [[ "$INSTALLED_VERSION" == "$TARGET_VERSION" ]]; then
    log "Roundcube ${TARGET_VERSION} is already installed. Migration skipped; guards verified."
    exit 0
fi

WORK_DIR="$(mktemp -d /usr/local/src/roundcube-1.6.20-upgrade.XXXXXX)"
readonly WORK_DIR
BACKUP_DIR="${BACKUP_ROOT}/$(date '+%Y%m%d-%H%M%S')"
readonly BACKUP_DIR
mkdir -p "$BACKUP_DIR"

log "Downloading the official Roundcube ${TARGET_VERSION} complete package."
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
    "$RELEASE_URL" --output "${WORK_DIR}/${ARCHIVE_NAME}"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
    "$SIGNATURE_URL" --output "${WORK_DIR}/${SIGNATURE_NAME}"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
    "$PUBKEY_URL" --output "${WORK_DIR}/roundcube-pubkey.asc"

printf '%s  %s\n' "$TARGET_SHA256" "${WORK_DIR}/${ARCHIVE_NAME}" | sha256sum --check --status || \
    die "SHA-256 verification failed."

GNUPGHOME="${WORK_DIR}/gnupg"
export GNUPGHOME
mkdir -m 700 "$GNUPGHOME"

ACTUAL_FINGERPRINT="$(
    gpg --batch --quiet --with-colons --import-options show-only --import \
        "${WORK_DIR}/roundcube-pubkey.asc" 2>/dev/null |
        awk -F: '$1 == "fpr" {print $10; exit}'
)"

[[ "$ACTUAL_FINGERPRINT" == "$EXPECTED_KEY_FINGERPRINT" ]] || \
    die "Unexpected Roundcube signing-key fingerprint: $ACTUAL_FINGERPRINT"

gpg --batch --quiet --import "${WORK_DIR}/roundcube-pubkey.asc" >>"$LOG_FILE" 2>&1
gpg --batch --verify "${WORK_DIR}/${SIGNATURE_NAME}" \
    "${WORK_DIR}/${ARCHIVE_NAME}" >>"$LOG_FILE" 2>&1 || die "PGP signature verification failed."

log "Release checksum and PGP signature verified."

log "Creating filesystem backup: $BACKUP_DIR"
tar --xattrs --acls --selinux -czpf "${BACKUP_DIR}/roundcube-files.tar.gz" \
    -C "$(dirname "$TARGET_DIR")" "$(basename "$TARGET_DIR")"
cp -a "$0" "${BACKUP_DIR}/migration-script.sh"
cp -a "$CONFIG_FILE" "${BACKUP_DIR}/config.inc.php"

# Try to create a database backup without printing credentials. Roundcube DSNs
# normally use mysql:// or mysqli://. Unsupported/custom DSNs abort the upgrade.
DB_ENV_FILE="${WORK_DIR}/read-db-config.php"
cat >"$DB_ENV_FILE" <<'PHP'
<?php
$config = [];
require $argv[1];
$dsn = $config['db_dsnw'] ?? '';
$parts = parse_url($dsn);
if (!is_array($parts) || !isset($parts['scheme'], $parts['host'], $parts['user'], $parts['path'])) {
    exit(2);
}
$scheme = strtolower($parts['scheme']);
if (!in_array($scheme, ['mysql', 'mysqli'], true)) {
    exit(3);
}
$values = [
    rawurldecode($parts['host']),
    (string) ($parts['port'] ?? 3306),
    rawurldecode($parts['user']),
    rawurldecode((string) ($parts['pass'] ?? '')),
    rawurldecode(ltrim($parts['path'], '/')),
];
foreach ($values as $value) {
    echo base64_encode($value), "\n";
}
PHP

mapfile -t DB_FIELDS < <("$PHP_BIN" -n "$DB_ENV_FILE" "$CONFIG_FILE") || \
    die "Could not parse a supported MySQL/MariaDB db_dsnw from Roundcube configuration."
[[ ${#DB_FIELDS[@]} -eq 5 ]] || die "Incomplete Roundcube database configuration."

decode_field() { printf '%s' "$1" | base64 --decode; }
DB_HOST="$(decode_field "${DB_FIELDS[0]}")"
DB_PORT="$(decode_field "${DB_FIELDS[1]}")"
DB_USER="$(decode_field "${DB_FIELDS[2]}")"
DB_PASS="$(decode_field "${DB_FIELDS[3]}")"
DB_NAME="$(decode_field "${DB_FIELDS[4]}")"

DUMP_BIN="$(command -v mariadb-dump || command -v mysqldump || true)"
[[ -n "$DUMP_BIN" ]] || die "Neither mariadb-dump nor mysqldump is installed."

DB_DEFAULTS="${WORK_DIR}/db-client.cnf"
{
    printf '%s\n' '[client]'
    printf 'host=%s\n' "$DB_HOST"
    printf 'port=%s\n' "$DB_PORT"
    printf 'user=%s\n' "$DB_USER"
    printf 'password=%s\n' "$DB_PASS"
} >"$DB_DEFAULTS"
chmod 600 "$DB_DEFAULTS"

"$DUMP_BIN" --defaults-extra-file="$DB_DEFAULTS" --single-transaction \
    --skip-lock-tables --hex-blob --databases "$DB_NAME" |
    gzip -9 >"${BACKUP_DIR}/roundcube-database.sql.gz"
[[ -s "${BACKUP_DIR}/roundcube-database.sql.gz" ]] || die "Database backup is empty."

unset DB_PASS DB_FIELDS
rm -f "$DB_DEFAULTS"
log "Filesystem and database backups completed."

mkdir -p "${WORK_DIR}/source"
tar -xzf "${WORK_DIR}/${ARCHIVE_NAME}" -C "${WORK_DIR}/source" --strip-components=1
[[ -f "${WORK_DIR}/source/bin/installto.sh" ]] || die "installto.sh missing from verified archive."

# installto.sh is started explicitly with CWP PHP below. It can also invoke
# child commands in the form `php bin/...`; provide a temporary PATH shim so
# those processes use the same CWP PHP binary without modifying signed source.
mkdir -p "${WORK_DIR}/cwp-php-bin"
ln -s "$PHP_BIN" "${WORK_DIR}/cwp-php-bin/php"

if systemctl is-active --quiet cwpsrv; then
    CWPSRV_WAS_ACTIVE=1
    log "Stopping cwpsrv for the migration."
    systemctl stop cwpsrv
fi

log "Migrating Roundcube files and database."
PATH="${WORK_DIR}/cwp-php-bin:${PATH}" \
    "$PHP_BIN" "${WORK_DIR}/source/bin/installto.sh" -y "$TARGET_DIR" \
    >>"$LOG_FILE" 2>&1

NEW_VERSION="$({
    grep "define('RCMAIL_VERSION'" "$VERSION_FILE" || true
} | awk -F"'" 'NR == 1 {print $4}')"
[[ "$NEW_VERSION" == "$TARGET_VERSION" ]] || die "Migration finished with unexpected version: $NEW_VERSION"

"$PHP_BIN" -l "$TARGET_DIR/index.php" >>"$LOG_FILE" 2>&1
"$PHP_BIN" -l "$CONFIG_FILE" >>"$LOG_FILE" 2>&1

log "Restarting cwpsrv."
"$RESTART_SCRIPT" >>"$LOG_FILE" 2>&1
systemctl is-active --quiet cwpsrv || die "cwpsrv did not become active."
CWPSRV_WAS_ACTIVE=0

log "Roundcube ${SOURCE_VERSION} -> ${TARGET_VERSION} migration completed."
log "Backup directory: $BACKUP_DIR"
log "Verify webmail login, message display, sending, attachments, address book, and enabled plugins."

  • Author

Additional procedure for servers still running Roundcube 1.5.8

The fleet migration script intentionally accepts only Roundcube 1.5.15 and 1.6.20. If it detects 1.5.8, it stops without changing the installation.

For an older 1.5.8 installation, use a controlled two-stage upgrade:

1.5.8 → 1.5.15 → 1.6.20

This provides a tested intermediate checkpoint and avoids bypassing the script’s version-safety gate.

1. Confirm the installed version

grep "define('RCMAIL_VERSION'" \
    /usr/local/cwpsrv/var/services/roundcube/program/include/iniset.php

Expected result for an affected server:

define('RCMAIL_VERSION', '1.5.8');

2. Create backups

Do not proceed without both a filesystem and database backup.

Create a filesystem backup:

BACKUP_DIR="/root/roundcube-1.5.8-backup-$(date +%Y%m%d-%H%M%S)"

mkdir -p "$BACKUP_DIR"

tar --xattrs --acls --selinux -czpf \
    "$BACKUP_DIR/roundcube-files.tar.gz" \
    -C /usr/local/cwpsrv/var/services \
    roundcube

Also create a database dump using the Roundcube database credentials stored in:

/usr/local/cwpsrv/var/services/roundcube/config/config.inc.php

Verify that both backup files exist and are non-empty before continuing.

3. Download the official Roundcube 1.5.15 package

Use a temporary working directory:

WORK_DIR="$(mktemp -d /usr/local/src/roundcube-1.5.15.XXXXXX)"

cd "$WORK_DIR"

Download the official complete package and its signature:

curl --fail --location --proto '=https' --tlsv1.2 \
    --output roundcubemail-1.5.15-complete.tar.gz \
    https://github.com/roundcube/roundcubemail/releases/download/1.5.15/roundcubemail-1.5.15-complete.tar.gz

curl --fail --location --proto '=https' --tlsv1.2 \
    --output roundcubemail-1.5.15-complete.tar.gz.asc \
    https://github.com/roundcube/roundcubemail/releases/download/1.5.15/roundcubemail-1.5.15-complete.tar.gz.asc

curl --fail --location --proto '=https' --tlsv1.2 \
    --output roundcube-pubkey.asc \
    https://roundcube.net/download/pubkey.asc

4. Verify the signing key and package

Use an isolated GPG directory:

export GNUPGHOME="$WORK_DIR/gnupg"

mkdir -m 700 "$GNUPGHOME"

Display the downloaded key fingerprint:

gpg --batch \
    --with-fingerprint \
    --keyid-format long \
    --import-options show-only \
    --import roundcube-pubkey.asc

The expected primary key fingerprint is:

F3E4 C04B B3DB 5D42 15C4 5F7F 5AB2 BAA1 41C4 F7D5

If the fingerprint differs, stop immediately.

Import the verified key and check the release signature:

gpg --batch --import roundcube-pubkey.asc

gpg --batch --verify \
    roundcubemail-1.5.15-complete.tar.gz.asc \
    roundcubemail-1.5.15-complete.tar.gz

Proceed only after a valid signature result.

5. Extract Roundcube 1.5.15

mkdir "$WORK_DIR/source"

tar -xzf roundcubemail-1.5.15-complete.tar.gz \
    -C "$WORK_DIR/source" \
    --strip-components=1

Confirm that the official installer exists:

test -f "$WORK_DIR/source/bin/installto.sh"

6. Ensure all migration commands use CWP PHP

The CWP PHP binary is:

/usr/local/cwp/php71/bin/php

Despite the directory name, verify its real version:

/usr/local/cwp/php71/bin/php -v

Create a temporary php PATH shim:

mkdir "$WORK_DIR/cwp-php-bin"

ln -s /usr/local/cwp/php71/bin/php \
    "$WORK_DIR/cwp-php-bin/php"

This is important because installto.sh may start additional commands using:

php bin/...

The shim ensures that these child commands also use the CWP PHP binary instead of the system default PHP.

7. Perform the 1.5.8 → 1.5.15 upgrade

Stop the CWP web service during the file and database migration:

systemctl stop cwpsrv

Run the official installer:

PATH="$WORK_DIR/cwp-php-bin:$PATH" \
    /usr/local/cwp/php71/bin/php \
    "$WORK_DIR/source/bin/installto.sh" \
    -y \
    /usr/local/cwpsrv/var/services/roundcube

Restart CWP:

/scripts/restart_cwpsrv

Confirm the service state:

systemctl is-active cwpsrv

Expected result:

active

8. Verify the intermediate version

grep "define('RCMAIL_VERSION'" \
    /usr/local/cwpsrv/var/services/roundcube/program/include/iniset.php

Expected result:

define('RCMAIL_VERSION', '1.5.15');

Test at least:

  1. direct webmail login;

  2. CWP administrator auto-login;

  3. opening an existing message;

  4. sending a test message;

  5. attachment upload and download;

  6. address-book access;

  7. logout and a new login.

Do not continue to 1.6.20 until the 1.5.15 installation works correctly.

9. Run the protected 1.5.15 → 1.6.20 migration

First run the fleet script without modifications:

./roundcube-cwp-1.5.15-to-1.6.20.sh

If the preflight succeeds:

./roundcube-cwp-1.5.15-to-1.6.20.sh --apply

The second migration installs Roundcube 1.6.20 and also:

  • disables the obsolete /scripts/update_roundcube;

  • protects it against later CWP overwrites;

  • removes the recurring redundant extension=intl configuration;

  • installs the systemd path and timer guards;

  • creates another complete filesystem and database backup.

10. Final verification

grep "define('RCMAIL_VERSION'" \
    /usr/local/cwpsrv/var/services/roundcube/program/include/iniset.php

/usr/local/cwp/php71/bin/php -v

/usr/local/cwp/php71/bin/php -m \
    | grep -i '^intl$'

systemctl is-active \
    cwpsrv \
    cwp-roundcube-guard.path \
    cwp-roundcube-guard.timer

Expected Roundcube version:

1.6.20

Expected service results:

active
active
active

Once the final functional tests have passed, remove the temporary 1.5.15 working directory:

rm -rf -- "$WORK_DIR"

Keep both sets of backups until the upgraded server has completed at least one normal operational and nightly CWP update cycle.

Create an account or sign in to comment

Topics

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.