├── LICENSE.txt ├── README.md ├── composer.json ├── config └── css-inliner.php └── src ├── CssInlinerPlugin.php └── LaravelMailCssInlinerServiceProvider.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Federico Isas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel Mail CSS Inliner 2 | ======================== 3 | 4 | [![CI status](https://github.com/fedeisas/laravel-mail-css-inliner/actions/workflows/main.yml/badge.svg)](https://github.com/fedeisas/laravel-mail-css-inliner/actions) 5 | [![Dependabot Status](https://img.shields.io/badge/dependabot-active-brightgreen?logo=dependabot)](https://dependabot.com) 6 | [![Latest Stable Version](https://poser.pugx.org/fedeisas/laravel-mail-css-inliner/v)](https://packagist.org/packages/fedeisas/laravel-mail-css-inliner) 7 | [![Latest Unstable Version](https://poser.pugx.org/fedeisas/laravel-mail-css-inliner/v/unstable)](https://packagist.org/packages/fedeisas/laravel-mail-css-inliner) 8 | [![Total Downloads](https://poser.pugx.org/fedeisas/laravel-mail-css-inliner/downloads)](https://packagist.org/packages/fedeisas/laravel-mail-css-inliner) 9 | [![License](https://poser.pugx.org/fedeisas/laravel-mail-css-inliner/license)](https://packagist.org/packages/fedeisas/laravel-mail-css-inliner) 10 | 11 | ## Why? 12 | Most email clients won't render CSS (on a `` or a ` 37 | 38 | 39 |

Hey you

40 | 41 | 42 | ``` 43 | Or the link tag: 44 | ```html 45 | 46 | 47 | 48 | 49 | 50 |

Hey you

51 | 52 | 53 | ``` 54 | 55 | Into this: 56 | ```html 57 | 58 | 59 | 65 | 66 | 67 |

Hey you

68 | 69 | 70 | ``` 71 | 72 | ## Installation 73 | This package needs Laravel 9.x. 74 | 75 | Begin by installing this package through Composer. Require it directly from the Terminal to take the last stable version: 76 | ```bash 77 | composer require fedeisas/laravel-mail-css-inliner 78 | ``` 79 | 80 | At this point the inliner should be already working with the default options. If you want to fine-tune these options, you can do so by publishing the configuration file: 81 | ```bash 82 | php artisan vendor:publish --provider='Fedeisas\LaravelMailCssInliner\LaravelMailCssInlinerServiceProvider' 83 | ``` 84 | and changing the settings on the generated `config/css-inliner.php` file. 85 | 86 | ## Contributing 87 | - Install project dependencies: 88 | ```bash 89 | composer install 90 | ``` 91 | 92 | - Execute tests with the following command: 93 | ```bash 94 | ./vendor/bin/phpunit 95 | ``` 96 | 97 | ## Found a bug? 98 | Please, let me know! Send a pull request or a patch. Questions? Ask! I will respond to all filed issues. 99 | 100 | ## Inspiration 101 | This package is greatly inspired, and mostly copied, from [SwiftMailer CSS Inliner](https://github.com/OpenBuildings/swiftmailer-css-inliner). I just made an easy drop-in solution for Laravel. 102 | 103 | ## License 104 | This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fedeisas/laravel-mail-css-inliner", 3 | "description": "Inline the CSS of your HTML emails using Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "mailer", 7 | "css" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Fede Isas", 13 | "email": "fedeisas@hotmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.0.2", 18 | "ext-dom": "*", 19 | "illuminate/mail": "^9.0 || ^10.0 || ^11.0 || ^12.0", 20 | "illuminate/support": "^9.0 || ^10.0 || ^11.0 || ^12.0", 21 | "paragonie/random_compat": "~2.0 || ~9.99", 22 | "tijsverkoyen/css-to-inline-styles": "~2.2" 23 | }, 24 | "require-dev": { 25 | "enlightn/security-checker": "^1.10 || ^2.0", 26 | "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", 27 | "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", 28 | "phpunit/phpunit": "^9.0", 29 | "symfony/mailer": "^6.0 || ^7.0" 30 | }, 31 | "config": { 32 | "sort-packages": true 33 | }, 34 | "extra": { 35 | "laravel": { 36 | "providers": [ 37 | "Fedeisas\\LaravelMailCssInliner\\LaravelMailCssInlinerServiceProvider" 38 | ] 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Fedeisas\\LaravelMailCssInliner\\": "src/" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Tests\\": "tests/" 49 | } 50 | }, 51 | "scripts": { 52 | "security-checker": "security-checker security:check --no-dev" 53 | }, 54 | "scripts-descriptions": { 55 | "security-checker": "Checks if your application uses dependencies with known security vulnerabilities." 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /config/css-inliner.php: -------------------------------------------------------------------------------- 1 | [], 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /src/CssInlinerPlugin.php: -------------------------------------------------------------------------------- 1 | cssToAlwaysInclude = $this->loadCssFromFiles($filesToInline); 25 | 26 | $this->converter = $converter ?? new CssToInlineStyles; 27 | } 28 | 29 | public function handle(MessageSending $event): void 30 | { 31 | $message = $event->message; 32 | 33 | if (!$message instanceof Email) { 34 | return; 35 | } 36 | 37 | $this->handleSymfonyEmail($message); 38 | } 39 | 40 | public function handleSymfonyEvent(MessageEvent $event): void 41 | { 42 | $message = $event->getMessage(); 43 | 44 | if (!$message instanceof Email) { 45 | return; 46 | } 47 | 48 | $this->handleSymfonyEmail($message); 49 | } 50 | 51 | private function processPart(AbstractPart $part): AbstractPart 52 | { 53 | if ($part instanceof TextPart && $part->getMediaType() === 'text' && $part->getMediaSubtype() === 'html') { 54 | return $this->processHtmlTextPart($part); 55 | } else if ($part instanceof AbstractMultipartPart) { 56 | $part_class = get_class($part); 57 | $parts = []; 58 | 59 | foreach ($part->getParts() as $childPart) { 60 | $parts[] = $this->processPart($childPart); 61 | } 62 | 63 | return new $part_class(...$parts); 64 | } 65 | 66 | return $part; 67 | } 68 | 69 | private function loadCssFromFiles(array $cssFiles): string 70 | { 71 | $css = ''; 72 | 73 | foreach ($cssFiles as $file) { 74 | $css .= file_get_contents($file); 75 | } 76 | 77 | return $css; 78 | } 79 | 80 | private function processHtmlTextPart(TextPart $part): TextPart 81 | { 82 | [$cssFiles, $bodyString] = $this->extractCssFilesFromMailBody($part->getBody()); 83 | 84 | $bodyString = $this->converter->convert($bodyString, $this->cssToAlwaysInclude . "\n" . $this->loadCssFromFiles($cssFiles)); 85 | 86 | return new TextPart($bodyString, $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8', 'html'); 87 | } 88 | 89 | private function handleSymfonyEmail(Email $message): void 90 | { 91 | $body = $message->getBody(); 92 | 93 | if ($body === null) { 94 | return; 95 | } 96 | 97 | if ($body instanceof TextPart) { 98 | $message->setBody($this->processPart($body)); 99 | } elseif ($body instanceof AbstractMultipartPart) { 100 | $part_type = get_class($body); 101 | $message->setBody(new $part_type( 102 | ...array_map( 103 | fn (AbstractPart $part) => $this->processPart($part), 104 | $body->getParts() 105 | ) 106 | )); 107 | } 108 | } 109 | 110 | private function extractCssFilesFromMailBody(string $message): array 111 | { 112 | $document = new DOMDocument; 113 | 114 | $previousUseInternalErrors = libxml_use_internal_errors(true); 115 | 116 | $document->loadHTML($message); 117 | 118 | libxml_use_internal_errors($previousUseInternalErrors); 119 | 120 | $cssLinkTags = []; 121 | 122 | foreach ($document->getElementsByTagName('link') as $linkTag) { 123 | if ($linkTag->getAttribute('rel') === 'stylesheet') { 124 | $cssLinkTags[] = $linkTag; 125 | } 126 | } 127 | 128 | $cssFiles = []; 129 | 130 | foreach ($cssLinkTags as $linkTag) { 131 | $cssFiles[] = $linkTag->getAttribute('href'); 132 | 133 | $linkTag->parentNode->removeChild($linkTag); 134 | } 135 | 136 | // If we found CSS files in the document we load them and return the document without the link tags 137 | if (!empty($cssFiles)) { 138 | /** @noinspection PhpExpressionResultUnusedInspection */ 139 | $this->loadCssFromFiles($cssFiles); 140 | 141 | return [$cssFiles, $document->saveHTML()]; 142 | } 143 | 144 | return [$cssFiles, $message]; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/LaravelMailCssInlinerServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | __DIR__ . '/../config/css-inliner.php' => base_path('config/css-inliner.php'), 18 | ], 'config'); 19 | } 20 | 21 | /** 22 | * Register the service provider. 23 | * 24 | * @return void 25 | */ 26 | public function register(): void 27 | { 28 | $this->mergeConfigFrom(__DIR__ . '/../config/css-inliner.php', 'css-files'); 29 | 30 | $this->app->singleton(CssInlinerPlugin::class, function ($app) { 31 | return new CssInlinerPlugin($app['config']->get('css-inliner.css-files', [])); 32 | }); 33 | 34 | Event::listen(MessageSending::class, CssInlinerPlugin::class); 35 | } 36 | } 37 | --------------------------------------------------------------------------------