├── README.md ├── composer.json ├── config └── mail-export.php └── src ├── Concerns └── Exportable.php ├── Contracts └── ShouldExport.php ├── Events └── MessageStored.php ├── Listeners └── ExportMessage.php ├── MailExportServiceProvider.php ├── Providers └── EventServiceProvider.php └── StorageOptions.php /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Mail Export 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/pod-point/laravel-mail-export.svg?style=flat-square)](https://packagist.org/packages/pod-point/laravel-mail-export) 4 | [![tests](https://github.com/Pod-Point/laravel-mail-export/actions/workflows/run-tests.yml/badge.svg?branch=2.x)](https://github.com/Pod-Point/laravel-mail-export/actions/workflows/run-tests.yml) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/pod-point/laravel-mail-export.svg?style=flat-square)](https://packagist.org/packages/pod-point/laravel-mail-export) 7 | 8 | This package can export any mail sent with Laravel's `Mailable` class to any desired filesystem disk and path as a `.eml` file. 9 | 10 | This can be useful when wanting to store emails sent for archive purposes. 11 | 12 | ## Installation 13 | 14 | You can install the package via composer: 15 | 16 | For Laravel 9.x, 10.x, 11.x (requires PHP version 8.2 or higher) 17 | 18 | ```bash 19 | composer require pod-point/laravel-mail-export 20 | ``` 21 | 22 | For Laravel 7.x and 8.x 23 | 24 | ```bash 25 | composer require pod-point/laravel-mail-export:^1.0 26 | ``` 27 | 28 | For Laravel 5.x and 6.x 29 | 30 | ```bash 31 | composer require pod-point/laravel-mail-export:^0.1 32 | ``` 33 | 34 | ### Publishing the config file 35 | 36 | The configuration for this package comes with some sensible values but you can optionally publish the config file with: 37 | 38 | ```bash 39 | php artisan vendor:publish --provider="PodPoint\MailExport\MailExportServiceProvider" 40 | ``` 41 | 42 | You will be able to specify: 43 | 44 | * `enabled`: whether this package is enabled or not. Once installed, it's enabled by default but the `MAIL_EXPORT` environment variable can be used to configure this. 45 | * `disk`: which disk to use by default. `null` will use the default disk from your application filesystem. 46 | * `path`: the default path, within the configured disk, where mail will be exported. 47 | 48 | See our [`config/mail-export.php`](config/mail-export.php) for more details. 49 | 50 | ## Usage 51 | 52 | Simply add the `Exportable` trait and the `ShouldExport` interface to any Mailable class that you want to persist into any storage disk. 53 | 54 | ```php 55 | user())->send(new OrderShipped($order)); 142 | ``` 143 | 144 | Even with Notifications too: 145 | 146 | ```php 147 | order))->to($notifiable->email); 161 | } 162 | } 163 | ``` 164 | 165 | ## Testing 166 | 167 | Run the tests with: 168 | 169 | ```bash 170 | composer test 171 | ``` 172 | 173 | ## Changelog 174 | 175 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 176 | 177 | ## Contributing 178 | 179 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 180 | 181 | ## Credits 182 | 183 | - [themsaid](https://github.com/themsaid) and Spatie's [laravel-mail-preview](https://github.com/spatie/laravel-mail-preview) for some inspiration 184 | - [Laravel Package Development](https://laravelpackage.com) documentation by [John Braun](https://github.com/Jhnbrn90) 185 | - [Pod Point](https://github.com/pod-point) 186 | - [All Contributors](https://github.com/pod-point/laravel-mail-export/graphs/contributors) 187 | 188 | ## License 189 | 190 | The MIT License (MIT). Please see [License File](LICENCE.md) for more information. 191 | 192 | --- 193 | 194 | 195 | 196 | Travel shouldn't damage the earth 🌍 197 | 198 | Made with ❤️  at [Pod Point](https://pod-point.com) 199 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pod-point/laravel-mail-export", 3 | "description": "A mailable trait to export mails to a storage disk once being sent", 4 | "keywords": ["laravel", "mail", "storage", "archive", "backup"], 5 | "homepage": "https://github.com/pod-point/laravel-mail-export", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Pod Point Software Team", 10 | "email": "software@pod-point.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^8.1", 15 | "illuminate/filesystem": "^9.0|^10.0|^11.0", 16 | "illuminate/mail": "^9.0|^10.0|^11.0", 17 | "illuminate/support": "^9.0|^10.0|^11.0", 18 | "nesbot/carbon": "^2.0" 19 | }, 20 | "require-dev": { 21 | "orchestra/testbench": "^7.0|^8.0|^9.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "PodPoint\\MailExport\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "PodPoint\\MailExport\\Tests\\": "tests" 31 | } 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "PodPoint\\MailExport\\MailExportServiceProvider" 37 | ] 38 | } 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit --colors=always", 42 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 43 | }, 44 | "minimum-stability": "dev", 45 | "prefer-stable": true, 46 | "config": { 47 | "sort-packages": true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/mail-export.php: -------------------------------------------------------------------------------- 1 | env('MAIL_EXPORT', true), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may specify the default filesystem disk that should be used 24 | | by the package in order to export a copy of your mail sent out by 25 | | the framework. Make sure your filesystem disks are configured! 26 | | 27 | | You can set this to `null` and we will automatically use the default 28 | | disk configured for your application. 29 | | 30 | */ 31 | 32 | 'disk' => null, 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Default Filesystem Path 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Here you may specify the default filesystem path that should be used 40 | | by the package when exporting a mail. This can also be changed at 41 | | a Mailable or Message level. Make sure to check our README.md. 42 | | 43 | */ 44 | 45 | 'path' => 'email-exports', 46 | 47 | ]; 48 | -------------------------------------------------------------------------------- /src/Concerns/Exportable.php: -------------------------------------------------------------------------------- 1 | withSymfonyMessage(function ($message) { 20 | if (! $this instanceof ShouldExport) { 21 | return; 22 | } 23 | 24 | $message->_storageOptions = new StorageOptions($message, [ 25 | 'disk' => $this->exportOption('exportDisk'), 26 | 'path' => $this->exportOption('exportPath'), 27 | 'filename' => $this->exportOption('exportFilename'), 28 | ]); 29 | }); 30 | 31 | parent::send($mailer); 32 | } 33 | 34 | /** 35 | * Tries to resolve storage options from an optional method and property. 36 | * 37 | * @param string $key 38 | * @return string|null 39 | */ 40 | private function exportOption(string $key): ?string 41 | { 42 | if (method_exists($this, $key)) { 43 | return $this->$key(); 44 | } 45 | 46 | return property_exists($this, $key) ? $this->$key : null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/ShouldExport.php: -------------------------------------------------------------------------------- 1 | message = $message; 38 | $this->storageOptions = $storageOptions; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Listeners/ExportMessage.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 26 | } 27 | 28 | /** 29 | * Handles the Event when it happens while listening. 30 | * 31 | * @param MessageSent $event 32 | */ 33 | public function handle(MessageSent $event) 34 | { 35 | if ($this->shouldStoreMessage($event->message)) { 36 | $this->storeMessage($event->message); 37 | } 38 | } 39 | 40 | /** 41 | * Finds out if whether we should store the mail or not. 42 | * 43 | * @return bool 44 | */ 45 | protected function shouldStoreMessage(Email $message): bool 46 | { 47 | return property_exists($message, '_storageOptions') 48 | && config('mail-export.enabled', false); 49 | } 50 | 51 | /** 52 | * Actually stores the stringified version of the \Symfony\Component\Mime\Email 53 | * including headers, recipients, subject and body onto the filesystem disk. 54 | * 55 | * @return void 56 | */ 57 | private function storeMessage(Email $message) 58 | { 59 | /** @var StorageOptions $storageOptions */ 60 | $storageOptions = $message->_storageOptions; 61 | 62 | $this->filesystem 63 | ->disk($storageOptions->disk) 64 | ->put($storageOptions->fullpath(), $message->toString(), [ 65 | 'mimetype' => $storageOptions::MIME_TYPE, 66 | ]); 67 | 68 | event(new MessageStored($message, $storageOptions)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MailExportServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/mail-export.php', 'mail-export'); 18 | 19 | $this->app->register(EventServiceProvider::class); 20 | } 21 | 22 | /** 23 | * Bootstrap any application services. 24 | * 25 | * @return void 26 | */ 27 | public function boot() 28 | { 29 | if ($this->app->runningInConsole()) { 30 | $this->publishes([ 31 | __DIR__.'/../config/mail-export.php' => config_path('mail-export.php'), 32 | ]); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 18 | ExportMessage::class, 19 | ], 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /src/StorageOptions.php: -------------------------------------------------------------------------------- 1 | message = $message; 50 | 51 | $properties = Arr::only($properties, ['disk', 'path', 'filename']); 52 | 53 | foreach ($properties as $propertyName => $propertyValue) { 54 | $default = 'default'.Str::studly($propertyName); 55 | $this->$propertyName = $propertyValue ?: $this->$default(); 56 | } 57 | } 58 | 59 | /** 60 | * Build the default storage disk using the config if none is provided by the developer. 61 | * 62 | * @return string 63 | */ 64 | private function defaultDisk(): string 65 | { 66 | return config('mail-export.disk') ?: config('filesystems.default'); 67 | } 68 | 69 | /** 70 | * Build the default storage path using the config if none is provided by the developer. 71 | * 72 | * @return string 73 | */ 74 | private function defaultPath(): string 75 | { 76 | return config('mail-export.path'); 77 | } 78 | 79 | /** 80 | * Build some default value for the filename of the message we're about to store 81 | * so this can be used if none is provided by the developer. 82 | * 83 | * @return string 84 | */ 85 | private function defaultFilename(): string 86 | { 87 | /** @var \Symfony\Component\Mime\Address[] */ 88 | $recipients = $this->message->getTo(); 89 | 90 | $to = ! empty($recipients) 91 | ? str_replace(['@', '.'], ['_at_', '_'], $recipients[0]->getAddress()).'_' 92 | : ''; 93 | 94 | $subject = $this->message->getSubject(); 95 | 96 | $timestamp = Carbon::now()->format('Y_m_d_His'); 97 | 98 | return Str::slug("{$timestamp}_{$to}{$subject}", '_'); 99 | } 100 | 101 | /** 102 | * Builds the full path we will store the file onto the disk. 103 | * 104 | * @return string 105 | */ 106 | public function fullpath(): string 107 | { 108 | return "{$this->path}/{$this->filename}.".self::EXTENSION; 109 | } 110 | 111 | /** 112 | * Build some array representation of that data transfer object. 113 | * 114 | * @return array 115 | */ 116 | public function toArray(): array 117 | { 118 | return [ 119 | 'disk' => $this->disk, 120 | 'path' => $this->path, 121 | 'filename' => $this->filename, 122 | 'extension' => self::EXTENSION, 123 | 'fullpath' => $this->fullpath(), 124 | ]; 125 | } 126 | } 127 | --------------------------------------------------------------------------------