diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..e1d2d11f39 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,99 @@ +FROM php:8.4-cli-bookworm + +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Build tools required for PHP extension configuration + autoconf \ + # PHP extension dependencies + libfreetype6-dev \ + libicu-dev \ + libjpeg62-turbo-dev \ + libldap2-dev \ + libpng-dev \ + libpq-dev \ + libsqlite3-dev \ + libzip-dev \ + libonig-dev \ + libldap-common \ + libenchant-2-dev \ + default-libmysqlclient-dev \ + # ImageMagick for Imagick extension + libmagickwand-dev \ + # Tools + nodejs \ + npm \ + aspell \ + aspell-en \ + hunspell-en-us \ + git \ + curl \ + unzip \ + locales \ + sudo \ + # For health checks and debugging + netcat-openbsd \ + gnupg \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install MariaDB client from Debian repositories +RUN apt-get update \ + && apt-get install -y --no-install-recommends mariadb-client \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set up locales +RUN sed -i 's/^# en_US.UTF-8 /en_US.UTF-8 /' /etc/locale.gen && locale-gen + +# Install PHP extensions +# Note: php-imap is NOT required - Roundcube has its own IMAP implementation using sockets +# Note: pspell is optional - Roundcube can use enchant or web-based spellcheck (googie) instead +RUN docker-php-ext-configure gd --with-jpeg --with-freetype \ + && docker-php-ext-configure ldap \ + && docker-php-ext-install \ + zip \ + pcntl \ + gd \ + ldap \ + intl \ + enchant \ + pdo_mysql \ + pdo_pgsql \ + pdo_sqlite \ + mysqli \ + opcache \ + exif + +# Install Imagick via PECL (for image manipulation) +RUN pecl install imagick && docker-php-ext-enable imagick + +# Install Xdebug for debugging +RUN pecl install xdebug && docker-php-ext-enable xdebug + +# Configure Xdebug +RUN echo "xdebug.mode=debug,coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.start_with_request=trigger" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + +# Configure PHP for development (increase memory limit for tools like PHPStan) +RUN echo "memory_limit=512M" >> /usr/local/etc/php/conf.d/dev-settings.ini + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Create non-root user +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Set working directory +WORKDIR /workspace + +USER $USERNAME diff --git a/.devcontainer/Dockerfile.dovecot b/.devcontainer/Dockerfile.dovecot new file mode 100644 index 0000000000..a682a604d9 --- /dev/null +++ b/.devcontainer/Dockerfile.dovecot @@ -0,0 +1,42 @@ +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + dovecot-core \ + dovecot-imapd \ + dovecot-lmtpd \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create test user (password: testpass) +RUN useradd -m -s /bin/bash testuser \ + && echo "testuser:testpass" | chpasswd + +# Create mail directories +RUN mkdir -p /var/mail/testuser \ + && chown -R testuser:testuser /var/mail/testuser + +# Configure Dovecot (use 99-* to override Debian defaults) +RUN echo 'protocols = imap lmtp' > /etc/dovecot/conf.d/99-custom.conf \ + && echo 'mail_location = maildir:/var/mail/%u' >> /etc/dovecot/conf.d/99-custom.conf \ + && echo 'disable_plaintext_auth = no' >> /etc/dovecot/conf.d/99-custom.conf \ + && echo 'auth_mechanisms = plain login' >> /etc/dovecot/conf.d/99-custom.conf \ + && echo 'ssl = no' >> /etc/dovecot/conf.d/99-custom.conf \ + && echo 'listen = *' >> /etc/dovecot/conf.d/99-custom.conf \ + && printf 'service lmtp {\n inet_listener lmtp {\n port = 24\n }\n}\n' >> /etc/dovecot/conf.d/99-custom.conf + +# Create entrypoint script to initialize maildir at runtime (after volume mount) +# In maildir format, the root directory IS the INBOX (no .INBOX subfolder) +RUN printf '#!/bin/bash\n\ +if [ ! -d /var/mail/testuser/cur ]; then\n\ + mkdir -p /var/mail/testuser/{cur,new,tmp}\n\ + chown -R testuser:testuser /var/mail/testuser\n\ +fi\n\ +exec "$@"\n' > /entrypoint.sh \ + && chmod +x /entrypoint.sh + +EXPOSE 143 24 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["dovecot", "-F"] diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000000..a163baebfb --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,345 @@ +# Roundcube Development Container + +This guide explains how to set up and use the Roundcube development environment using VS Code Dev Containers. + +## System Requirements + +### All Platforms + +- **Docker Desktop** (or Docker Engine + Docker Compose on Linux) + - Windows/macOS: [Download Docker Desktop](https://www.docker.com/products/docker-desktop/) + - Linux: Install Docker Engine and Docker Compose plugin +- **Visual Studio Code** with the **Dev Containers extension** + - Extension ID: `ms-vscode-remote.remote-containers` +- **Git** for cloning the repository +- **4GB+ RAM** available for Docker (8GB recommended) +- **10GB+ free disk space** for images and volumes + +### Windows-Specific + +- **Windows 10/11** with WSL 2 enabled +- Docker Desktop configured to use WSL 2 backend (recommended for performance) +- For best performance, clone the repository inside WSL 2 filesystem (`/home/...`) rather than Windows filesystem (`/mnt/c/...`) + +### macOS-Specific + +- **macOS 10.15 (Catalina)** or later +- Apple Silicon (M1/M2/M3) and Intel Macs both supported +- Docker Desktop automatically handles architecture differences + +### Linux-Specific + +- Docker Engine 20.10+ with Docker Compose v2 +- User must be in the `docker` group: `sudo usermod -aG docker $USER` + +## Getting Started + +### 1. Clone the Repository + +```bash +git clone https://github.com/roundcube/roundcubemail.git +cd roundcubemail +``` + +### 2. Open in VS Code + +**Option A: From Command Line** +```bash +code . +``` + +**Option B: From VS Code** +1. Open VS Code +2. File → Open Folder → Select the `roundcubemail` directory + +### 3. Reopen in Container + +When you open the folder, VS Code should detect the `.devcontainer` configuration and prompt: + +> "Folder contains a Dev Container configuration file. Reopen folder to develop in a container?" + +Click **"Reopen in Container"**. + +**If the prompt doesn't appear:** +1. Press `F1` (or `Cmd+Shift+P` / `Ctrl+Shift+P`) +2. Type "Dev Containers: Reopen in Container" +3. Press Enter + +### 4. Wait for Setup + +The first time you open the container: +1. Docker builds the PHP development image (may take several minutes) +2. Docker pulls MariaDB, Dovecot, and Mailhog images +3. The `post-create.sh` script runs automatically to: + - Wait for MariaDB to be ready + - Initialize the database schema + - Install Composer dependencies + - Install npm dependencies + - Download JavaScript libraries (jQuery, TinyMCE, CodeMirror) + - Create the development configuration file + +Subsequent opens are much faster as images and volumes are cached. + +### 5. Start the Development Server + +Press `Ctrl+Shift+B` (or `Cmd+Shift+B` on macOS) to run the default build task. + +You'll be prompted for a port number (default: 8081). VS Code will automatically forward the port and show a notification with a link to open the webmail interface. + +**Test Credentials:** +- Username: `testuser` +- Password: `testpass` + +## Services and Ports + +| Service | Internal Host | Forwarded Port | Description | +|------------|---------------|----------------|--------------------------------| +| Roundcube | localhost | (user-chosen) | PHP development server | +| MariaDB | db:3306 | 3306 | Database server | +| IMAP | mail:143 | 143 | Dovecot IMAP server | +| Mailhog UI | mailhog:8025 | 8025 | Email testing interface | +| SMTP | mailhog:1025 | - | Mailhog SMTP (internal only) | + +**Mailhog** captures all outgoing emails for testing. Access the web UI at http://localhost:8025 to view sent messages. + +## VS Code Tasks + +Access tasks via `Terminal → Run Task...` or `Ctrl+Shift+P` → "Tasks: Run Task" + +| Task | Description | +|-------------------------------|--------------------------------------------------| +| **Start Roundcube Dev Server** | Start PHP built-in server (default build task) | +| **Run PHPUnit Tests** | Run the full test suite | +| **PHP CS Fixer (check)** | Check code style without making changes | +| **PHP CS Fixer (fix)** | Automatically fix code style issues | +| **PHPStan Analyse** | Run static analysis | +| **ESLint Check** | Check JavaScript code style | +| **Initialize Database** | Smart initialization (detects current state) | +| **Reset Database** | Drop and recreate database (destroys all data) | +| **Check Database Status** | Show database state without making changes | +| **Install Suggested Packages**| Install optional packages (zxcvbn-php, net_ldap3)| + +## Command Line Scripts + +These commands are available in the container terminal: + +### Database Management + +```bash +# Check database state (initialized, partial, or empty) +.devcontainer/post-create.sh --db-status + +# Initialize database if needed (smart detection) +.devcontainer/post-create.sh --db-only + +# Force reset database (drops all tables, reinitializes) +.devcontainer/post-create.sh --db-reset + +# Show help +.devcontainer/post-create.sh --help +``` + +### Running Tests + +```bash +# Run all unit tests +vendor/bin/phpunit -c tests/phpunit.xml --fail-on-warning + +# Run a specific test file +vendor/bin/phpunit -c tests/phpunit.xml tests/Framework/SomeTest.php + +# Run tests matching a pattern +vendor/bin/phpunit -c tests/phpunit.xml --filter "TestClassName" +``` + +### Code Quality + +```bash +# PHP code style - check only +vendor/bin/php-cs-fixer fix --dry-run --diff + +# PHP code style - apply fixes +vendor/bin/php-cs-fixer fix + +# PHP static analysis +vendor/bin/phpstan analyse -v + +# JavaScript linting +npx eslint --ext .js . + +# JavaScript linting with auto-fix +npx eslint --fix . +``` + +### Dependency Management + +```bash +# Update PHP dependencies +composer install --prefer-dist + +# Update JavaScript dependencies +npm install --include=dev --omit=optional + +# Reinstall JavaScript libraries (jQuery, TinyMCE, etc.) +./bin/install-jsdeps.sh +``` + +### Database Access + +```bash +# Connect to MariaDB +mariadb -h db -u roundcube -proundcube roundcube + +# Run a SQL file +mariadb -h db -u roundcube -proundcube roundcube < path/to/file.sql +``` + +## Troubleshooting + +### Container Won't Start + +1. Ensure Docker Desktop is running +2. Check Docker has enough resources (Settings → Resources) +3. Try rebuilding: `F1` → "Dev Containers: Rebuild Container" + +### Database Connection Issues + +If MariaDB isn't ready when the setup script runs: + +```bash +# Check database status +.devcontainer/post-create.sh --db-status + +# Manually initialize if needed +.devcontainer/post-create.sh --db-only +``` + +### "Database is in partial/inconsistent state" + +This can happen if initialization was interrupted. Reset the database: + +```bash +.devcontainer/post-create.sh --db-reset +``` + +### Port Already in Use + +If port 8081 (or your chosen port) is in use: +- Choose a different port when prompted +- Or stop the process using that port + +### Slow Performance on Windows + +Clone the repository inside WSL 2 for better I/O performance: + +```bash +# In WSL terminal +cd ~ +git clone https://github.com/roundcube/roundcubemail.git +code roundcubemail +``` + +### Extensions Not Working + +If PHP extensions (like Intelephense) aren't working: +1. Wait for container to fully initialize +2. Try reloading the window: `F1` → "Developer: Reload Window" + +## Container Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Network: roundcube-dev │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ app │ │ db │ │ mail │ │ +│ │ (PHP 8.4) │ │ (MariaDB 11)│ │ (Dovecot) │ │ +│ │ │ │ │ │ │ │ +│ │ Composer │ │ roundcube │ │ testuser:testpass │ │ +│ │ Node.js │ │ database │ │ IMAP on :143 │ │ +│ │ Xdebug │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +│ │ │ +│ │ ┌─────────────────────┐ │ +│ │ │ mailhog │ │ +│ └────────▶│ SMTP on :1025 │ │ +│ │ Web UI on :8025 │ │ +│ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## PHP Extensions + +The container includes all required and recommended PHP extensions for Roundcube: + +### Required Extensions +| Extension | Purpose | +|-----------|--------------------------------------------| +| gd | Image processing | +| intl | Internationalization support | +| ldap | LDAP directory access | +| pdo_mysql | MySQL/MariaDB database driver | +| pdo_pgsql | PostgreSQL database driver | +| pdo_sqlite| SQLite database driver | +| zip | ZIP archive handling | +| opcache | PHP bytecode caching | + +### Optional Extensions (Performance Recommended) +| Extension | Purpose | +|-----------|--------------------------------------------| +| exif | EXIF metadata reading from images | +| imagick | Advanced image manipulation (ImageMagick) | +| enchant | Spell checking support | +| xdebug | Debugging and code coverage | + +### Suggested Packages (Optional) +| Library | Source | Purpose | +|---------------|-----------------------------|---------------------------------| +| Net_LDAP3 | kolab/net_ldap3 | Enhanced LDAP support | +| zxcvbn-php | bjeavons/zxcvbn-php | Password strength checking | + +These packages are optional. Install them via the **Install Suggested Packages** VS Code task or manually with: +```bash +composer require --dev bjeavons/zxcvbn-php kolab/net_ldap3 +``` + +## VS Code Extensions + +The following extensions are automatically installed in the container: + +| Extension | Purpose | +|-------------------------------|----------------------------------| +| Intelephense | PHP intelligence | +| PHP Debug | Xdebug integration | +| PHP CS Fixer | Code formatting | +| ESLint | JavaScript linting | +| Prettier | Code formatting | + +## Configuration Files + +| File | Purpose | +|-------------------------------|----------------------------------| +| `.devcontainer/devcontainer.json` | VS Code Dev Container config | +| `.devcontainer/docker-compose.yml` | Service definitions | +| `.devcontainer/Dockerfile` | PHP development image | +| `.devcontainer/Dockerfile.dovecot` | IMAP test server image | +| `.devcontainer/post-create.sh` | Setup automation script | +| `config/config.inc.php` | Roundcube configuration (generated) | + +## Stopping and Removing + +### Stop the Container + +Close VS Code or use: `F1` → "Remote: Close Remote Connection" + +### Remove Container and Volumes + +To completely reset (removes all data including database): + +```bash +# From outside the container +cd .devcontainer +docker compose down -v +``` + +Then reopen in VS Code to rebuild. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ac8caf39b8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,40 @@ +{ + "name": "Roundcube Development", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + + "features": { + "ghcr.io/devcontainers/features/git:1": {} + }, + + "customizations": { + "vscode": { + "extensions": [ + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + "junstyle.php-cs-fixer", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ], + "settings": { + "php.validate.executablePath": "/usr/local/bin/php", + "editor.formatOnSave": true + } + } + }, + + "forwardPorts": [3306, 143, 8025], + "portsAttributes": { + "3306": { "label": "MySQL" }, + "143": { "label": "IMAP" }, + "8025": { "label": "Mailhog UI" } + }, + "otherPortsAttributes": { + "onAutoForward": "notify" + }, + + "postCreateCommand": ".devcontainer/post-create.sh", + + "remoteUser": "vscode" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..873d0c8e44 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,68 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ..:/workspace:cached + command: sleep infinity + depends_on: + db: + condition: service_healthy + mail: + condition: service_started + mailhog: + condition: service_started + environment: + - ROUNDCUBE_DB_HOST=db + - ROUNDCUBE_DB_USER=roundcube + - ROUNDCUBE_DB_PASSWORD=roundcube + - ROUNDCUBE_DB_NAME=roundcube + - ROUNDCUBE_IMAP_HOST=mail + - ROUNDCUBE_SMTP_HOST=mailhog:1025 + networks: + - roundcube-dev + + db: + image: mariadb:11 + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: rootpassword + MARIADB_DATABASE: roundcube + MARIADB_USER: roundcube + MARIADB_PASSWORD: roundcube + volumes: + - mariadb-data:/var/lib/mysql + networks: + - roundcube-dev + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 30s + + mail: + build: + context: . + dockerfile: Dockerfile.dovecot + restart: unless-stopped + volumes: + - mail-data:/var/mail + networks: + - roundcube-dev + + mailhog: + image: mailhog/mailhog:latest + restart: unless-stopped + ports: + - "8025:8025" # Web UI + networks: + - roundcube-dev + +volumes: + mariadb-data: + mail-data: + +networks: + roundcube-dev: diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000000..b407e8e4c0 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,379 @@ +#!/bin/bash +set -e + +# ============================================================================= +# Roundcube Dev Container Setup Script +# ============================================================================= +# +# This script handles database initialization and dependency installation +# for the Roundcube development container environment. +# +# FUTURE ENHANCEMENTS (not yet implemented): +# ----------------------------------------------------------------------------- +# 1. LOGGING TO FILE +# - Add --log-file option to capture all output for debugging failed setups +# - Useful when postCreateCommand fails silently +# - Implementation: tee output to /workspace/.devcontainer/setup.log +# +# 2. VERSION COMPATIBILITY CHECKING +# - Compare schema version in database against expected version in codebase +# - Detect when database migrations are needed after code updates +# - Parse SQL/mysql.initial.sql for version and compare with DB system table +# - Offer to run migration scripts if versions don't match +# +# 3. BACKUP BEFORE RESET +# - Add --backup option to mysqldump before reset operations +# - Store backups in .devcontainer/backups/ with timestamps +# - Auto-cleanup old backups (keep last N) +# - Useful for preserving test data during development +# +# 4. HEALTHCHECK VERIFICATION +# - After initialization, verify core functionality works +# - Test a simple query on each table to ensure schema is correct +# - Report any permission or constraint issues +# +# 5. MULTI-DATABASE SUPPORT +# - Support PostgreSQL and SQLite in addition to MySQL +# - Auto-detect database type from environment or config +# - Use appropriate SQL initialization file +# +# ============================================================================= + +# Usage: ./post-create.sh [OPTIONS] +# (no args) - Full setup: database + dependencies + config +# --db-only - Only run database setup +# --db-reset - Force reset and reinitialize database +# --db-status - Check and report database state only +# --help - Show this help message + +show_help() { + echo "Roundcube Dev Container Setup Script" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " (no args) Full setup: database + dependencies + config" + echo " --db-only Only run database setup (detect state and initialize if needed)" + echo " --db-reset Force reset and reinitialize database (drops all tables)" + echo " --db-status Check and report database state only (no changes)" + echo " --help Show this help message" + echo "" +} + +# Function to check MariaDB readiness (not just connection, but ready for operations) +wait_for_mariadb() { + echo "Waiting for MariaDB to be ready..." + local max_tries=20 + local tries=0 + + # Phase 1: Wait for basic connectivity + until mariadb -h db -u roundcube -proundcube -e "SELECT 1" &> /dev/null; do + tries=$((tries + 1)) + if [ $tries -ge $max_tries ]; then + echo "ERROR: MariaDB did not become ready after $max_tries attempts" + echo "Last connection attempt output:" + mariadb -h db -u roundcube -proundcube -e "SELECT 1" 2>&1 || true + nc -zv db 3306 2>&1 || echo "Cannot connect to db:3306" + return 1 + fi + echo " Attempt $tries/$max_tries - waiting for MariaDB connection..." + sleep 2 + done + + # Phase 2: Wait for MariaDB to be fully ready for DDL operations + # Sometimes MariaDB accepts connections before it's ready for schema changes + tries=0 + until mariadb -h db -u roundcube -proundcube roundcube -e "SELECT 1" &> /dev/null; do + tries=$((tries + 1)) + if [ $tries -ge 10 ]; then + echo "ERROR: Database 'roundcube' is not accessible" + return 1 + fi + echo " Waiting for database 'roundcube' to be accessible..." + sleep 2 + done + + echo "MariaDB is ready!" + return 0 +} + +# Function to detect database state +# Returns: 0=uninitialized, 1=initialized, 2=partial/corrupt +detect_db_state() { + # Check if system table exists (it's the last thing created and contains version) + if mariadb -h db -u roundcube -proundcube roundcube -e "SELECT name FROM system WHERE name='roundcube-version'" &> /dev/null; then + local version=$(mariadb -h db -u roundcube -proundcube roundcube -N -e "SELECT value FROM system WHERE name='roundcube-version'" 2>/dev/null) + if [ -n "$version" ]; then + echo "DB_STATE: Initialized (version: $version)" + return 1 + fi + fi + + # Check if any core tables exist (indicates partial initialization) + local table_count=$(mariadb -h db -u roundcube -proundcube roundcube -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='roundcube'" 2>/dev/null) + # Default to 0 if empty or non-numeric + if [ -z "$table_count" ] || ! [[ "$table_count" =~ ^[0-9]+$ ]]; then + table_count=0 + fi + if [ "$table_count" -gt 0 ]; then + echo "DB_STATE: Partial initialization detected ($table_count tables exist, but no version marker)" + return 2 + fi + + echo "DB_STATE: Uninitialized (empty database)" + return 0 +} + +# Function to initialize database +init_database() { + echo "Initializing database schema..." + + # Run the initialization SQL + if mariadb -h db -u roundcube -proundcube roundcube < SQL/mysql.initial.sql 2>&1; then + echo "Database schema initialized successfully!" + return 0 + else + echo "ERROR: Database initialization failed" + return 1 + fi +} + +# Function to reset and reinitialize database +reset_database() { + echo "Resetting database (dropping all tables)..." + + # Build a single SQL script to drop all tables (FOREIGN_KEY_CHECKS must be in same session) + local tables=$(mariadb -h db -u roundcube -proundcube roundcube -N -e "SELECT table_name FROM information_schema.tables WHERE table_schema='roundcube'" 2>/dev/null) + + if [ -n "$tables" ]; then + local drop_sql="SET FOREIGN_KEY_CHECKS=0;" + for table in $tables; do + echo " Will drop table: $table" + drop_sql="${drop_sql} DROP TABLE IF EXISTS \`$table\`;" + done + drop_sql="${drop_sql} SET FOREIGN_KEY_CHECKS=1;" + + # Execute all drops in a single session + if ! mariadb -h db -u roundcube -proundcube roundcube -e "$drop_sql"; then + echo "ERROR: Failed to execute DROP statements" + echo "drop_sql was: $drop_sql" + echo "Aborting before init_database to prevent inconsistent state" + return 1 + fi + echo "All tables dropped." + else + echo "No tables to drop." + fi + + # Now initialize fresh + init_database +} + +# Main database setup logic +setup_database() { + if ! wait_for_mariadb; then + echo "WARNING: Skipping database setup due to MariaDB connection issues" + echo "You can manually initialize later with: mariadb -h db -u roundcube -proundcube roundcube < SQL/mysql.initial.sql" + return 1 + fi + + echo "" + echo "Checking database state..." + local db_state=0 + detect_db_state || db_state=$? + + case $db_state in + 0) # Uninitialized + init_database + ;; + 1) # Already initialized + echo "Database is already initialized - no action needed" + ;; + 2) # Partial/corrupt + echo "" + echo "WARNING: Database is in a partial/inconsistent state." + echo "This can happen if initialization was interrupted." + echo "" + echo "Options:" + echo " 1. Reset and reinitialize (recommended for dev)" + echo " 2. Skip (you can manually fix later)" + echo "" + # In non-interactive mode (like postCreateCommand), auto-reset for dev convenience + if [ ! -t 0 ]; then + echo "Non-interactive mode detected - auto-resetting database..." + reset_database + else + read -p "Reset database? [Y/n] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + reset_database + else + echo "Skipping database reset. Manual intervention may be required." + fi + fi + ;; + esac +} + +# Full setup: database + dependencies + config +full_setup() { + echo "=== Roundcube Dev Container Setup ===" + + # Run database setup + setup_database + + echo "" + + # Install PHP dependencies + echo "Installing Composer dependencies..." + composer install --prefer-dist --no-interaction + + # Download JavaScript libraries (jQuery, TinyMCE, CodeMirror) + # Run this BEFORE npm install since it's independent and npm can fail on some systems + echo "Installing JavaScript libraries..." + ./bin/install-jsdeps.sh + + # Install JavaScript dependencies (for build tools like lessc) + echo "Installing npm dependencies..." + npm install --include=dev --omit=optional || { + echo "WARNING: npm install failed. CSS compilation may not work." + echo "You can retry manually with: npm install --include=dev --omit=optional" + } + + # Compile LESS files to CSS for elastic skin + echo "Compiling CSS from LESS sources..." + npx lessc --clean-css="--s1 --advanced" skins/elastic/styles/styles.less > skins/elastic/styles/styles.min.css + npx lessc --clean-css="--s1 --advanced" skins/elastic/styles/print.less > skins/elastic/styles/print.min.css + npx lessc --clean-css="--s1 --advanced" skins/elastic/styles/embed.less > skins/elastic/styles/embed.min.css + + # Create config file if it doesn't exist + create_config +} + +# Create config file if it doesn't exist +create_config() { + if [ ! -f config/config.inc.php ]; then + echo "Creating development configuration..." + cat > config/config.inc.php << 'EOF' + -t public_html" + echo "" + echo "VS Code will auto-forward the port and show a notification." + echo "" +} + +# MariaDB connection options +# No SSL required for local development container +MARIADB_OPTS="" + +# Main entry point - parse arguments +case "${1:-}" in + --help|-h) + show_help + exit 0 + ;; + --db-only) + echo "=== Database Setup Only ===" + setup_database + ;; + --db-reset) + echo "=== Force Database Reset ===" + if ! wait_for_mariadb; then + echo "ERROR: Cannot connect to MariaDB" + exit 1 + fi + reset_database + ;; + --db-status) + echo "=== Database Status Check ===" + if ! wait_for_mariadb; then + echo "ERROR: Cannot connect to MariaDB" + exit 1 + fi + detect_db_state + ;; + "") + # No arguments - full setup + full_setup + show_completion + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; +esac diff --git a/.devcontainer/start-server.sh b/.devcontainer/start-server.sh new file mode 100644 index 0000000000..6b94d63486 --- /dev/null +++ b/.devcontainer/start-server.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Start Roundcube development server and open browser to appropriate page + +PORT=${1:-8081} + +# Determine the startup URL based on installer presence +if [ -f "installer/index.php" ] && [ -f "config/config.inc.php" ]; then + # Check if enable_installer is set to true in config + if grep -q "enable_installer.*=.*true" config/config.inc.php 2>/dev/null; then + STARTUP_PATH="/installer/" + echo "Installer is enabled - will open installer page" + else + STARTUP_PATH="/" + echo "Installer is disabled - will open main page" + fi +elif [ -f "installer/index.php" ] && [ ! -f "config/config.inc.php" ]; then + STARTUP_PATH="/installer/" + echo "No config found - will open installer page" +else + STARTUP_PATH="/" + echo "Will open main page" +fi + +STARTUP_URL="http://localhost:${PORT}${STARTUP_PATH}" + +echo "" +echo "Starting PHP development server on port ${PORT}..." +echo "URL: ${STARTUP_URL}" +echo "" + +# Give the server a moment to start, then try to open the browser +( + sleep 2 + # Try various methods to open the URL + if command -v xdg-open &> /dev/null; then + xdg-open "$STARTUP_URL" 2>/dev/null & + elif command -v open &> /dev/null; then + open "$STARTUP_URL" 2>/dev/null & + fi +) & + +# Start the PHP built-in server (this blocks) +exec php -S "0.0.0.0:${PORT}" -t public_html 2>&1 diff --git a/.eslintrc.js b/.eslintrc.js index 5c2c9a4fee..5e28a4817e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { '/plugins/managesieve/codemirror', '/program/js/tinymce', '/program/js/publickey.js', + '/temp', 'node_modules', '*.min.js', ], diff --git a/.gitignore b/.gitignore index 9b3ed0104b..dbad3cd540 100644 --- a/.gitignore +++ b/.gitignore @@ -30,10 +30,24 @@ program/js/tinymce/ skins/elastic/styles/_styles.less skins/elastic/styles/_variables.less -# ignore IDE files -.vscode +# ignore IDE files (except shared VS Code tasks) +.vscode/* +!.vscode/tasks.json .DS_Store .idea # Ignore files used for local overriding. /tests/MessageRendering/.env + +# PHPUnit cache +/tests/.phpunit.result.cache + +# Ignore vendor specific assisted coding AGENTS +CLAUDE.md +GEMINI.md +.claude +.claudeignore +.geminiignore + +# Ignore build artifacts +skins/elastic/styles/*.min.css diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 826cf9e250..2e98ce4e80 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,12 +8,22 @@ $finder = Finder::create() ->in([__DIR__]) ->exclude([ + '.ci', + '.devcontainer', 'node_modules', 'vendor', ]) ->ignoreDotFiles(false) ->name('*.php.dist') - ->name('*.sh'); + ->name('*.sh') + // bin/*.sh files are mostly PHP scripts with .sh extension + // Actual shell scripts in bin/ are excluded via notPath below + ->notPath([ + 'bin/cssshrink.sh', + 'bin/jsshrink.sh', + 'bin/makedoc.sh', + 'bin/transifexpull.sh', + ]); require_once __DIR__ . '/.php-cs-fixer-fully_qualified_strict_types.php'; diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..3ced18c30b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,162 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Roundcube Dev Server", + "type": "shell", + "command": "php", + "args": [ + "-S", + "0.0.0.0:${input:serverPort}", + "-t", + "public_html" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "isBackground": true, + "problemMatcher": { + "owner": "php", + "pattern": { + "regexp": "^PHP\\s+(Warning|Fatal error|Parse error|Notice):\\s+(.*)\\s+in\\s+(.*)\\s+on line\\s+(\\d+)$", + "severity": 1, + "message": 2, + "file": 3, + "line": 4 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^PHP .* Development Server", + "endsPattern": "^$" + } + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "label": "Run PHPUnit Tests", + "type": "shell", + "command": "vendor/bin/phpunit", + "args": [ + "-c", + "tests/phpunit.xml", + "--fail-on-warning" + ], + "group": "test", + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": "$phpunit" + }, + { + "label": "PHP CS Fixer (check)", + "type": "shell", + "command": "vendor/bin/php-cs-fixer", + "args": ["fix", "--dry-run", "--diff"], + "group": "build", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "PHP CS Fixer (fix)", + "type": "shell", + "command": "vendor/bin/php-cs-fixer", + "args": ["fix"], + "group": "build", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "PHPStan Analyse", + "type": "shell", + "command": "vendor/bin/phpstan", + "args": ["analyse", "-v"], + "group": "build", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "ESLint Check", + "type": "shell", + "command": "npx", + "args": ["eslint", "--ext", ".js", "."], + "group": "build", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Initialize Database", + "type": "shell", + "command": ".devcontainer/post-create.sh", + "args": ["--db-only"], + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [] + }, + { + "label": "Reset Database", + "type": "shell", + "command": ".devcontainer/post-create.sh", + "args": ["--db-reset"], + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [] + }, + { + "label": "Check Database Status", + "type": "shell", + "command": ".devcontainer/post-create.sh", + "args": ["--db-status"], + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [] + }, + { + "label": "Install Suggested Packages", + "type": "shell", + "command": "composer", + "args": [ + "require", + "--dev", + "bjeavons/zxcvbn-php", + "kolab/net_ldap3", + "--no-interaction" + ], + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "serverPort", + "type": "promptString", + "description": "Port for the development server", + "default": "8081" + } + ] +}