Compare commits

...

13 Commits

Author SHA1 Message Date
idevakk
42e2f1bf0a fix: force https scheme in production to resolve mixed-content browser blocking in dokploy 2026-03-01 09:02:40 +05:30
idevakk
2989613b75 fix: remove forced migrations from entrypoint as it halts container boot on partially migrated databases 2026-03-01 08:54:29 +05:30
idevakk
285b995afb fix: auto-run safe migrations in entrypoint for missing cache/queue tables 2026-03-01 08:51:19 +05:30
idevakk
0d9a524267 fix: auto-create database.sqlite gracefully in existing folder to prevent laravel fallback crashes 2026-03-01 08:45:46 +05:30
idevakk
b30da6614a feat: setup custom webdevops Dockerfile with explicit PHP 8.4 Zemailnator extensions and configs 2026-03-01 08:36:49 +05:30
idevakk
0aed57bc2c fix: remove docker-compose.yml in favor of pure Dockerfile deployment for simplicity 2026-03-01 00:08:55 +05:30
idevakk
6d24d1f6fc fix: add zip extension to php-extension-installer 2026-02-28 23:50:35 +05:30
idevakk
189c2d7c58 fix: use php-extension-installer for PHP 8.4 IMAP PECL compilation 2026-02-28 23:46:27 +05:30
idevakk
7f58ca9f45 fix: migrate Dockerfile to php:8.4-fpm-alpine to restore native IMAP support 2026-02-28 23:43:51 +05:30
idevakk
66b5d2f89e docs: move MailOps webhook handover document to the docs directory 2026-02-28 23:38:18 +05:30
idevakk
a04222dcf3 fix: restore missing Dockerfile 2026-02-28 23:38:00 +05:30
idevakk
58aa4d2dbc docs: update Project.md with Dokploy architecture and resolved tech debt 2026-02-28 23:28:01 +05:30
idevakk
e342c2bdae chore: remove redis and mariadb from compose for standalone deployment 2026-02-28 23:26:19 +05:30
8 changed files with 150 additions and 78 deletions

69
Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
# 1. Use the specific PHP 8.4 Alpine image
FROM webdevops/php-nginx:8.4-alpine
# 2. Environment Configuration
ENV WEB_DOCUMENT_ROOT=/code/public
ENV APP_ENV=production
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_PROCESS_TIMEOUT=2000
# 3. Install System Dependencies & Extensions
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN apk add --no-cache \
nodejs \
npm \
sqlite \
shadow \
&& install-php-extensions pdo_sqlite mongodb redis imap pdo_mysql
# 4. Copy Custom Configs
COPY ./dockerizer/php.ini /opt/docker/etc/php/php.ini
COPY ./dockerizer/vhost.conf /opt/docker/etc/nginx/vhost.conf
COPY ./dockerizer/supervisor.laravel.conf /opt/docker/etc/supervisor.d/laravel.conf
# 5. Set Working Directory
WORKDIR /code
# CRITICAL FIX: Transfer ownership of /code to the 'application' user
# WORKDIR creates folders as 'root' by default, which blocks Composer later.
RUN chown application:application /code
# 6. Install PHP Dependencies (Layered)
COPY --chown=application:application composer.json composer.lock ./
USER application
RUN composer install --no-interaction --no-scripts --no-autoloader --prefer-dist --no-dev
# 7. Install Node Dependencies
COPY --chown=application:application package.json package-lock.json* ./
RUN npm install
# 8. Copy Application Code
COPY --chown=application:application . .
# 9. Final Builds
RUN composer dump-autoload --optimize && \
composer run-script post-root-package-install && \
php artisan package:discover --ansi
RUN npm run build
# 10. Runtime Initialization Script
# This runs when the container STARTS, not when it builds.
USER root
RUN echo '#!/bin/bash' > /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'set -e' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
# 1. Fix Permissions \
echo 'echo "Fixing storage permissions..."' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'mkdir -p /code/storage/framework/{views,cache,sessions} /code/bootstrap/cache' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'touch /code/database/database.sqlite' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'chown -R application:application /code/storage /code/bootstrap/cache /code/database' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'chmod -R 775 /code/storage /code/bootstrap/cache /code/database' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
# 2. Run Optimization (As the application user) \
echo 'echo "Running Laravel Optimizations..."' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'su -s /bin/sh application -c "php artisan optimize"' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
# 3. Cache Views (Optional but recommended for production) \
echo 'su -s /bin/sh application -c "php artisan view:cache"' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
chmod +x /opt/docker/provision/entrypoint.d/99-init-laravel.sh

View File

@@ -11,8 +11,13 @@ Zemailnator is a scalable disposable temporary email service (like Mailinator or
**Core Mechanics:** **Core Mechanics:**
- The platform uses IMAP (`ddeboer/imap` via PHP `ext-imap`) to fetch emails from a master mailbox. - The platform uses IMAP (`ddeboer/imap` via PHP `ext-imap`) to fetch emails from a master mailbox.
- `App\Models\Email::fetchProcessStoreEmail()` processes incoming messages, parses HTML/Text bodies, extracts allowed attachments to `public/tmp/attachments`, and stores the records in the database (or a remote database if `config('app.fetch_from_remote_db')` is true). - `App\Models\Email::fetchProcessStoreEmail()` processes incoming messages, parses HTML/Text bodies, extracts allowed attachments to the `public` Laravel Storage disk, and stores the records in the database.
- External cron/scripts (like `dropmail.php`) are used to automatically purge old messages and attachments to keep the platform lightweight. - Background cleanup and email fetching are handled automatically via Supervisor running Laravel Scheduled Commands (`emails:fetch`, `mailbox:clean`, `attachments:clean`).
## 1.1 Deployment Architecture (Dokploy)
Zemailnator is containerized for zero-downtime deployment on Dokploy:
- **Application Container:** Uses `php:8.4-fpm-alpine` configured via a single `Dockerfile` with a bundled Nginx server and Supervisor to manage background queues.
- **External Services:** MariaDB and Redis run as fully separate, standalone Dokploy application instances to allow for independent backups and to prevent database restarts during application deployment.
## 2. Technology Stack ## 2. Technology Stack
- **Framework:** Laravel 12.x - **Framework:** Laravel 12.x
@@ -29,27 +34,27 @@ Zemailnator is a scalable disposable temporary email service (like Mailinator or
- **Admin Dashboard:** Fully managed via Filament, including logs functionality, failed jobs monitoring, and dynamic database configuration. - **Admin Dashboard:** Fully managed via Filament, including logs functionality, failed jobs monitoring, and dynamic database configuration.
- **Impersonation:** Admins can impersonate users for troubleshooting (`ImpersonationController`). - **Impersonation:** Admins can impersonate users for troubleshooting (`ImpersonationController`).
## 4. Known Security Flaws & Vulnerabilities (CRITICAL) ## 4. Security & Technical Debt Improvements (Resolved)
When working on this codebase, prioritize addressing or being mindful of the following security issues: The following critical flaws have been successfully addressed:
1. **Hardcoded Credentials in Routes:** 1. **Hardcoded Credentials in Routes:**
- Found in `routes/web.php` for endpoints `/0xdash/slink` and `/0xdash/scache`. They use basic HTTP authentication with a hardcoded password (`admin@9608`). This is highly insecure and should be migrated to secure Artisan commands via the Filament Admin panel or proper token-based middleware. - *Resolved:* The highly insecure `/0xdash/slink` and `/0xdash/scache` endpoints with basic HTTP authentication have been permanently removed. Storage linking is now handled securely during deployment via `entrypoint.sh`.
2. **Public Attachment Storage:** 2. **Public Attachment Storage (RCE Risk):**
- Attachments are stored directly in `public/tmp/attachments`. While there is an extension filter in `Email::fetchProcessStoreEmail()`, any bypass of this filter (e.g., uploading a `.php` file masquerading as another or zero-byte vulnerabilities) can lead to direct Remote Code Execution (RCE) since the directory is publicly accessible. - *Resolved:* Attachments are no longer stored directly in `public/tmp/attachments`. The `Email` model now strictly leverages Laravel's secure `Storage::disk('public')` abstraction avoiding direct `file_put_contents`.
3. **No Strict Rate Limiting on Generation Endpoints:** 3. **Standalone PHP Scripts:**
- Temporary email and bulk email endpoints in `routes/web.php` appear to lack specific rate limiting, making the application vulnerable to Denial of Service (DoS) and excessive spam generation. - *Resolved:* Legacy scripts like `dropmail.php` and `cleanCron.php` now securely dispatch the new native Laravel Console Commands (`mailbox:clean`, etc.).
4. **Standalone PHP Scripts:** 4. **Legacy Checkout System:**
- Scripts like `dropmail.php`, `closeTicket.php`, and `cleanCron.php` inside the public/root directory bootstrap Laravel manually. They lack proper authentication and could potentially be executed repeatedly by unauthorized entities, causing database or IMAP server exhaustion. They should be migrated to Laravel Console Commands (`app/Console/Commands`). - *Resolved:* Old Stripe Cashier routes (`checkout/{plan}`) were removed to enforce the newer Unified Payment System.
*(Note: Ensure rate limiting is strictly applied to Livewire generation endpoints to prevent DoS.)*
## 5. Pros & Cons ## 5. Pros & Cons
**Pros:** **Pros:**
- **Modern Stack:** Utilizes bleeding-edge tools (Laravel 12, Livewire 3, Tailwind 4) resulting in a highly responsive SPA-like feel. - **Modern Stack:** Utilizes bleeding-edge tools (Laravel 12, PHP 8.4, Livewire 3, Tailwind 4) resulting in a highly responsive SPA-like feel.
- **Robust Admin Panel:** Leveraging Filament provides out-of-the-box CRUD, logging, and mail monitoring. - **Robust Admin Panel:** Leveraging Filament provides out-of-the-box CRUD, logging, and mail monitoring.
- **Flexible Monetization:** Mix of traditional Stripe and unified payments (crypto via OxaPay) allows for diverse revenue streams. - **Production Ready:** Fully containerized setup tailored for Dokploy using MariaDB/Redis standalone instances for maximum scalability and zero-downtime deployments.
**Cons:** **Cons:**
- **Technical Debt:** Legacy payment routes (`checkout/{plan}`) exist alongside the unified payment system, leading to code fragmentation. - **Rate Limiting:** Needs comprehensive throttling on Livewire generation endpoints to completely mitigate aggressive abuse.
- **Synchronous IMAP Polling:** Running IMAP fetch directly or via sequential cron is slow. It should be offloaded to robust Laravel Queue workers.
- **File Management:** Relying on basic `file_put_contents` for attachments locally limits horizontal scalability (e.g., scaling across multiple Docker pods/servers). S3 or equivalent Object Storage should be used.
## 6. Development Guide & Setup Best Practices ## 6. Development Guide & Setup Best Practices
1. Ensure both `ext-imap` and `ext-curl` are enabled in your `php.ini`. 1. Ensure both `ext-imap` and `ext-curl` are enabled in your `php.ini`.

View File

@@ -35,11 +35,14 @@ class AppServiceProvider extends ServiceProvider
$this->loadConfiguration(); $this->loadConfiguration();
$this->loadDomainUsernameData(); $this->loadDomainUsernameData();
// Only load application data when not in testing environment
if (! $this->app->environment('testing')) { if (! $this->app->environment('testing')) {
$this->loadApplicationData(); $this->loadApplicationData();
} }
if ($this->app->environment('production')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
Cashier::calculateTaxes(); Cashier::calculateTaxes();
} }

View File

@@ -1,61 +0,0 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "${APP_PORT:-8000}:80"
environment:
- APP_ENV=${APP_ENV:-production}
- APP_DEBUG=${APP_DEBUG:-false}
- APP_URL=${APP_URL}
- DB_CONNECTION=${DB_CONNECTION:-mysql}
- DB_HOST=${DB_HOST:-mariadb}
- DB_PORT=${DB_PORT:-3306}
- DB_DATABASE=${DB_DATABASE:-zemail}
- DB_USERNAME=${DB_USERNAME:-zemail_user}
- DB_PASSWORD=${DB_PASSWORD:-secret}
- CACHE_STORE=${CACHE_STORE:-redis}
- QUEUE_CONNECTION=${QUEUE_CONNECTION:-redis}
- SESSION_DRIVER=${SESSION_DRIVER:-redis}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PASSWORD=${REDIS_PASSWORD:-null}
- REDIS_PORT=${REDIS_PORT:-6379}
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
- app-storage:/var/www/storage
- app-public-tmp:/var/www/public/tmp
depends_on:
- redis
- mariadb
redis:
image: redis:alpine
restart: unless-stopped
volumes:
- redis-data:/data
mariadb:
image: mariadb:10.11
restart: unless-stopped
environment:
- MYSQL_DATABASE=${DB_DATABASE:-zemail}
- MYSQL_USER=${DB_USERNAME:-zemail_user}
- MYSQL_PASSWORD=${DB_PASSWORD:-secret}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-rootsecret}
volumes:
- mariadb-data:/var/lib/mysql
volumes:
app-storage:
app-public-tmp:
redis-data:
mariadb-data:

6
dockerizer/php.ini Normal file
View File

@@ -0,0 +1,6 @@
date.time = UTC
display_errors = Off
memory_limit = 128M
max_execution_time = 60
post_max_size = 32M
upload_max_filesize = 16M

View File

@@ -0,0 +1,25 @@
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /code/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=application
numprocs=1
redirect_stderr=true
stdout_logfile=/docker.stdout
stdout_logfile_maxbytes=0
[program:laravel-scheduler]
process_name=%(program_name)s_%(process_num)02d
command=php /code/artisan schedule:work
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=application
numprocs=1
redirect_stderr=true
stdout_logfile=/docker.stdout
stdout_logfile_maxbytes=0

25
dockerizer/vhost.conf Normal file
View File

@@ -0,0 +1,25 @@
server {
listen 80 default_server;
server_name _;
root "/code/public";
index index.php;
client_max_body_size 50m;
access_log /docker.stdout;
error_log /docker.stderr warn;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_read_timeout 600;
}
}