Repository: eduardokum/laravel-mail-auto-embed Branch: master Commit: eab2c5ef2f0a Files: 38 Total size: 65.1 KB Directory structure: gitextract_3h0c0u8y/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── format_php.yml │ └── test.yml ├── .gitignore ├── .php_cs.dist ├── LICENSE ├── README.md ├── composer.json ├── config/ │ └── mail-auto-embed.php ├── phpunit.xml ├── src/ │ ├── Contracts/ │ │ ├── Embedder/ │ │ │ ├── EntityEmbedder.php │ │ │ └── UrlEmbedder.php │ │ └── Listeners/ │ │ └── EmbedImages.php │ ├── Embedder/ │ │ ├── AttachmentEmbedder.php │ │ ├── Base64Embedder.php │ │ └── Embedder.php │ ├── Listeners/ │ │ ├── SwiftEmbedImages.php │ │ └── SymfonyEmbedImages.php │ ├── Models/ │ │ └── EmbeddableEntity.php │ └── ServiceProvider.php └── tests/ ├── Embedder/ │ ├── AttachmentEmbedderTest.php │ └── Base64EmbedderTest.php ├── FormatTest.php ├── MailTest.php ├── TestCase.php ├── Traits/ │ └── InteractsWithMessage.php ├── fixtures/ │ ├── PictureEntity.php │ └── WrongEntity.php └── lib/ ├── embed-can-skip.html ├── embed-when-enabled.html ├── formats/ │ ├── html5-custom-embeds.html │ ├── html5-user-generated.html │ └── html5-valid.html ├── graceful-fails.html ├── manual-embed-when-disabled.html ├── override-to-attachment.html ├── override-to-base64.html └── raw-message.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: eduardokum ================================================ FILE: .github/workflows/format_php.yml ================================================ name: Format (PHP) on: pull_request: paths: - "**.php" jobs: php-cs-fixer: runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup PHP with php-cs-fixer uses: shivammathur/setup-php@master with: php-version: '8.0' tools: friendsofphp/php-cs-fixer:^2.19 - name: Run php-cs-fixer run: php-cs-fixer fix - uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: Apply php-cs-fixer changes ================================================ FILE: .github/workflows/test.yml ================================================ name: Run unit tests on: - push - pull_request env: COMPOSER_MEMORY_LIMIT: -1 jobs: test: runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: fail-fast: false matrix: php: [8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2] laravel: ['6.*', '7.*', '8.*', '9.*', '10.*', '11.*', '12.*', '13.*'] os: [ubuntu-latest] include: - laravel: 12.* testbench: 10.* - laravel: 11.* testbench: 9.* - laravel: 10.* testbench: 8.* - laravel: 9.* testbench: 7.* - laravel: 8.* testbench: 6.* - laravel: 7.* testbench: 5.* - laravel: 6.* testbench: 4.* - laravel: 13.* testbench: 11.* exclude: - laravel: 6.* php: 8.1 - laravel: 6.* php: 8.2 - laravel: 6.* php: 8.3 - laravel: 7.* php: 8.1 - laravel: 7.* php: 8.2 - laravel: 7.* php: 8.3 - laravel: 8.* php: 7.2 - laravel: 8.* php: 8.0 - laravel: 8.* php: 8.1 - laravel: 8.* php: 8.2 - laravel: 8.* php: 8.3 - laravel: 9.* php: 7.2 - laravel: 9.* php: 7.3 - laravel: 9.* php: 7.4 - laravel: 10.* php: 7.2 - laravel: 10.* php: 7.3 - laravel: 10.* php: 7.4 - laravel: 10.* php: 8.0 - laravel: 11.* php: 7.2 - laravel: 11.* php: 7.2 - laravel: 11.* php: 7.3 - laravel: 11.* php: 7.4 - laravel: 11.* php: 8.0 - laravel: 11.* php: 8.1 - laravel: 12.* php: 7.2 - laravel: 12.* php: 7.2 - laravel: 12.* php: 7.3 - laravel: 12.* php: 7.4 - laravel: 12.* php: 8.0 - laravel: 12.* php: 8.1 - laravel: 13.* php: 8.2 - laravel: 13.* php: 8.1 - laravel: 13.* php: 8.0 - laravel: 13.* php: 7.4 - laravel: 13.* php: 7.3 - laravel: 13.* php: 7.2 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} env: extensions: exif, json, mbstring, dom steps: - name: Checkout repository uses: actions/checkout@v4 - name: Cache dependencies uses: actions/cache@v4 with: path: ~/.composer/cache/files key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - name: Setup PHP extensions id: cache-env uses: shivammathur/cache-extensions@v1 with: php-version: ${{ matrix.php }} extensions: ${{ env.extensions }} key: php-extensions-cache-v1 - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: ${{ env.extensions }} coverage: none - name: Install dependencies run: composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update - name: Update dependencies run: composer update --prefer-dist --no-interaction - name: Execute tests run: vendor/bin/phpunit ================================================ FILE: .gitignore ================================================ /vendor /composer.lock .idea/ .phpunit.result.cache ================================================ FILE: .php_cs.dist ================================================ ['syntax' => 'short'], 'binary_operator_spaces' => [ 'default' => 'single_space', 'operators' => ['=>' => null], ], 'blank_line_after_namespace' => true, 'blank_line_after_opening_tag' => true, 'blank_line_before_statement' => [ 'statements' => ['return'], ], 'braces' => true, 'cast_spaces' => true, 'class_attributes_separation' => [ 'elements' => ['method'], ], 'class_definition' => true, 'concat_space' => [ 'spacing' => 'none', ], 'declare_equal_normalize' => true, 'elseif' => true, 'encoding' => true, 'full_opening_tag' => true, 'fully_qualified_strict_types' => true, // added by Shift 'function_declaration' => true, 'function_typehint_space' => true, 'heredoc_to_nowdoc' => true, 'include' => true, 'increment_style' => ['style' => 'post'], 'indentation_type' => true, 'linebreak_after_opening_tag' => true, 'line_ending' => true, 'lowercase_cast' => true, 'lowercase_constants' => true, 'lowercase_keywords' => true, 'lowercase_static_reference' => true, // added from Symfony 'magic_method_casing' => true, // added from Symfony 'magic_constant_casing' => true, 'method_argument_space' => true, 'native_function_casing' => true, 'no_alias_functions' => true, 'no_extra_blank_lines' => [ 'tokens' => [ 'extra', 'throw', 'use', 'use_trait', ], ], 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_closing_tag' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, 'no_leading_import_slash' => true, 'no_leading_namespace_whitespace' => true, 'no_mixed_echo_print' => [ 'use' => 'echo', ], 'no_multiline_whitespace_around_double_arrow' => true, 'multiline_whitespace_before_semicolons' => [ 'strategy' => 'no_multi_line', ], 'no_short_bool_cast' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_after_function_name' => true, 'no_spaces_around_offset' => true, 'no_spaces_inside_parenthesis' => true, 'no_trailing_comma_in_list_call' => true, 'no_trailing_comma_in_singleline_array' => true, 'no_trailing_whitespace' => true, 'no_trailing_whitespace_in_comment' => true, 'no_unneeded_control_parentheses' => true, 'no_unreachable_default_argument_value' => true, 'no_useless_return' => true, 'no_whitespace_before_comma_in_array' => true, 'no_whitespace_in_blank_line' => true, 'normalize_index_brace' => true, 'not_operator_with_successor_space' => true, 'object_operator_without_whitespace' => true, 'ordered_imports' => ['sortAlgorithm' => 'alpha'], 'phpdoc_indent' => true, 'phpdoc_inline_tag' => true, 'phpdoc_no_access' => true, 'phpdoc_no_package' => true, 'phpdoc_no_useless_inheritdoc' => true, 'phpdoc_scalar' => true, 'phpdoc_single_line_var_spacing' => true, 'phpdoc_summary' => true, 'phpdoc_to_comment' => true, 'phpdoc_trim' => true, 'phpdoc_types' => true, 'phpdoc_var_without_name' => true, 'psr4' => true, 'self_accessor' => true, 'short_scalar_cast' => true, 'simplified_null_return' => false, // disabled by Shift 'single_blank_line_at_eof' => true, 'single_blank_line_before_namespace' => true, 'single_class_element_per_statement' => true, 'single_import_per_statement' => true, 'single_line_after_imports' => true, 'single_line_comment_style' => [ 'comment_types' => ['hash'], ], 'single_quote' => true, 'space_after_semicolon' => true, 'standardize_not_equals' => true, 'switch_case_semicolon_to_colon' => true, 'switch_case_space' => true, 'ternary_operator_spaces' => true, 'trailing_comma_in_multiline_array' => true, 'trim_array_spaces' => true, 'unary_operator_spaces' => true, 'visibility_required' => [ 'elements' => ['method', 'property'], ], 'whitespace_after_comma_in_array' => true, ]; $finder = Finder::create() ->in([ __DIR__.'/src', __DIR__.'/config', __DIR__.'/tests', ]) ->name('*.php') ->notName('*.blade.php') ->ignoreDotFiles(true) ->ignoreVCS(true); return Config::create() ->setFinder($finder) ->setRules($rules) ->setRiskyAllowed(true) // this is disabled, due to unexpected errors in some environments. Fell free to enable this to fits your needs. ->setUsingCache(false); ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Eduardo Gusmão Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Packagist](https://img.shields.io/packagist/v/eduardokum/laravel-mail-auto-embed.svg?style=flat-square)](https://github.com/eduardokum/laravel-mail-auto-embed) [![Packagist](https://img.shields.io/packagist/dt/eduardokum/laravel-mail-auto-embed.svg?style=flat-square)](https://github.com/eduardokum/laravel-mail-auto-embed) [![Packagist](https://img.shields.io/packagist/l/eduardokum/laravel-mail-auto-embed.svg?style=flat-square)](https://github.com/eduardokum/laravel-mail-auto-embed) [![GitHub Actions](https://img.shields.io/endpoint.svg?style=flat-square&url=https%3A%2F%2Factions-badge.atrox.dev%2Feduardokum%2Flaravel-mail-auto-embed%2Fbadge)](https://actions-badge.atrox.dev/eduardokum/laravel-mail-auto-embed/goto) [![GitHub forks](https://img.shields.io/github/forks/eduardokum/laravel-mail-auto-embed.svg?color=lightgrey&style=flat-square)](https://github.com/eduardokum/laravel-mail-auto-embed) # Laravel Mail Auto Embed Automatically parses your messages and embeds the images found into your mail, replacing the original online-version of the image. Should work on Laravel 5.3+. Automatically tested for Laravel 5.4+ on PHP 7.0+. ## Version Compatibility | Laravel | Package | |---------|---------| | \< 8.x | 1.x | | \> 9.x | 2.x | ## Install You can install the package via composer: ```shell composer require eduardokum/laravel-mail-auto-embed ``` This package uses Laravel 5.5 Package Auto-Discovery. For previous versions of Laravel, you need to add the following Service Provider: ```php $providers = [ ... \Eduardokum\LaravelMailAutoEmbed\ServiceProvider::class, ... ]; ``` ## Usage Its use is very simple, you write your markdown normally: ```markdown @component('mail::message') # Order Shipped Your order has been shipped! @component('mail::button', ['url' => $url]) View Order @endcomponent Purchased product: ![product](https://domain.com/products/product-1.png) Thanks,
{{ config('app.name') }} @endcomponent ``` When sending, it will replace the link that would normally be generated: > `` by an embedded inline attachment of the image: > ``. It works for raw html too: ```html ``` If you do not want to use automatic embedding for specific images (because they are hosted elsewhere, if you want to use some kind of image tracker, etc.), simply add the attribute `data-skip-embed` in the image tag: ```html ``` ### Local resources For local resources that are not available publicly, use `file://` urls: ```html Logo ``` ## Configuration The defaults are set in `config/mail-auto-embed.php`. You can copy this file to your own config directory to modify the values using this command: ```shell php artisan vendor:publish --provider="Eduardokum\LaravelMailAutoEmbed\ServiceProvider" ``` ### Explicit embedding configuration By default, images are embedded automatically, unless you add the `data-skip-embed` attribute. You can also disable auto-embedding globally by setting the `MAIL_AUTO_EMBED` environment variable to `false`, or by modifying the `enabled` property in the published config. You can then enable embedding for individual images with the `data-auto-embed` attribute. ```env # .env MAIL_AUTO_EMBED=false ``` ```php return [ /* |-------------------------------------------------------------------------- | Mail auto embed |-------------------------------------------------------------------------- | | If true, images will be automatically embedded. | If false, only images with the 'data-auto-embed' attribute will be embedded | */ 'enabled' => false, // … ]; ``` ```html

``` ### Base64 embedding If you prefer to use Base64 instead of inline attachments, you can do so by setting the `MAIL_AUTO_EMBED_METHOD` environment variable or the `method` config property to `base64`. ```php return [ // … /* |-------------------------------------------------------------------------- | Mail embed method |-------------------------------------------------------------------------- | | Supported: "attachment", "base64" | */ 'method' => 'base64', ]; ``` Note that it will increase the e-mail size, and that it won't be decoded by some e-mail clients such as Gmail. ## Mixed embedding methods If you want to use both inline attachment and Base64 depending on the image, you can specify the embedding method as the `data-auto-embed` attribute value: ```html

``` ## Embedding entities You might want to embed images that don't actually exist in your filesystem (stored in the database). In that case, make the entities you want to embed implement the `EmbeddableEntity` interface: ```php namespace App\Models; use Eduardokum\LaravelMailAutoEmbed\Models\EmbeddableEntity; use Illuminate\Database\Eloquent\Model; class Picture extends Model implements EmbeddableEntity { /** * @param mixed $id * @return Picture */ public static function findEmbeddable($id) { return static::find($id); } /** * @return mixed */ public function getRawContent() { return $this->data; } /** * @return string */ public function getFileName() { return 'profile_'.$this->id.'.png'; } /** * @return string */ public function getMimeType() { return 'image/png'; } } ``` Then, you can use the `embed:ClassName:id` syntax in your e-mail template: ```html

``` ## Contributing Please feel free to submit pull requests if you can improve or add any features. We are currently using PSR-2. This is easy to implement and check with the PHP Coding Standards Fixer. Donate with Paypal ================================================ FILE: composer.json ================================================ { "name": "eduardokum/laravel-mail-auto-embed", "description": "Library for embed images in emails automatically", "keywords": [ "eduardokum", "laravel-mail-auto-embed" ], "homepage": "https://github.com/eduardokum/laravel-mail-auto-embed", "license": "MIT", "authors": [ { "name": "Eduardo Gusmão", "email": "eduguscontra3@hotmail.com" } ], "require": { "php": "^7.2|^8.0|^8.1|^8.2", "ext-dom": "*", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "illuminate/mail": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "masterminds/html5": "^2.7", "ext-curl": "*", "ext-fileinfo": "*" }, "require-dev": { "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "phpunit/phpunit": "^8.5.30|^9.0|^10.0|^11.0|^12.5.12", "squizlabs/php_codesniffer": "^3.5|^4.0" }, "config": { "sort-packages": true }, "autoload": { "psr-4": { "Eduardokum\\LaravelMailAutoEmbed\\": "src" } }, "autoload-dev": { "psr-4": { "Eduardokum\\LaravelMailAutoEmbed\\Tests\\": "tests" } }, "extra": { "laravel": { "providers": [ "Eduardokum\\LaravelMailAutoEmbed\\ServiceProvider" ] } }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: config/mail-auto-embed.php ================================================ env('MAIL_AUTO_EMBED', true), /* |-------------------------------------------------------------------------- | Mail embed method |-------------------------------------------------------------------------- | | Supported: "attachment", "base64" | */ 'method' => env('MAIL_AUTO_EMBED_METHOD', 'attachment'), 'curl' => [ 'connect_timeout' => 5, // Seconds 'timeout' => 10, // Seconds 'cache' => false, 'cache_ttl' => 3600, ] ]; ================================================ FILE: phpunit.xml ================================================ ./tests ./src ================================================ FILE: src/Contracts/Embedder/EntityEmbedder.php ================================================ isLaravel9()) { throw new Exception('Laravel 9 and greater must use symfony mailer'); } $this->swiftMessage = $message; return $this; } /** * @param Email $message * * @return AttachmentEmbedder * @throws Exception */ public function setSymfonyMessage(Email $message) { if (!$this->isLaravel9()) { throw new Exception('Laravel 8 and below must use swift mailer'); } $this->symfonyMessage = $message; return $this; } /** * @param string $url * * @throws Exception */ public function fromUrl($url) { $localFile = str_replace(url('/'), public_path(), $url); if (file_exists($localFile)) { return $this->fromPath($localFile); } if ($embeddedFromRemoteUrl = $this->fromRemoteUrl($url)) { return $embeddedFromRemoteUrl; } return $url; } /** * @param $path * * @return string * @throws Exception */ public function fromPath($path) { return $this->embed(file_get_contents($path), basename($path), mime_content_type($path)); } /** * @param string $base64 * * @return string * @throws Exception */ public function fromBase64($base64) { $data = explode(',', $base64); $type = explode(';', explode(':', $data[0])[1])[0]; $content = base64_decode($data[1]); $name = Str::random(); return $this->embed($content, $name, $type); } /** * @param EmbeddableEntity $entity * * @return string * @throws Exception */ public function fromEntity(EmbeddableEntity $entity) { return $this->embed($entity->getRawContent(), $entity->getFileName(), $entity->getMimeType()); } /** * @param string $url * * @throws Exception */ public function fromRemoteUrl($url) { if (filter_var($url, FILTER_VALIDATE_URL)) { $hashName = implode('_', [ 'laravel-mail-auto-embed', hash('sha256', $url) ]); if (config('mail-auto-embed.curl.cache', false) && $file = Cache::get($hashName)) { return $this->embed($file['content'], $file['name'], $file['type']); } $ch = curl_init($url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $raw = curl_exec($ch); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); if ($httpcode == 200) { $pathInfo = pathinfo($url); $queryStr = parse_url($url, PHP_URL_QUERY) ?: ''; parse_str($queryStr ?? '', $queryParams); $basename = $queryParams['basename'] ?? $pathInfo['basename']; if (config('mail-auto-embed.curl.cache', false)) { Cache::put($hashName, [ 'content' => $raw, 'name' => $basename, 'type' => $contentType ], config('mail-auto-embed.curl.cache_ttl', 3600)); } return $this->embed($raw, $basename, $contentType); } } return $url; } /** * @param $body * @param $name * @param $type * * @return string * @throws Exception */ protected function embed($body, $name, $type) { if ($this->isLaravel9() && !empty($this->symfonyMessage)) { if (gettype($name) !== 'string') { $name = Str::random(); } $this->symfonyMessage->embed($body, $name, $type); return "cid:$name"; } if (!$this->isLaravel9() && !empty($this->swiftMessage)) { return $this->swiftMessage->embed( new Swift_EmbeddedFile( $body, $name, $type ) ); } throw new Exception('No message defined'); } /** * @return bool */ private function isLaravel9() { return version_compare(Application::VERSION, '9.0.0', '>='); } } ================================================ FILE: src/Embedder/Base64Embedder.php ================================================ config = $config; } /** * @param string $url * @return string */ public function fromUrl($url) { $localFile = str_replace(url('/'), public_path('/'), $url); if (file_exists($localFile)) { return $this->fromPath($localFile); } if ($embeddedFromRemoteUrl = $this->fromRemoteUrl($url)) { return $embeddedFromRemoteUrl; } return $url; } /** * @param $path * * @return string */ public function fromPath($path) { if (file_exists($path)) { return $this->base64String(mime_content_type($path), file_get_contents($path)); } return $path; } /** * @param EmbeddableEntity $entity * @return string */ public function fromEntity(EmbeddableEntity $entity) { return $this->base64String($entity->getMimeType(), $entity->getRawContent()); } /** * @param string $url */ public function fromRemoteUrl($url) { if (filter_var($url, FILTER_VALIDATE_URL)) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, Arr::get($this->config, 'curl.connect_timeout', 5)); curl_setopt($ch, CURLOPT_TIMEOUT, Arr::get($this->config, 'curl.timeout', 10)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $raw = curl_exec($ch); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); if ($httpcode == 200) { return $this->base64String($contentType, $raw); } } return $url; } /** * @param string $base64 * @return string */ public function fromBase64($base64) { return $base64; } /** * @param string $mimeType * @param mixed $content */ private function base64String($mimeType, $content) { return 'data:'.$mimeType.';base64,'.base64_encode($content); } } ================================================ FILE: src/Embedder/Embedder.php ================================================ config = $config; } /** * @param Swift_Events_SendEvent $evt */ public function beforeSendPerformed(Swift_Events_SendEvent $evt) { $this->handle($evt->getMessage()); } /** * @param Swift_Mime_SimpleMessage $message * @return void */ public function handle(Swift_Mime_SimpleMessage $message) { $this->message = $message; $this->attachImages(); } /** * @param Swift_Events_SendEvent $evt * @return bool */ public function sendPerformed(Swift_Events_SendEvent $evt) { return true; } /** * */ private function attachImages() { // Get body $body = $this->message->getBody(); // Parse document $parser = new HTML5(); $document = $parser->loadHTML($body); if (! $document) { // Cannot read return; } // Invalid HTML (raw message) if ($this->shouldSkipDocument($document)) { return; } // Add images $this->attachImagesToDom($document); // Replace body $this->message->setBody($parser->saveHTML($document)); // $html_body = $this->message->getBody(); // /* $html_body = preg_replace_callback('//', [$this, 'replaceCallback'], $html_body);*/ // // $this->message->setBody($html_body); } /** * @param DOMDocument $document * @return bool */ private function shouldSkipDocument(DOMDocument $document) { if ($document->childNodes->count() != 1) { return false; } if ($document->childNodes->item(0)->nodeType == XML_DOCUMENT_TYPE_NODE) { return true; } return false; } /** * @param DOMDocument $document */ private function attachImagesToDom(DOMDocument &$document) { foreach ($document->getElementsByTagName('img') as $image) { \assert($image instanceof DOMElement); // Skip if embed is not required if ($this->needsEmbed($image)) { // Get proper embedder $embedder = $this->getEmbedder($image); // Update src $image->setAttribute('src', $this->embed( $embedder, $image->getAttribute('src') )); } // Remove data properties $image->removeAttribute('data-skip-embed'); $image->removeAttribute('data-auto-embed'); } } /** * @param DOMElement $imageTag * @return bool */ private function needsEmbed(DOMElement $imageTag) { // Don't embed if 'data-skip-embed' is present if ($imageTag->hasAttribute('data-skip-embed')) { return false; } // Don't embed if auto-embed is disabled and 'data-auto-embed' is absent if (! $this->config['enabled'] && ! $imageTag->hasAttribute('data-auto-embed')) { return false; } return true; } /** * @param DOMElement $imageTag * * @return Embedder * @throws Exception */ private function getEmbedder(DOMElement $imageTag) { $method = $imageTag->getAttribute('data-auto-embed'); if (empty($method)) { $method = $this->config['method']; } switch ($method) { case 'attachment': default: return (new AttachmentEmbedder()) ->setSwiftMessage($this->message); case 'base64': return new Base64Embedder($this->config); } } /** * @param Embedder $embedder * @param string $src * @return string */ private function embed(Embedder $embedder, $src) { // Entity embedding if (strpos($src, 'embed:') === 0) { $embedParams = explode(':', $src); if (count($embedParams) < 3) { return $src; } $className = urldecode($embedParams[1]); $id = $embedParams[2]; if (!class_exists($className)) { return $src; } $class = new ReflectionClass($className); if (! $class->implementsInterface(EmbeddableEntity::class) ) { return $src; } /** @var EmbeddableEntity $className */ if (! $instance = $className::findEmbeddable($id)) { return $src; } return $embedder->fromEntity($instance); } // URL embedding if (filter_var($src, FILTER_VALIDATE_URL) !== false) { return $embedder->fromUrl($src); } $appPath = method_exists(app(), 'path') ? app_path($src) : null; $publicPath = app()->bound('path.public') ? public_path($src) : null; $storagePath = app()->bound('path.storage') ? storage_path($src) : null; $storageAppPath = app()->bound('path.storage') ? storage_path("app/$src") : null; if (file_exists($src)) { return $embedder->fromPath($src); } elseif ($publicPath && file_exists($publicPath)) { // Try to guess where the file is at that priority level return $embedder->fromPath($publicPath); } elseif ($appPath && file_exists($appPath)) { return $embedder->fromPath($appPath); } elseif ($storagePath && file_exists($storagePath)) { return $embedder->fromPath($storagePath); } elseif ($storageAppPath && file_exists($storageAppPath)) { return $embedder->fromPath($storageAppPath); } return $src; } } ================================================ FILE: src/Listeners/SymfonyEmbedImages.php ================================================ config = $config; } /** * @param MessageSending $event */ public function beforeSendPerformed(MessageSending $event) { $this->handle($event->message); } /** * @param Email $message * @return void */ public function handle(Email $message) { $this->message = $message; $this->attachImages(); } /** * Attaches images by parsing the HTML document. */ private function attachImages() { // Get body $body = $this->message->getHtmlBody(); if ($body === null) { // Not an HTML message return; } // Parse document $parser = new HTML5(); $document = $parser->loadHTML($body); if (! $document) { // Cannot read return; } // Add images $this->attachImagesToDom($document); // Replace body $this->message->html($parser->saveHTML($document)); } /** * @param DOMDocument $document * * @return void * @throws Exception */ private function attachImagesToDom(DOMDocument &$document) { foreach ($document->getElementsByTagName('img') as $image) { \assert($image instanceof DOMElement); // Skip if embed is not required if ($this->needsEmbed($image)) { // Get proper embedder $embedder = $this->getEmbedder($image); // Update src $image->setAttribute('src', $this->embed( $embedder, $image->getAttribute('src') )); } // Remove data properties $image->removeAttribute('data-skip-embed'); $image->removeAttribute('data-auto-embed'); } } /** * @param DOMElement $imageTag * * @return bool */ private function needsEmbed(DOMElement $imageTag) { // Don't embed if 'data-skip-embed' is present if ($imageTag->hasAttribute('data-skip-embed')) { return false; } // Don't embed if auto-embed is disabled and 'data-auto-embed' is absent if (! $this->config['enabled'] && ! $imageTag->hasAttribute('data-auto-embed')) { return false; } return true; } /** * @param DOMElement $imageTag * * @return Embedder * @throws Exception */ private function getEmbedder(DOMElement $imageTag) { $method = $imageTag->getAttribute('data-auto-embed'); if (empty($method)) { $method = $this->config['method']; } switch ($method) { case 'attachment': default: return (new AttachmentEmbedder()) ->setSymfonyMessage($this->message); case 'base64': return new Base64Embedder($this->config); } } /** * @param Embedder $embedder * @param string $src * @return string */ private function embed(Embedder $embedder, $src) { // Entity embedding if (strpos($src, 'embed:') === 0) { $embedParams = explode(':', $src); if (count($embedParams) < 3) { return $src; } $className = urldecode($embedParams[1]); $id = $embedParams[2]; if (! class_exists($className)) { return $src; } $class = new ReflectionClass($className); if (! $class->implementsInterface(EmbeddableEntity::class)) { return $src; } /** @var EmbeddableEntity $className */ if (! $instance = $className::findEmbeddable($id)) { return $src; } return $embedder->fromEntity($instance); } // URL embedding if (filter_var($src, FILTER_VALIDATE_URL) !== false) { return $embedder->fromUrl($src); } // Base64 embedding if (preg_match('/^data:image\/[a-z]+;base64,/', $src)) { return $embedder->fromBase64($src); } $appPath = method_exists(app(), 'path') ? app_path($src) : null; $publicPath = app()->bound('path.public') ? public_path($src) : null; $storagePath = app()->bound('path.storage') ? storage_path($src) : null; $storageAppPath = app()->bound('path.storage') ? storage_path("app/$src") : null; if (file_exists($src)) { return $embedder->fromPath($src); } elseif ($publicPath && file_exists($publicPath)) { // Try to guess where the file is at that priority level return $embedder->fromPath($publicPath); } elseif ($appPath && file_exists($appPath)) { return $embedder->fromPath($appPath); } elseif ($storagePath && file_exists($storagePath)) { return $embedder->fromPath($storagePath); } elseif ($storageAppPath && file_exists($storageAppPath)) { return $embedder->fromPath($storageAppPath); } return $src; } } ================================================ FILE: src/Models/EmbeddableEntity.php ================================================ publishes([$this->getConfigPath() => config_path('mail-auto-embed.php')], 'config'); $this->app->singleton(EmbedImages::class, function($app) { if (version_compare(Application::VERSION, '9.0.0', '>=')) { return new SymfonyEmbedImages($app['config']->get('mail-auto-embed')); } return new SwiftEmbedImages($app['config']->get('mail-auto-embed')); }); if (version_compare(Application::VERSION, '9.0.0', '>=')) { Event::listen(function (MessageSending $event) { $this->app->make(EmbedImages::class)->beforeSendPerformed($event); }); } else { foreach (Arr::get($this->app['config'], 'mail.mailers', []) as $driver => $mailer) { try { // If transport not exists this will throw an exception Mail::driver($driver)->getSwiftMailer()->registerPlugin($this->app->make(EmbedImages::class)); } catch (Throwable $e) {} } } } /** * Register the application services. * * @return void */ public function register() { $this->mergeConfigFrom($this->getConfigPath(), 'mail-auto-embed'); } /** * @return string */ protected function getConfigPath() { return __DIR__.'/../config/mail-auto-embed.php'; } } ================================================ FILE: tests/Embedder/AttachmentEmbedderTest.php ================================================ assertTrue(true); } // /** @var Swift_Message */ // private $message; // // /** @var AttachmentEmbedder */ // private $embedder; // // /** // * @before // * @return void // */ // protected function setUpEmbedder() // { // $this->message = new Swift_Message(); // $this->embedder = new AttachmentEmbedder($this->message); // } // // /** // * @return int // */ // private function getEmbeddedFilesCount() // { // return collect($this->message->getChildren()) // ->filter( // function ($item) { // return $item instanceof Swift_EmbeddedFile; // } // ) // ->count(); // } // // /** // * @test // */ // public function testLocalConversion() // { // $result = $this->embedder->fromUrl('http://localhost/test.png'); // // $this->assertStringStartsWith('cid:', $result); // // $this->assertEquals(1, $this->getEmbeddedFilesCount()); // } // // /** // * @test // */ // public function testEntityConversion() // { // $picture = new PictureEntity(); // // $result = $this->embedder->fromEntity($picture); // // $this->assertStringStartsWith('cid:', $result); // // $this->assertEquals(1, $this->getEmbeddedFilesCount()); // } // // /** // * @test // */ // public function testRemoteUrl() // { // $result = $this->embedder->fromRemoteUrl('https://via.placeholder.com/1'); // // $this->assertStringStartsWith('cid:', $result); // // $this->assertEquals(1, $this->getEmbeddedFilesCount()); // } } ================================================ FILE: tests/Embedder/Base64EmbedderTest.php ================================================ assertTrue(true); } // /** // * @before // * @return void // */ // protected function setUpEmbedder(): void // { // $this->embedder = new Base64Embedder(); // } // // /** // * @test // */ // public function testLocalConversion() // { // $embedder = new Base64Embedder(); // // $result = $embedder->fromUrl('http://localhost/test.png'); // // $this->assertStringStartsWith('data:image/png;base64,', $result); // } // // /** // * @test // */ // public function testEntityConversion() // { // $embedder = new Base64Embedder(); // // $picture = new PictureEntity(); // // $result = $embedder->fromEntity($picture); // // $this->assertStringStartsWith('data:image/png;base64,', $result); // } // // /** // * @test // */ // public function testRemoteUrl() // { // $result = $this->embedder->fromRemoteUrl('https://via.placeholder.com/1.png'); // // $this->assertStringStartsWith('data:image/png;base64,', $result); // } } ================================================ FILE: tests/FormatTest.php ================================================ true, 'method' => 'attachment', ]; /** * @test */ public function testValidHtml5Message() { $message = $this->handleBeforeSendPerformedEvent('formats/html5-valid.html', self::HANDLE_CONFIG); $this->assertEmailImageTags([ 'url' => 'cid:', 'entity' => 'cid:', ], $message); } /** * @test */ public function testUserGeneratedHtml5Message() { $message = $this->handleBeforeSendPerformedEvent('formats/html5-user-generated.html', self::HANDLE_CONFIG); $this->assertEmailImageTags([ 'url' => 'cid:', 'entity' => 'cid:', ], $message); } } ================================================ FILE: tests/MailTest.php ================================================ handleBeforeSendPerformedEvent('embed-when-enabled.html', [ 'enabled' => true, 'method' => 'attachment', ]); $this->assertEmailImageTags([ 'url' => 'cid:', 'entity' => 'cid:', ], $message); } /** * @test */ public function testSkippedConversions() { $message = $this->handleBeforeSendPerformedEvent('embed-can-skip.html', [ 'enabled' => true, 'method' => 'attachment', ]); $this->assertEmailImageTags([ 'embed' => 'cid:', 'skip' => 'http://localhost/test.png', ], $message); } /** * @test */ public function testManualConversions() { $message = $this->handleBeforeSendPerformedEvent('manual-embed-when-disabled.html', [ 'enabled' => false, 'method' => 'attachment', ]); $this->assertEmailImageTags([ 'ignore' => 'http://localhost/test.png', 'embed' => 'cid:', ], $message); } /** * @test */ public function testOverrideTypeBase64() { $message = $this->handleBeforeSendPerformedEvent('override-to-base64.html', [ 'enabled' => true, 'method' => 'attachment', ]); $this->assertEmailImageTags([ 'attachment' => 'cid:', 'base64' => 'data:image/png;base64,', ], $message); } /** * @test */ public function testOverrideTypeAttachment() { $message = $this->handleBeforeSendPerformedEvent('override-to-attachment.html', [ 'enabled' => true, 'method' => 'base64', ]); $this->assertEmailImageTags([ 'attachment' => 'cid:', 'base64' => 'data:image/png;base64,', ], $message); } /** * @test */ public function testGracefulFailureWithAttachments() { $message = $this->handleBeforeSendPerformedEvent('graceful-fails.html', [ 'enabled' => true, 'method' => 'attachment', ]); $this->assertEmailImageTags([ 'host' => 'http://example.com/test.png', 'image' => 'http://localhost/other.png', 'source' => 'whatever', 'syntax' => 'embed:whatever', 'class' => 'embed:WrongEntityClassName:1', 'implementation' => 'embed:Eduardokum\\LaravelMailAutoEmbed\\Tests\\fixtures\\WrongEntity:1', 'not found' => 'embed:Eduardokum\\LaravelMailAutoEmbed\\Tests\\fixtures\\PictureEntity:9', ], $message); } /** * @test */ public function testGracefulFailureWithBase64() { $message = $this->handleBeforeSendPerformedEvent('graceful-fails.html', [ 'enabled' => true, 'method' => 'base64', ]); $this->assertEmailImageTags([ 'host' => 'http://example.com/test.png', 'image' => 'http://localhost/other.png', 'source' => 'whatever', 'syntax' => 'embed:whatever', 'class' => 'embed:WrongEntityClassName:1', 'implementation' => 'embed:Eduardokum\\LaravelMailAutoEmbed\\Tests\\fixtures\\WrongEntity:1', 'not found' => 'embed:Eduardokum\\LaravelMailAutoEmbed\\Tests\\fixtures\\PictureEntity:9', ], $message); } /** * @test */ public function testDoesntHandleSendPerformedEvent() { if ($this->isLaravel9()) { $this->assertTrue(true); } else { $message = $this->createMessage('

Test

'); $embedPlugin = new SwiftEmbedImages(['enabled' => true, 'method' => 'attachment']); $this->assertTrue( $embedPlugin->sendPerformed($this->createSwiftEvent($message)) ); } } /** * @test */ public function testDoesntTransformRawMessages() { $message = $this->handleBeforeSendPerformedEvent('raw-message.txt', [ 'enabled' => true, 'method' => 'attachment', ]); $this->assertEquals( $this->getLibraryFile('raw-message.txt'), ($this->isLaravel9() ? $message->getTextBody() : $message->getBody()) ); } /** * @test */ public function testDoesNotCreateHtmlBodyForSymfonyRawMessage() { if (! $this->isLaravel9()) { $this->assertTrue(true); return; } $message = $this->handleBeforeSendPerformedEvent( 'raw-message.txt', ['enabled' => true, 'method' => 'attachment'], true ); $this->assertNull($message->getHtmlBody()); } } ================================================ FILE: tests/TestCase.php ================================================ usePublicPath($path); } } /** * @param \Illuminate\Foundation\Application $app * @return array */ protected function getPackageProviders($app) { return [\Eduardokum\LaravelMailAutoEmbed\ServiceProvider::class]; } /** * Returns a library file. * @param string $name * @return string */ protected function getLibraryFile($name) { $path = __DIR__.'/lib/'.$name; if (! \file_exists($path) || ! \is_file($path)) { $this->fail("Cannot find {$name} in file library"); } return \file_get_contents($path); } } ================================================ FILE: tests/Traits/InteractsWithMessage.php ================================================ createApplication()->version(), '9.0.0', '>='); } /** * @param string $htmlMessage * @param bool $isRawMessage * * @return Email|Swift_Message */ protected function createMessage($htmlMessage, $isRawMessage = false) { if ($this->isLaravel9()) { return (new Email())->to('test@test.com')->from('sender@test.com')->subject('test') ->text($htmlMessage) ->html($isRawMessage ? null : $htmlMessage); } else { return new Swift_Message('test', $htmlMessage, $isRawMessage ? 'text/plain' : null); } } /** * @param Swift_Message $message * @return \Swift_Events_SendEvent */ protected function createSwiftEvent(Swift_Message $message) { $dispatcher = new \Swift_Events_SimpleEventDispatcher(); $transport = new \Swift_Transport_NullTransport($dispatcher); $event = new \Swift_Events_SendEvent($transport, $message); return $event; } /** * @param string $libraryFile * @param array $options * @param bool $isRawMessage * @return Swift_Message|Email */ protected function handleBeforeSendPerformedEvent($libraryFile, $options, $isRawMessage = false) { $htmlMessage = $this->getLibraryFile($libraryFile); $message = $this->createMessage($htmlMessage, $isRawMessage); if ($this->isLaravel9()) { $event = new MessageSending($message); (new SymfonyEmbedImages($options)) ->beforeSendPerformed($event); $event->message->getBody(); return $event->message; } $embedPlugin = new SwiftEmbedImages($options); $embedPlugin->beforeSendPerformed($this->createSwiftEvent($message)); return $message; } /** * Check the body for image tags with the given keys as comment preceding them. * @param array $expectations * @param string $body * @return void */ protected function assertEmailImageTags($expectations, $body) { foreach ($expectations as $comment => $src) { // Fix for PHPUnit <8.0 // phpcs:ignore Generic.Files.LineLength.TooLong $method = \method_exists($this, 'assertStringContainsString') ? 'assertStringContainsString' : 'assertContains'; // Check if the string is contained within the string $this->$method( sprintf(' ================================================ FILE: tests/lib/embed-when-enabled.html ================================================ ================================================ FILE: tests/lib/formats/html5-custom-embeds.html ================================================

Lorem ipsum dolor sit amet consectetur adipisicing elit. Nulla, dolorum assumenda aliquam blanditiis, necessitatibus mollitia delectus sapiente amet earum minima qui non deserunt quidem, doloremque architecto voluptatem eveniet illo aperiam.

Lorem ipsum

================================================ FILE: tests/lib/formats/html5-user-generated.html ================================================

Lorem Ipsum

Lorem ipsum dolor sit amet consectetur adipisicing elit. Nulla, dolorum assumenda aliquam blanditiis, necessitatibus mollitia delectus sapiente amet earum minima qui non deserunt quidem, doloremque architecto voluptatem eveniet illo aperiam.

Superheros and sidekicks
Batman Robin The Flash Kid Flash
Skill Smarts Dex, acrobat Super speed Super speed

Lorem ipsum

================================================ FILE: tests/lib/formats/html5-valid.html ================================================

Lorem Ipsum

Lorem ipsum dolor sit amet consectetur adipisicing elit. Nulla, dolorum assumenda aliquam blanditiis, necessitatibus mollitia delectus sapiente amet earum minima qui non deserunt quidem, doloremque architecto voluptatem eveniet illo aperiam.

Lorem ipsum
================================================ FILE: tests/lib/graceful-fails.html ================================================ ================================================ FILE: tests/lib/manual-embed-when-disabled.html ================================================ ================================================ FILE: tests/lib/override-to-attachment.html ================================================ ================================================ FILE: tests/lib/override-to-base64.html ================================================ ================================================ FILE: tests/lib/raw-message.txt ================================================ The is a raw message that should be skipped. It doesn't contain any images.