From 6291b6a07f730e7f56e47d25732eff20ff706780 Mon Sep 17 00:00:00 2001 From: Nick Cotterill <18194821+NickC404@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:04:50 +0000 Subject: [PATCH 1/6] Add orphaned attachment cleanup and email observer Adds a console command to remove orphaned email attachments and their files. Adds MailwebEmailObserver to delete attachment files when emails are deleted (needed for singlestore. Updates migration for mailweb_email_attachments to handle foreign key constraints based on database type (SingleStore has limitations with foreign key constraints). Registers the new command and observer in the service provider. --- .../Commands/CleanupOrphanedAttachments.php | 45 +++++++++++++++++++ src/Http/Models/MailwebEmailAttachment.php | 7 +++ src/MailWebServiceProvider.php | 9 ++++ ...0_000002_add_mail_web_attachment_table.php | 6 +++ src/Observers/MailwebEmailObserver.php | 19 ++++++++ 5 files changed, 86 insertions(+) create mode 100644 src/Console/Commands/CleanupOrphanedAttachments.php create mode 100644 src/Observers/MailwebEmailObserver.php diff --git a/src/Console/Commands/CleanupOrphanedAttachments.php b/src/Console/Commands/CleanupOrphanedAttachments.php new file mode 100644 index 0000000..a1b6ead --- /dev/null +++ b/src/Console/Commands/CleanupOrphanedAttachments.php @@ -0,0 +1,45 @@ +leftJoin('mailweb_emails', 'mailweb_email_attachments.mailweb_email_id', '=', 'mailweb_emails.id') + ->whereNull('mailweb_emails.id') + ->select('mailweb_email_attachments.*') + ->get(); + + $deletedCount = 0; + $deletedFiles = 0; + + foreach ($orphanedAttachments as $attachment) { + // Delete the physical file + if ($attachment->path && Storage::exists($attachment->path)) { + Storage::delete($attachment->path); + $deletedFiles++; + } + + // Delete the database record + DB::table('mailweb_email_attachments') + ->where('id', $attachment->id) + ->delete(); + + $deletedCount++; + } + + $this->info("Cleaned up {$deletedCount} orphaned attachments and {$deletedFiles} files."); + + return Command::SUCCESS; + } +} diff --git a/src/Http/Models/MailwebEmailAttachment.php b/src/Http/Models/MailwebEmailAttachment.php index ae40449..cd8469e 100644 --- a/src/Http/Models/MailwebEmailAttachment.php +++ b/src/Http/Models/MailwebEmailAttachment.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Storage; use Illuminate\Database\Eloquent\Concerns\HasUuids; +use Appoly\MailWeb\Models\MailwebEmail; +use App\Observers\MailwebEmailObserver; class MailwebEmailAttachment extends Model { @@ -47,4 +49,9 @@ public function getDownloadUrlAttribute() return route('mailweb.download-attachment', [$this->mailwebEmail, $this]); } + public function boot() + { + MailwebEmail::observe(MailwebEmailObserver::class); + } + } diff --git a/src/MailWebServiceProvider.php b/src/MailWebServiceProvider.php index 12ea1eb..0fd1752 100644 --- a/src/MailWebServiceProvider.php +++ b/src/MailWebServiceProvider.php @@ -2,6 +2,8 @@ namespace Appoly\MailWeb; +use App\Console\Commands\CleanupOrphanedAttachments; +use Appoly\MailWeb\Http\Models\MailwebEmail; use Illuminate\Support\Js; use Illuminate\Support\HtmlString; use Appoly\MailWeb\Facades\MailWeb; @@ -18,6 +20,7 @@ public function register() $this->commands([ PruneMailwebMails::class, + CleanupOrphanedAttachments::class ]); $this->app->singleton('MailWeb', function () { @@ -36,6 +39,12 @@ public function boot() __DIR__ . '/../config/config.php' => config_path('MailWeb.php'), ], 'mailweb-config'); } + if ($this->app->runningInConsole()) { + $this->commands([ + \Appoly\MailWeb\Console\Commands\CleanupOrphanedAttachments::class, + ]); + } + MailwebEmail::observe(MailWebEmailObserver::class); } /** diff --git a/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php b/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php index 2877044..f4e77ed 100644 --- a/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php +++ b/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php @@ -15,6 +15,12 @@ public function up() { Schema::create('mailweb_email_attachments', function (Blueprint $table) { $table->uuid('id')->primary(); + if ($this->isSingleStore()) { + $table->foreignUuid('mailweb_email_id'); + $table->index('mailweb_email_id'); + } else { + $table->foreignUuid('mailweb_email_id')->constrained('mailweb_emails')->onDelete('cascade'); + } $table->foreignUuid('mailweb_email_id')->constrained('mailweb_emails')->onDelete('cascade'); $table->string('name')->nullable(); $table->string('path')->nullable(); diff --git a/src/Observers/MailwebEmailObserver.php b/src/Observers/MailwebEmailObserver.php new file mode 100644 index 0000000..6a9e485 --- /dev/null +++ b/src/Observers/MailwebEmailObserver.php @@ -0,0 +1,19 @@ +attachments as $attachment) { + if ($attachment->path && Storage::exists($attachment->path)) { + Storage::delete($attachment->path); + } + $attachment->delete(); + } + } +} From 65535b50649c9672d4e7b148b6b4d17b81583848 Mon Sep 17 00:00:00 2001 From: Nick Cotterill <18194821+NickC404@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:24:49 +0000 Subject: [PATCH 2/6] Refactor namespaces and fix migration duplication Updated namespaces for CleanupOrphanedAttachments command and MailwebEmailAttachment model to ensure consistency. Fixed duplicate foreign key definition in mail web attachment migration and added isSingleStore helper. Adjusted observer registration in MailwebEmailAttachment and updated service provider imports. --- src/Console/Commands/CleanupOrphanedAttachments.php | 2 +- src/Http/Models/MailwebEmailAttachment.php | 7 ++++--- src/MailWebServiceProvider.php | 3 ++- .../0000_00_00_000002_add_mail_web_attachment_table.php | 7 ++++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Console/Commands/CleanupOrphanedAttachments.php b/src/Console/Commands/CleanupOrphanedAttachments.php index a1b6ead..d53d084 100644 --- a/src/Console/Commands/CleanupOrphanedAttachments.php +++ b/src/Console/Commands/CleanupOrphanedAttachments.php @@ -1,6 +1,6 @@ mailwebEmail, $this]); } - public function boot() + protected static function boot() { + parent::boot(); MailwebEmail::observe(MailwebEmailObserver::class); } diff --git a/src/MailWebServiceProvider.php b/src/MailWebServiceProvider.php index 0fd1752..ca35b55 100644 --- a/src/MailWebServiceProvider.php +++ b/src/MailWebServiceProvider.php @@ -2,14 +2,15 @@ namespace Appoly\MailWeb; -use App\Console\Commands\CleanupOrphanedAttachments; use Appoly\MailWeb\Http\Models\MailwebEmail; +use Appoly\MailWeb\Observers\MailwebEmailObserver; use Illuminate\Support\Js; use Illuminate\Support\HtmlString; use Appoly\MailWeb\Facades\MailWeb; use Illuminate\Support\ServiceProvider; use Appoly\MailWeb\Providers\MessageServiceProvider; use Appoly\MailWeb\Console\Commands\PruneMailwebMails; +use Appoly\MailWeb\Console\Commands\CleanupOrphanedAttachments; class MailWebServiceProvider extends ServiceProvider { diff --git a/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php b/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php index f4e77ed..4786678 100644 --- a/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php +++ b/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php @@ -21,7 +21,6 @@ public function up() } else { $table->foreignUuid('mailweb_email_id')->constrained('mailweb_emails')->onDelete('cascade'); } - $table->foreignUuid('mailweb_email_id')->constrained('mailweb_emails')->onDelete('cascade'); $table->string('name')->nullable(); $table->string('path')->nullable(); $table->timestamps(); @@ -37,4 +36,10 @@ public function down() { Schema::dropIfExists('mailweb_email_attachments'); } + + private function isSingleStore(): bool + { + return DB::connection()->getDriverName() === 'singlestore' + || str_contains(DB::connection()->getConfig('driver') ?? '', 'singlestore'); + } }; From 508e5d79695c7bd2870940c77a4dfc41c50a908a Mon Sep 17 00:00:00 2001 From: Nick Cotterill <18194821+NickC404@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:50:43 +0000 Subject: [PATCH 3/6] Improve access control docs and refactor service provider Expanded README with detailed access control options for MailWeb dashboard, including environment-based, email-based, and role-based gates. Refactored MailWebServiceProvider to register commands only in console context. Removed unused observer boot logic from MailwebEmailAttachment model. Clarified migration comments for SingleStore foreign key handling. --- README.md | 68 ++++++++++++++++--- src/Http/Models/MailwebEmailAttachment.php | 7 -- src/MailWebServiceProvider.php | 8 +-- ...0_000002_add_mail_web_attachment_table.php | 2 + 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c0ff02b..c59124c 100644 --- a/README.md +++ b/README.md @@ -57,18 +57,72 @@ Route::mailweb(); ### 2. Access Control -Add to your `AppServiceProvider` (Laravel 11+) or `AuthServiceProvider`: +By default, access to the MailWeb dashboard may be restricted to local environments. To control access in specific environments (like production or staging), you should define a Gate in `AppServiceProvider` (Laravel 11+) or `AuthServiceProvider`. +The gate name must be `'view-mailweb'`. You can choose the logic that best fits your workflow below. + +### Option 1: Allow Specific Emails (via .env) +This is the most flexible method for small teams. It allows you to grant access via your `.env` file without redeploying code. + +**1. Add to your `.env` file:** +```dotenv +MAILWEB_ALLOWED_EMAILS="admin@company.com,developer@agency.com" +``` + +**2. Add to your `config/services.php` file:** +```php +'mailweb' => [ + 'authorized_users' => explode(',', env('MAILWEB_ALLOWED_EMAILS', '')), +] +``` + +**3. Add to `AppServiceProvider::boot()`:** +```php +use Illuminate\Support\Facades\Gate; + +public function boot() +{ + Gate::define('view-mailweb', function ($user = null) { + // Get emails from mailweb config, split by comma + $allowed = config('services.mailweb.authorized_users', []); + + // Check if user is logged in AND their email is in the list + return $user && in_array($user->email, $allowed); + }); +} +``` + +### Option 2: Role or Permission Based +If your application uses a role management system (like a `role` column, `is_admin` boolean, or Spatie Permissions), use that as your source of truth. + +**Add to `AppServiceProvider::boot()`:** +```php +use Illuminate\Support\Facades\Gate; + +public function boot() +{ + Gate::define('view-mailweb', function ($user = null) { + // Example: Check a boolean column + // return $user && $user->is_admin; + + // Example: Check a specific role string + // return $user && $user->role === 'admin'; + }); +} +``` + +### Option 3: Open Access (Local/Staging Only) +Use this if you want to allow **everyone** (including guests/non-logged-in users) to view the emails, but **only** on specific environments. + +**Add to `AppServiceProvider::boot()`:** ```php use Illuminate\Support\Facades\Gate; public function boot() { - Gate::define('view-mailweb', function ($user) { - return in_array($user->email, [ - 'admin@example.com', - // Add authorized emails - ]); + Gate::define('view-mailweb', function ($user = null) { + // ⚠️ Allows anyone to view emails if the environment matches + return app()->environment('local', 'staging'); }); } ``` @@ -107,8 +161,6 @@ MAILWEB_ATTACHMENTS_DISK=s3 # Or any configured disk MAILWEB_ATTACHMENTS_PATH=/custom/path # Optional, defaults to /mailweb/attachments ``` - - ## 🤝 Contributing We welcome contributions! Please follow these steps: diff --git a/src/Http/Models/MailwebEmailAttachment.php b/src/Http/Models/MailwebEmailAttachment.php index 012c34a..6442cd3 100644 --- a/src/Http/Models/MailwebEmailAttachment.php +++ b/src/Http/Models/MailwebEmailAttachment.php @@ -2,8 +2,6 @@ namespace Appoly\MailWeb\Http\Models; -use Appoly\MailWeb\Observers\MailwebEmailObserver; -use Appoly\MailWeb\Http\Models\MailwebEmail; use Illuminate\Support\Number; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Storage; @@ -49,10 +47,5 @@ public function getDownloadUrlAttribute() return route('mailweb.download-attachment', [$this->mailwebEmail, $this]); } - protected static function boot() - { - parent::boot(); - MailwebEmail::observe(MailwebEmailObserver::class); - } } diff --git a/src/MailWebServiceProvider.php b/src/MailWebServiceProvider.php index ca35b55..c696829 100644 --- a/src/MailWebServiceProvider.php +++ b/src/MailWebServiceProvider.php @@ -9,8 +9,6 @@ use Appoly\MailWeb\Facades\MailWeb; use Illuminate\Support\ServiceProvider; use Appoly\MailWeb\Providers\MessageServiceProvider; -use Appoly\MailWeb\Console\Commands\PruneMailwebMails; -use Appoly\MailWeb\Console\Commands\CleanupOrphanedAttachments; class MailWebServiceProvider extends ServiceProvider { @@ -19,11 +17,6 @@ public function register() $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'MailWeb'); $this->app->register(MessageServiceProvider::class); - $this->commands([ - PruneMailwebMails::class, - CleanupOrphanedAttachments::class - ]); - $this->app->singleton('MailWeb', function () { return new MailWeb; }); @@ -43,6 +36,7 @@ public function boot() if ($this->app->runningInConsole()) { $this->commands([ \Appoly\MailWeb\Console\Commands\CleanupOrphanedAttachments::class, + \Appoly\MailWeb\Console\Commands\PruneMailwebMails::class, ]); } MailwebEmail::observe(MailWebEmailObserver::class); diff --git a/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php b/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php index 4786678..2bf2ac3 100644 --- a/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php +++ b/src/Migrations/0000_00_00_000002_add_mail_web_attachment_table.php @@ -16,9 +16,11 @@ public function up() Schema::create('mailweb_email_attachments', function (Blueprint $table) { $table->uuid('id')->primary(); if ($this->isSingleStore()) { + // Singlestore doesn't support foreign keys, so we will just make it an index if the DB connection is this $table->foreignUuid('mailweb_email_id'); $table->index('mailweb_email_id'); } else { + // ...otheriwse, we're safe to foreign key it $table->foreignUuid('mailweb_email_id')->constrained('mailweb_emails')->onDelete('cascade'); } $table->string('name')->nullable(); From 32669afbbd57af6507728b76bead528763d49fab Mon Sep 17 00:00:00 2001 From: Nathan James <64075030+Nathanjms@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:58:59 +0000 Subject: [PATCH 4/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c59124c..e42c4a6 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ By default, access to the MailWeb dashboard may be restricted to local environme The gate name must be `'view-mailweb'`. You can choose the logic that best fits your workflow below. -### Option 1: Allow Specific Emails (via .env) +#### Option 1: Allow Specific Emails (via .env) This is the most flexible method for small teams. It allows you to grant access via your `.env` file without redeploying code. **1. Add to your `.env` file:** From d33572bd56deb0fc1091842e7884316804b8b336 Mon Sep 17 00:00:00 2001 From: Nathan James <64075030+Nathanjms@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:59:22 +0000 Subject: [PATCH 5/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e42c4a6..451a789 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ public function boot() } ``` -### Option 2: Role or Permission Based +#### Option 2: Role or Permission Based If your application uses a role management system (like a `role` column, `is_admin` boolean, or Spatie Permissions), use that as your source of truth. **Add to `AppServiceProvider::boot()`:** From d3a76242b1a09dcab33d524604b36812dba26f27 Mon Sep 17 00:00:00 2001 From: Nathan James <64075030+Nathanjms@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:59:29 +0000 Subject: [PATCH 6/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 451a789..ecf7001 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ public function boot() } ``` -### Option 3: Open Access (Local/Staging Only) +#### Option 3: Open Access (Local/Staging Only) Use this if you want to allow **everyone** (including guests/non-logged-in users) to view the emails, but **only** on specific environments. **Add to `AppServiceProvider::boot()`:**