diff --git a/composer.json b/composer.json index be5ef65e29b..f399ac8a8e0 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,6 @@ "laminas/laminas-i18n": "2.28.0", "laminas/laminas-loader": "2.10.0", "laminas/laminas-log": "2.17.0", - "laminas/laminas-mail": "2.25.1", "laminas/laminas-modulemanager": "2.16.0", "laminas/laminas-mvc": "3.7.0", "laminas/laminas-mvc-i18n": "1.8.0", @@ -87,6 +86,7 @@ "steverhoades/oauth2-openid-connect-server": "2.6.1", "swagger-api/swagger-ui": "5.17.14", "symfony/console": "6.4.11", + "symfony/mailer": "6.4.12", "symfony/rate-limiter": "^6.4", "symfony/var-dumper": "6.4.11", "symfony/yaml": "6.4.11", diff --git a/composer.lock b/composer.lock index 24e67857a60..f227e348fb1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8e2980eedae649fe224b7e49018beeb", + "content-hash": "a3b3369d5130dc83c7a9a6616ebbce7a", "packages": [ { "name": "ahand/mobileesp", @@ -842,6 +842,83 @@ ], "time": "2024-05-22T20:47:39+00:00" }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, { "name": "doctrine/persistence", "version": "3.3.3", @@ -939,6 +1016,73 @@ ], "time": "2024-06-20T10:14:30+00:00" }, + { + "name": "egulias/email-validator", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2023-10-06T06:47:41+00:00" + }, { "name": "endroid/qr-code", "version": "5.0.9", @@ -3207,82 +3351,6 @@ ], "time": "2023-12-05T18:27:50+00:00" }, - { - "name": "laminas/laminas-mail", - "version": "2.25.1", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-mail.git", - "reference": "110e04497395123998220e244cceecb167cc6dda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/110e04497395123998220e244cceecb167cc6dda", - "reference": "110e04497395123998220e244cceecb167cc6dda", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "laminas/laminas-loader": "^2.9.0", - "laminas/laminas-mime": "^2.11.0", - "laminas/laminas-stdlib": "^3.17.0", - "laminas/laminas-validator": "^2.31.0", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", - "symfony/polyfill-intl-idn": "^1.27.0", - "symfony/polyfill-mbstring": "^1.27.0", - "webmozart/assert": "^1.11.0" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-db": "^2.18", - "laminas/laminas-servicemanager": "^3.22.1", - "phpunit/phpunit": "^10.4.2", - "psalm/plugin-phpunit": "^0.18.4", - "symfony/process": "^6.3.4", - "vimeo/psalm": "^5.15" - }, - "suggest": { - "laminas/laminas-servicemanager": "^3.21 when using SMTP to deliver messages" - }, - "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\Mail", - "config-provider": "Laminas\\Mail\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Laminas\\Mail\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "mail" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-mail/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-mail/issues", - "rss": "https://github.com/laminas/laminas-mail/releases.atom", - "source": "https://github.com/laminas/laminas-mail" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "abandoned": "symfony/mailer", - "time": "2023-11-02T10:32:34+00:00" - }, { "name": "laminas/laminas-math", "version": "3.7.0", @@ -3350,67 +3418,6 @@ ], "time": "2023-10-18T09:53:37+00:00" }, - { - "name": "laminas/laminas-mime", - "version": "2.12.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-mime.git", - "reference": "08cc544778829b7d68d27a097885bd6e7130135e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mime/zipball/08cc544778829b7d68d27a097885bd6e7130135e", - "reference": "08cc544778829b7d68d27a097885bd6e7130135e", - "shasum": "" - }, - "require": { - "laminas/laminas-stdlib": "^2.7 || ^3.0", - "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" - }, - "conflict": { - "zendframework/zend-mime": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-mail": "^2.19.0", - "phpunit/phpunit": "~9.5.25" - }, - "suggest": { - "laminas/laminas-mail": "Laminas\\Mail component" - }, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Mime\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Create and parse MIME messages and parts", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "mime" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-mime/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-mime/issues", - "rss": "https://github.com/laminas/laminas-mime/releases.atom", - "source": "https://github.com/laminas/laminas-mime" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2023-11-02T16:47:19+00:00" - }, { "name": "laminas/laminas-modulemanager", "version": "2.16.0", @@ -8249,31 +8256,45 @@ "time": "2024-04-18T09:32:20+00:00" }, { - "name": "symfony/filesystem", - "version": "v6.4.9", + "name": "symfony/event-dispatcher", + "version": "v6.4.8", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Filesystem\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8293,10 +8314,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides basic utilities for the filesystem", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" }, "funding": [ { @@ -8312,29 +8333,336 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { - "name": "symfony/options-resolver", - "version": "v6.4.8", + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3" + "psr/event-dispatcher": "^1" }, "type": "library", - "autoload": { - "psr-4": { + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", + "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-28T09:49:33+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b6a25408c569ae2366b3f663a4edad19420a9c26", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-08T12:30:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/abe16ee7790b16aa525877419deb0f113953f0e1", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-20T08:18:25+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" }, "exclude-from-classmap": [ @@ -13558,162 +13886,6 @@ ], "time": "2024-08-29T08:15:38+00:00" }, - { - "name": "symfony/event-dispatcher", - "version": "v6.4.8", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", - "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/event-dispatcher-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-05-31T14:49:08+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" - }, { "name": "symfony/finder", "version": "v6.4.11", @@ -14119,5 +14291,5 @@ "platform-overrides": { "php": "8.1" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/config/vufind/config.ini b/config/vufind/config.ini index cfac1e0e703..0ca15445a5f 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -695,27 +695,34 @@ search_enabled = false ; This section requires no changes for most installations; if your SMTP server ; requires authentication, you can fill in a username and password below. [Mail] +; For normal SMTP you can use the following settings: host = localhost port = 25 ;username = user ;password = pass -; The server name to report to the upstream mail server when sending mail. -;name = vufind.myuniversity.edu ; If a login is required you can define which protocol to use for securing the ; connection. If no explicit protocol ('tls' or 'ssl') is configured, a protocol ; based on the configured port is chosen (587 -> tls, 487 -> ssl). ;secure = tls -; This setting enforces a limit (in seconds) on the lifetime of an SMTP -; connection, which can be useful when sending batches of emails, since it can -; help avoid errors caused by server timeouts. Comment out the setting to disable -; the limit. -connection_time_limit = 60 +; The server name to report to the upstream mail server when sending mail. +;name = vufind.myuniversity.edu + +; To use any other transport than SMTP, you can use the dsn setting to define +; the connection string. +; See https://symfony.com/doc/current/mailer.html#transport-setup for more +; information on the DSN configuration. Also note that VuFind does not include the +; third-party transports out of box, but you can add them to composer.local.json. +;dsn = "native://default" + ; Uncomment this setting to disable outbound mail but simulate success; this ; is useful for interface testing but should never be used in production! ;testOnly = true ; Set to a file path writable by VuFind to log email messages into that file; ; primarily intended for testing/debugging purposes. ;message_log = /tmp/emails.log +; Message log format. Valid options are 'plain' for plain text (default) and +; 'serialized' for serialized Email objects. +;message_log_format = plain ; The action to email records and searches may be "enabled", "disabled" or "require_login" ; (default = "require_login") ; If set to "enabled", users can send anonymous emails; If set to "require_login", diff --git a/module/VuFind/src/VuFind/Form/Handler/Email.php b/module/VuFind/src/VuFind/Form/Handler/Email.php index 3d7b61efe46..b06316921c2 100644 --- a/module/VuFind/src/VuFind/Form/Handler/Email.php +++ b/module/VuFind/src/VuFind/Form/Handler/Email.php @@ -33,8 +33,8 @@ use Laminas\Config\Config; use Laminas\Log\LoggerAwareInterface; -use Laminas\Mail\Address; use Laminas\View\Renderer\RendererInterface; +use Symfony\Component\Mime\Address; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Exception\Mail as MailException; use VuFind\Form\Form; @@ -127,7 +127,7 @@ public function handle( foreach ($recipients as $recipient) { if ($recipient['email']) { $success = $this->sendEmail( - $recipient['name'], + $recipient['name'] ?? '', $recipient['email'], $senderName, $senderEmail, @@ -167,14 +167,14 @@ protected function getSender(Form $form) /** * Send form data as email. * - * @param string $recipientName Recipient name - * @param string $recipientEmail Recipient email - * @param string $senderName Sender name - * @param string $senderEmail Sender email - * @param string $replyToName Reply-to name - * @param string $replyToEmail Reply-to email - * @param string $emailSubject Email subject - * @param string $emailMessage Email message + * @param ?string $recipientName Recipient name + * @param string $recipientEmail Recipient email + * @param string $senderName Sender name + * @param string $senderEmail Sender email + * @param string $replyToName Reply-to name + * @param string $replyToEmail Reply-to email + * @param string $emailSubject Email subject + * @param string $emailMessage Email message * * @return bool */ @@ -190,7 +190,7 @@ protected function sendEmail( ): bool { try { $this->mailer->send( - new Address($recipientEmail, $recipientName), + new Address($recipientEmail, $recipientName ?? ''), new Address($senderEmail, $senderName), $emailSubject, $emailMessage, diff --git a/module/VuFind/src/VuFind/Log/LoggerFactory.php b/module/VuFind/src/VuFind/Log/LoggerFactory.php index 49fb7715ad1..128a6a41a40 100644 --- a/module/VuFind/src/VuFind/Log/LoggerFactory.php +++ b/module/VuFind/src/VuFind/Log/LoggerFactory.php @@ -109,16 +109,14 @@ protected function addEmailWriters( $email = $parts[0]; $error_types = $parts[1] ?? ''; - // use smtp - $mailer = $container->get(\VuFind\Mailer\Mailer::class); - $msg = $mailer->getNewMessage() - ->addFrom($config->Site->email) - ->addTo($email) - ->setSubject('VuFind Log Message'); - // Make Writers $filters = explode(',', $error_types); - $writer = new Writer\Mail($msg, $mailer->getTransport()); + $writer = new Writer\Mail( + $container->get(\VuFind\Mailer\Mailer::class), + $config->Site->email, + $email, + 'VuFind Log Message' + ); $this->addWriters($logger, $writer, $filters); } diff --git a/module/VuFind/src/VuFind/Log/Writer/Mail.php b/module/VuFind/src/VuFind/Log/Writer/Mail.php index 822c27b01fe..7953183dfe6 100644 --- a/module/VuFind/src/VuFind/Log/Writer/Mail.php +++ b/module/VuFind/src/VuFind/Log/Writer/Mail.php @@ -3,9 +3,11 @@ /** * Mail log writer * + * Inspired by Laminas Mail log writer + * * PHP version 8 * - * Copyright (C) Villanova University 2010. + * Copyright (C) The National Library of Finland 2024. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -22,26 +24,70 @@ * * @category VuFind * @package Error_Logging - * @author Chris Hallberg + * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ namespace VuFind\Log\Writer; +use Laminas\Log\Formatter\FormatterInterface; +use Laminas\Log\Formatter\Simple as SimpleFormatter; +use Laminas\Log\Writer\AbstractWriter; +use Symfony\Component\Mime\Email; +use VuFind\Exception\Mail as MailException; +use VuFind\Mailer\Mailer; + /** - * This class extends the Laminas Logging towards Mail systems + * This class implements the Laminas Logging interface for Mail systems + * + * Inspired by Laminas Mail log writer * * @category VuFind * @package Error_Logging - * @author Chris Hallberg + * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -class Mail extends \Laminas\Log\Writer\Mail +class Mail extends AbstractWriter { use VerbosityTrait; + /** + * Array of formatted events to include in message body. + * + * @var array + */ + protected $eventsToMail = []; + + /** + * Array keeping track of the number of entries per priority level. + * + * @var array + */ + protected $numEntriesPerPriority = []; + + /** + * Constructor + * + * @param Mailer $mailer Mailer + * @param string $from Sender address + * @param string $to Recipient address + * @param string $subject Email subject + * @param ?FormatterInterface $formatter Log entry formatter + * + * @throws Exception\InvalidArgumentException + */ + public function __construct( + protected Mailer $mailer, + protected string $from, + protected string $to, + protected string $subject, + ?FormatterInterface $formatter = null + ) { + $this->setFormatter($formatter ?? new SimpleFormatter()); + } + /** * Write a message to the log. * @@ -52,7 +98,63 @@ class Mail extends \Laminas\Log\Writer\Mail */ protected function doWrite(array $event) { - // Apply verbosity, Call parent method: - parent::doWrite($this->applyVerbosity($event)); + $event = $this->applyVerbosity($event); + // Track the number of entries per priority level. + if (!isset($this->numEntriesPerPriority[$event['priorityName']])) { + $this->numEntriesPerPriority[$event['priorityName']] = 1; + } else { + $this->numEntriesPerPriority[$event['priorityName']]++; + } + + // All plaintext events are to use the standard formatter. + $this->eventsToMail[] = $this->formatter->format($event); + } + + /** + * Sends mail to recipient(s) if log entries are present. Note that both + * plaintext and HTML portions of email are handled here. + * + * @return void + */ + public function shutdown() + { + if (!$this->eventsToMail) { + return; + } + + // Merge all messages into a single text: + $message = implode(PHP_EOL, $this->eventsToMail); + + // Finally, send the mail. If an exception occurs, convert it into a + // warning-level message so we can avoid an exception thrown without a + // stack frame. + // N.B. Logger cannot be used when reporting errors with Logger! + try { + $this->mailer->send( + $this->to, + $this->from, + $this->subject, + $message + ); + } catch (MailException $e) { + trigger_error('Unable to send log entries via email: ' . (string)$e, E_USER_WARNING); + } + } + + /** + * Gets a string of number of entries per-priority level that occurred, or + * an empty string if none occurred. + * + * @return string + */ + protected function getFormattedNumEntriesPerPriority() + { + $strings = []; + + foreach ($this->numEntriesPerPriority as $priority => $numEntries) { + $strings[] = "{$priority}={$numEntries}"; + } + + return implode(', ', $strings); } } diff --git a/module/VuFind/src/VuFind/Mailer/Bcc.php b/module/VuFind/src/VuFind/Mailer/Bcc.php deleted file mode 100644 index 9eb625c6bb0..00000000000 --- a/module/VuFind/src/VuFind/Mailer/Bcc.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -/** - * Tweaked Laminas "Bcc" header class - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class Bcc extends \Laminas\Mail\Header\Bcc -{ - use GetFieldValueFixTrait; -} diff --git a/module/VuFind/src/VuFind/Mailer/Cc.php b/module/VuFind/src/VuFind/Mailer/Cc.php deleted file mode 100644 index bb91280afc0..00000000000 --- a/module/VuFind/src/VuFind/Mailer/Cc.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -/** - * Tweaked Laminas "Cc" header class - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class Cc extends \Laminas\Mail\Header\Cc -{ - use GetFieldValueFixTrait; -} diff --git a/module/VuFind/src/VuFind/Mailer/Factory.php b/module/VuFind/src/VuFind/Mailer/Factory.php index f05f4568a8e..e608e12e3e2 100644 --- a/module/VuFind/src/VuFind/Mailer/Factory.php +++ b/module/VuFind/src/VuFind/Mailer/Factory.php @@ -6,6 +6,7 @@ * PHP version 8 * * Copyright (C) Villanova University 2009. + * Copyright (C) The National Library of Finland 2024. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -23,20 +24,19 @@ * @category VuFind * @package Mailer * @author Demian Katz + * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development Wiki */ namespace VuFind\Mailer; -use Laminas\Mail\Transport\InMemory; -use Laminas\Mail\Transport\Smtp; -use Laminas\Mail\Transport\SmtpOptions; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface as ContainerException; use Psr\Container\ContainerInterface; +use VuFind\Config\Feature\SecretTrait; /** * Factory for instantiating Mailer objects @@ -44,6 +44,7 @@ * @category VuFind * @package Mailer * @author Demian Katz + * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development Wiki * @@ -51,48 +52,44 @@ */ class Factory implements FactoryInterface { + use SecretTrait; + /** - * Build the mail transport object. + * Return DSN from the configuration * - * @param \Laminas\Config\Config $config Configuration + * @param array $config Configuration * - * @return InMemory|Smtp + * @return string */ - protected function getTransport($config) + protected function getDSN(array $config): string { - // In test mode? Return fake object: - if (isset($config->Mail->testOnly) && $config->Mail->testOnly) { - return new InMemory(); + // In test mode? Use null transport: + if ($config['Mail']['testOnly'] ?? false) { + return 'null://null'; + } + + if ($dsn = $config['Mail']['dsn'] ?? null) { + return $dsn; } - // Create mail transport: - $settings = [ - 'host' => $config->Mail->host, 'port' => $config->Mail->port, - ]; - if (isset($config->Mail->name)) { - $settings['name'] = $config->Mail->name; + // Create DSN from settings: + $protocol = ($config['Mail']['secure'] ?? false) ? 'smtps' : 'smtp'; + $dsn = "$protocol://"; + if ( + ($username = $config['Mail']['username'] ?? null) + && ($password = $this->getSecretFromConfig($config['Mail'], 'password')) + ) { + $dsn .= "$username:$password@"; } - if (isset($config->Mail->username) && isset($config->Mail->password)) { - $settings['connection_class'] = 'login'; - $settings['connection_config'] = [ - 'username' => $config->Mail->username, - 'password' => $config->Mail->password, - ]; - // Set user defined secure connection if provided; otherwise set default - // secure connection based on configured port number. - if (isset($config->Mail->secure)) { - $settings['connection_config']['ssl'] = $config->Mail->secure; - } elseif ($settings['port'] == '587') { - $settings['connection_config']['ssl'] = 'tls'; - } elseif ($settings['port'] == '487') { - $settings['connection_config']['ssl'] = 'ssl'; - } + $dsn .= $config['Mail']['host']; + if ($port = $config['Mail']['port'] ?? null) { + $dsn .= ":$port"; } - if (isset($config->Mail->connection_time_limit)) { - $settings['connection_time_limit'] - = $config->Mail->connection_time_limit; + if ($name = $config['Mail']['name'] ?? null) { + $dsn .= "?local_domain=$name"; } - return new Smtp(new SmtpOptions($settings)); + + return $dsn; } /** @@ -119,16 +116,20 @@ public function __invoke( } // Load configurations: - $config = $container->get(\VuFind\Config\PluginManager::class) - ->get('config'); + $config = $container->get(\VuFind\Config\PluginManager::class)->get('config')->toArray(); // Create service: $class = new $requestedName( - $this->getTransport($config), - $config->Mail->message_log + new \Symfony\Component\Mailer\Mailer( + \Symfony\Component\Mailer\Transport::fromDsn($this->getDSN($config)) + ), + [ + 'message_log' => $config['Mail']['message_log'] ?? null, + 'message_log_format' => $config['Mail']['message_log_format'] ?? null, + ] ); if (!empty($config->Mail->override_from)) { - $class->setFromAddressOverride($config->Mail->override_from); + $class->setFromAddressOverride($config['Mail']['override_from'] ?? null); } return $class; } diff --git a/module/VuFind/src/VuFind/Mailer/From.php b/module/VuFind/src/VuFind/Mailer/From.php deleted file mode 100644 index 1ac5fea05fe..00000000000 --- a/module/VuFind/src/VuFind/Mailer/From.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -/** - * Tweaked Laminas "From" header class - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class From extends \Laminas\Mail\Header\From -{ - use GetFieldValueFixTrait; -} diff --git a/module/VuFind/src/VuFind/Mailer/GetFieldValueFixTrait.php b/module/VuFind/src/VuFind/Mailer/GetFieldValueFixTrait.php deleted file mode 100644 index 1390c21bddb..00000000000 --- a/module/VuFind/src/VuFind/Mailer/GetFieldValueFixTrait.php +++ /dev/null @@ -1,101 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -use Laminas\Mail\Header\HeaderInterface; -use Laminas\Mail\Header\HeaderValue; -use Laminas\Mail\Header\HeaderWrap; -use Laminas\Mail\Headers; - -use function sprintf; - -/** - * Trait that provides an improved version of the getFieldValue method. - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -trait GetFieldValueFixTrait -{ - /** - * Retrieve header value - * - * Overrides the original implementation to always enclose name in quotes. - * - * @param bool $format Return the value in Mime::Encoded (HeaderInterface::FORMAT_ENCODED) or - * in Raw format (HeaderInterface::FORMAT_RAW). Using a constant from HeaderInterface is - * recommended instead of a raw boolean value. - * - * @return string - */ - public function getFieldValue($format = HeaderInterface::FORMAT_RAW) - { - $emails = []; - $encoding = $this->getEncoding(); - - foreach ($this->getAddressList() as $address) { - $email = $address->getEmail(); - $name = $address->getName(); - - if ( - $format === HeaderInterface::FORMAT_ENCODED - && 'ASCII' !== $encoding - ) { - if (! empty($name)) { - $name = HeaderWrap::mimeEncodeValue($name, $encoding); - } - - if (preg_match('/^(.+)@([^@]+)$/', $email, $matches)) { - $localPart = $matches[1]; - $hostname = $this->idnToAscii($matches[2]); - $email = sprintf('%s@%s', $localPart, $hostname); - } - } - - if (empty($name)) { - $emails[] = $email; - } else { - $emails[] = sprintf('"%s" <%s>', $name, $email); - } - } - - // Ensure the values are valid before sending them. - if ($format !== HeaderInterface::FORMAT_RAW) { - foreach ($emails as $email) { - HeaderValue::assertValid($email); - } - } - - return implode(',' . Headers::FOLDING, $emails); - } -} diff --git a/module/VuFind/src/VuFind/Mailer/Mailer.php b/module/VuFind/src/VuFind/Mailer/Mailer.php index 5d28ab88056..638fb26bc05 100644 --- a/module/VuFind/src/VuFind/Mailer/Mailer.php +++ b/module/VuFind/src/VuFind/Mailer/Mailer.php @@ -6,6 +6,7 @@ * PHP version 8 * * Copyright (C) Villanova University 2009. + * Copyright (C) The National Library of Finland 2024. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -23,23 +24,23 @@ * @category VuFind * @package Mailer * @author Demian Katz + * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development Wiki */ namespace VuFind\Mailer; -use Laminas\Mail\Address; -use Laminas\Mail\AddressList; -use Laminas\Mail\Header\ContentType; -use Laminas\Mail\Transport\TransportInterface; -use Laminas\Mime\Message as MimeMessage; -use Laminas\Mime\Mime; -use Laminas\Mime\Part as MimePart; +use Laminas\View\Renderer\PhpRenderer; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Exception\RfcComplianceException; use VuFind\Exception\Mail as MailException; +use VuFind\RecordDriver\AbstractBase; use function count; -use function is_callable; +use function is_array; /** * VuFind Mailer Class @@ -47,6 +48,7 @@ * @category VuFind * @package Mailer * @author Demian Katz + * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development Wiki */ @@ -60,17 +62,14 @@ class Mailer implements /** * Mail transport * - * @var TransportInterface + * @var MailerInterface */ protected $transport; /** - * A clone of $transport above. This can be used to reset the connection state - * in case transport doesn't support the disconnect method or it throws an - * exception (this can happen if the connection is stale and the connector tries - * to issue a QUIT message for clean disconnect). + * A clone of $transport above. This can be used to reset the connection state. * - * @var TransportInterface + * @var MailerInterface */ protected $initialTransport; @@ -91,10 +90,10 @@ class Mailer implements /** * Constructor * - * @param TransportInterface $transport Mail transport - * @param ?string $messageLog File to log messages into (null for no logging) + * @param MailerInterface $transport Mail transport + * @param array $options Message log options */ - public function __construct(TransportInterface $transport, protected ?string $messageLog = null) + public function __construct(MailerInterface $transport, protected array $options = []) { $this->setTransport($transport); } @@ -102,9 +101,9 @@ public function __construct(TransportInterface $transport, protected ?string $me /** * Get the mail transport object. * - * @return TransportInterface + * @return MailerInterface */ - public function getTransport() + public function getTransport(): MailerInterface { return $this->transport; } @@ -112,17 +111,11 @@ public function getTransport() /** * Get a text email message object. * - * @return Message + * @return Email */ - public function getNewMessage() + public function getNewMessage(): Email { - $message = $this->getNewBlankMessage(); - $headers = $message->getHeaders(); - $ctype = new ContentType(); - $ctype->setType(Mime::TYPE_TEXT); - $ctype->addParameter('charset', 'UTF-8'); - $headers->addHeader($ctype); - return $message; + return $this->getNewBlankMessage(); } /** @@ -132,46 +125,31 @@ public function getNewMessage() */ public function resetConnection() { - // If the transport has a disconnect method, call it. Otherwise, and in case - // disconnect fails, revert to the transport instance clone made before a - // connection was made. - $transport = $this->getTransport(); - if (is_callable([$transport, 'disconnect'])) { - try { - $transport->disconnect(); - } catch (\Exception $e) { - $this->setTransport($this->initialTransport); - } - } else { - $this->setTransport($this->initialTransport); - } + $this->setTransport($this->initialTransport); return $this; } /** * Get a blank email message object. * - * @return Message + * @return Email */ - public function getNewBlankMessage() + public function getNewBlankMessage(): Email { - $message = new Message(); - $message->setEncoding('UTF-8'); - return $message; + return new Email(); } /** * Set the mail transport object. * - * @param TransportInterface $transport Mail transport object + * @param MailerInterface $transport Mail transport object * * @return void */ - public function setTransport($transport) + public function setTransport(MailerInterface $transport): void { $this->transport = $transport; - // Store a clone of the given transport so that we can reset the connection - // as necessary. + // Store a clone of the given transport so that we can reset the connection as necessary: $this->initialTransport = clone $this->transport; } @@ -180,83 +158,91 @@ public function setTransport($transport) * * @param string $input String to convert * - * @return AddressList + * @return array */ - public function stringToAddressList($input) + public function stringToAddressList($input): array { // Create recipient list - $list = new AddressList(); + $list = []; foreach (preg_split('/[\s,;]/', $input) as $current) { $current = trim($current); if (!empty($current)) { - $list->add($current); + $list[] = $current; } } return $list; } /** - * Constructs a {@see MimeMessage} body from given text and html content. + * Constructs a {@see Email} body from given text and html content. * * @param string|null $text Mail content used for plain text part * @param string|null $html Mail content used for html part * - * @return MimeMessage + * @return Email */ public function buildMultipartBody( string $text = null, string $html = null - ): MimeMessage { - $parts = new MimeMessage(); - - if ($text) { - $textPart = new MimePart($text); - $textPart->setType(Mime::TYPE_TEXT); - $textPart->setCharset('utf-8'); - $textPart->setEncoding(Mime::ENCODING_QUOTEDPRINTABLE); - $parts->addPart($textPart); + ): Email { + $email = $this->getNewBlankMessage(); + if (null !== $text) { + $email->text($text); } - - if ($html) { - $htmlPart = new MimePart($html); - $htmlPart->setType(Mime::TYPE_HTML); - $htmlPart->setCharset('utf-8'); - $htmlPart->setEncoding(Mime::ENCODING_QUOTEDPRINTABLE); - $parts->addPart($htmlPart); + if (null !== $html) { + $email->html($html); } - - $alternativePart = new MimePart($parts->generateMessage()); - $alternativePart->setType('multipart/alternative'); - $alternativePart->setBoundary($parts->getMime()->boundary()); - $alternativePart->setCharset('utf-8'); - - $body = new MimeMessage(); - $body->setParts([$alternativePart]); - - return $body; + return $email; } /** * Send an email message. * - * @param string|Address|AddressList $to Recipient email address (or - * delimited list) - * @param string|Address $from Sender name and email address - * @param string $subject Subject line for message - * @param string|MimeMessage $body Message body - * @param string $cc CC recipient (null for none) - * @param string|Address|AddressList $replyTo Reply-To address (or delimited - * list, null for none) + * @param string|Address|Address[] $to Recipient email address(es) (or delimited list) + * @param string|Address $from Sender name and email address + * @param string $subject Subject line for message + * @param string|Email $body Message body + * @param string|Address|Address[]|null $cc CC recipient(s) (null for none) + * @param string|Address|Address[]|null $replyTo Reply-To address(es) (or delimited list, null for none) * * @throws MailException * @return void */ - public function send($to, $from, $subject, $body, $cc = null, $replyTo = null) - { - $recipients = $this->convertToAddressList($to); - $replyTo = $this->convertToAddressList($replyTo); + public function send( + string|Address|array $to, + string|Address $from, + string $subject, + string|Email $body, + string|Address|array|null $cc = null, + string|Address|array|null $replyTo = null + ) { + try { + if (!($from instanceof Address)) { + $from = new Address($from); + } + } catch (RfcComplianceException $e) { + throw new MailException('Invalid Sender Email Address', MailException::ERROR_INVALID_SENDER, $e); + } + try { + $recipients = $this->convertToAddressList($to); + } catch (RfcComplianceException $e) { + throw new MailException('Invalid Recipient Email Address', MailException::ERROR_INVALID_RECIPIENT, $e); + } + try { + $replyTo = $this->convertToAddressList($replyTo); + } catch (RfcComplianceException $e) { + throw new MailException('Invalid Reply-To Email Address', MailException::ERROR_INVALID_REPLY_TO, $e); + } + try { + $cc = $this->convertToAddressList($cc); + } catch (RfcComplianceException $e) { + throw new MailException('Invalid CC Email Address', MailException::ERROR_INVALID_RECIPIENT, $e); + } - // Validate email addresses: + // Validate recipient email address count: + if (count($recipients) == 0) { + throw new MailException('Invalid Recipient Email Address', MailException::ERROR_INVALID_RECIPIENT); + } if ($this->maxRecipients > 0) { if ($this->maxRecipients < count($recipients)) { throw new MailException( @@ -265,53 +251,19 @@ public function send($to, $from, $subject, $body, $cc = null, $replyTo = null) ); } } - $validator = new \Laminas\Validator\EmailAddress(); - if (count($recipients) == 0) { - throw new MailException( - 'Invalid Recipient Email Address', - MailException::ERROR_INVALID_RECIPIENT - ); - } - foreach ($recipients as $current) { - if (!$validator->isValid($current->getEmail())) { - throw new MailException( - 'Invalid Recipient Email Address', - MailException::ERROR_INVALID_RECIPIENT - ); - } - } - foreach ($replyTo as $current) { - if (!$validator->isValid($current->getEmail())) { - throw new MailException( - 'Invalid Reply-To Email Address', - MailException::ERROR_INVALID_REPLY_TO - ); - } - } - $fromEmail = ($from instanceof Address) - ? $from->getEmail() : $from; - if (!$validator->isValid($fromEmail)) { - throw new MailException( - 'Invalid Sender Email Address', - MailException::ERROR_INVALID_SENDER - ); - } if ( !empty($this->fromAddressOverride) - && $this->fromAddressOverride != $fromEmail + && $this->fromAddressOverride != $from->getAddress() ) { // Add the original from address as the reply-to address unless // a reply-to address has been specified - if (count($replyTo) === 0) { - $replyTo->add($fromEmail); - } - if (!($from instanceof Address)) { - $from = new Address($from); + if (!$replyTo) { + $replyTo[] = $from->getAddress(); } $name = $from->getName(); if (!$name) { - [$fromPre] = explode('@', $from->getEmail()); + [$fromPre] = explode('@', $from->getAddress()); $name = $fromPre ? $fromPre : null; } $from = new Address($this->fromAddressOverride, $name); @@ -320,59 +272,64 @@ public function send($to, $from, $subject, $body, $cc = null, $replyTo = null) // Convert all exceptions thrown by mailer into MailException objects: try { // Send message - $message = $body instanceof MimeMessage - ? $this->getNewBlankMessage() - : $this->getNewMessage(); - $message->addFrom($from) - ->addTo($recipients) - ->setBody($body) - ->setSubject($subject); - if ($cc !== null) { - $message->addCc($cc); + if ($body instanceof Email) { + $email = $body; + if (null === $email->getSubject()) { + $email->subject($subject); + } + } else { + $email = $this->getNewBlankMessage(); + $email->text($body); + $email->subject($subject); } - if ($replyTo) { - $message->addReplyTo($replyTo); + $email->addFrom($from); + foreach ($recipients as $current) { + $email->addTo($current); } - $this->getTransport()->send($message); - if ($this->messageLog) { - file_put_contents($this->messageLog, $message->toString() . "\n", FILE_APPEND); + foreach ($cc as $current) { + $email->addCc($current); + } + foreach ($replyTo as $current) { + $email->addReplyTo($current); + } + $this->getTransport()->send($email); + if ($logFile = $this->options['message_log'] ?? null) { + $format = $this->options['message_log_format'] ?? 'plain'; + $data = 'serialized' === $format + ? serialize($email) . "\x1E" // use Record Separator to separate messages + : $email->toString() . "\n\n"; + file_put_contents($logFile, $data, FILE_APPEND); } } catch (\Exception $e) { $this->logError($e->getMessage()); - throw new MailException($e->getMessage(), MailException::ERROR_UNKNOWN); + throw new MailException($e->getMessage(), MailException::ERROR_UNKNOWN, $e); } } /** * Send an email message representing a link. * - * @param string $to Recipient email address - * @param string|\Laminas\Mail\Address $from Sender name and email - * address - * @param string $msg User notes to include in - * message - * @param string $url URL to share - * @param \Laminas\View\Renderer\PhpRenderer $view View object (used to render - * email templates) - * @param string $subject Subject for email - * (optional) - * @param string $cc CC recipient (null for - * none) - * @param string|Address|AddressList $replyTo Reply-To address (or - * delimited list, null for none) + * @param string|Address|Address[] $to Recipient email address(es) (or delimited list) + * @param string|Address $from Sender name and email address + * @param string $msg User notes to include in message + * @param string $url URL to share + * @param PhpRenderer $view View object (used to render email templates) + * @param ?string $subject Subject for email (optional) + * @param string|Address|Address[]|null $cc CC recipient(s) (null for none) + * @param string|Address|Address[]|null $replyTo Reply-To address(es) (or delimited list, null for none) * * @throws MailException * @return void */ public function sendLink( - $to, - $from, - $msg, - $url, - $view, - $subject = null, - $cc = null, - $replyTo = null + string|Address|array $to, + string|Address $from, + string $msg, + string $url, + PhpRenderer $view, + ?string $subject = null, + string|Address|array|null $cc = null, + string|Address|array|null $replyTo = null ) { if (null === $subject) { $subject = $this->getDefaultLinkSubject(); @@ -399,33 +356,27 @@ public function getDefaultLinkSubject() /** * Send an email message representing a record. * - * @param string $to Recipient email address - * @param string|\Laminas\Mail\Address $from Sender name and email - * address - * @param string $msg User notes to include in - * message - * @param \VuFind\RecordDriver\AbstractBase $record Record being emailed - * @param \Laminas\View\Renderer\PhpRenderer $view View object (used to render - * email templates) - * @param string $subject Subject for email - * (optional) - * @param string $cc CC recipient (null for - * none) - * @param string|Address|AddressList $replyTo Reply-To address (or - * delimited list, null for none) + * @param string|Address|Address[] $to Recipient email address(es) (or delimited list) + * @param string|Address $from Sender name and email address + * @param string $msg User notes to include in message + * @param AbstractBase $record Record being emailed + * @param PhpRenderer $view View object (used to render email templates) + * @param ?string $subject Subject for email (optional) + * @param string|Address|Address[]|null $cc CC recipient(s) (null for none) + * @param string|Address|Address[]|null $replyTo Reply-To address(es) (or delimited list, null for none) * * @throws MailException * @return void */ public function sendRecord( - $to, - $from, - $msg, - $record, - $view, - $subject = null, - $cc = null, - $replyTo = null + string|Address|array $to, + string|Address $from, + string $msg, + AbstractBase $record, + PhpRenderer $view, + ?string $subject = null, + string|Address|array|null $cc = null, + string|Address|array|null $replyTo = null ) { if (null === $subject) { $subject = $this->getDefaultRecordSubject($record); @@ -460,8 +411,7 @@ public function setMaxRecipients($max) */ public function getDefaultRecordSubject($record) { - return $this->translate('Library Catalog Record') . ': ' - . $record->getBreadcrumb(); + return $this->translate('Library Catalog Record') . ': ' . $record->getBreadcrumb(); } /** @@ -487,21 +437,26 @@ public function setFromAddressOverride($address) } /** - * Convert the given addresses to an AddressList object + * Convert the given addresses to an array * - * @param string|Address|AddressList $addresses Addresses + * @param string|Address|Address[]|null $addresses Addresses * - * @return AddressList + * @return array */ - protected function convertToAddressList($addresses) + protected function convertToAddressList(string|Address|array|null $addresses): array { - if ($addresses instanceof AddressList) { - $result = $addresses; - } elseif ($addresses instanceof Address) { - $result = new AddressList(); - $result->add($addresses); - } else { - $result = $this->stringToAddressList($addresses ? $addresses : ''); + if (empty($addresses)) { + return []; + } + if ($addresses instanceof Address) { + return [$addresses]; + } + if (is_array($addresses)) { + return Address::createArray($addresses); + } + $result = []; + foreach (explode(';', $addresses) as $current) { + $result[] = Address::create($current); } return $result; } diff --git a/module/VuFind/src/VuFind/Mailer/Message.php b/module/VuFind/src/VuFind/Mailer/Message.php deleted file mode 100644 index 2d291b1e301..00000000000 --- a/module/VuFind/src/VuFind/Mailer/Message.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -use Laminas\Mail\AddressList; - -/** - * Tweaked Laminas Message class - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class Message extends \Laminas\Mail\Message -{ - /** - * Retrieve list of From senders - * - * Returns our local "From" class - * - * @return AddressList - */ - public function getFrom() - { - return $this->getAddressListFromHeader('from', From::class); - } - - /** - * Access the address list of the To header - * - * @return AddressList - */ - public function getTo() - { - return $this->getAddressListFromHeader('to', To::class); - } - - /** - * Retrieve list of CC recipients - * - * @return AddressList - */ - public function getCc() - { - return $this->getAddressListFromHeader('cc', Cc::class); - } - - /** - * Retrieve list of BCC recipients - * - * @return AddressList - */ - public function getBcc() - { - return $this->getAddressListFromHeader('bcc', Bcc::class); - } - - /** - * Access the address list of the Reply-To header - * - * @return AddressList - */ - public function getReplyTo() - { - return $this->getAddressListFromHeader('reply-to', ReplyTo::class); - } -} diff --git a/module/VuFind/src/VuFind/Mailer/ReplyTo.php b/module/VuFind/src/VuFind/Mailer/ReplyTo.php deleted file mode 100644 index a022782aaf2..00000000000 --- a/module/VuFind/src/VuFind/Mailer/ReplyTo.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -/** - * Tweaked Laminas "ReplyTo" header class - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class ReplyTo extends \Laminas\Mail\Header\ReplyTo -{ - use GetFieldValueFixTrait; -} diff --git a/module/VuFind/src/VuFind/Mailer/To.php b/module/VuFind/src/VuFind/Mailer/To.php deleted file mode 100644 index 44390d2f968..00000000000 --- a/module/VuFind/src/VuFind/Mailer/To.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Mailer; - -/** - * Tweaked Laminas "To" header class - * - * @category VuFind - * @package Mailer - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class To extends \Laminas\Mail\Header\To -{ - use GetFieldValueFixTrait; -} diff --git a/module/VuFind/src/VuFindTest/Feature/EmailTrait.php b/module/VuFind/src/VuFindTest/Feature/EmailTrait.php index 77da4e60e67..61ac8b3802f 100644 --- a/module/VuFind/src/VuFindTest/Feature/EmailTrait.php +++ b/module/VuFind/src/VuFindTest/Feature/EmailTrait.php @@ -29,6 +29,8 @@ namespace VuFindTest\Feature; +use Symfony\Component\Mime\Email; + /** * Trait adding the ability to inspect sent emails. * @@ -50,6 +52,16 @@ protected function getEmailLogPath(): string return APPLICATION_PATH . '/vufind-mail.log'; } + /** + * Get the format to use for email message log. + * + * @return string + */ + protected function getEmailLogFormat(): string + { + return 'serialized'; + } + /** * Clear out the email log to eliminate any past contents. * @@ -59,4 +71,24 @@ protected function resetEmailLog(): void { file_put_contents($this->getEmailLogPath(), ''); } + + /** + * Get a logged email from the log file. + * + * @param int $index Index of the message to get (0-based) + * + * @return Email + */ + protected function getLoggedEmail(int $index = 0): Email + { + $data = file_get_contents($this->getEmailLogPath()); + if (!$data) { + throw new \Exception('No serialized email message data found'); + } + $records = explode("\x1E", $data); + if (null === ($record = $records[$index] ?? null)) { + throw new \Exception("Message with index $index not found"); + } + return unserialize($record); + } } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php index 75ee44791b1..b0b71ebd103 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php @@ -474,6 +474,7 @@ public function testRecoverPasswordByUsername(): void 'Mail' => [ 'testOnly' => true, 'message_log' => $this->getEmailLogPath(), + 'message_log_format' => $this->getEmailLogFormat(), ], ], ] @@ -498,8 +499,8 @@ public function testRecoverPasswordByUsername(): void ); // Extract URL from email: - $email = file_get_contents($this->getEmailLogPath()); - preg_match('/You can reset your password at this URL: (http.*)/', $email, $matches); + $email = $this->getLoggedEmail(); + preg_match('/You can reset your password at this URL: (http.*)/', $email->getBody()->getBody(), $matches); $link = $matches[1]; // Reset the password: diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php index ccba4bc5b0e..c4988cad62d 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php @@ -104,6 +104,7 @@ protected function getConfigIniEmailOverrides(): array 'Mail' => [ 'testOnly' => true, 'message_log' => $this->getEmailLogPath(), + 'message_log_format' => $this->getEmailLogFormat(), ], ]; } @@ -286,8 +287,8 @@ public function testEmailAuthentication(): void ); // Extract the link from the provided message: - $email = file_get_contents($this->getEmailLogPath()); - preg_match('/Link to login: <(http.*)>/', $email, $matches); + $email = $this->getLoggedEmail(); + preg_match('/Link to login: <(http.*)>/', $email->getBody()->getBody(), $matches); $session->visit($matches[1]); // Log out diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/EmailVerificationTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/EmailVerificationTest.php index b224bfa19cd..74382855c19 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/EmailVerificationTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/EmailVerificationTest.php @@ -73,6 +73,7 @@ public function testEmailVerification(): void 'Mail' => [ 'testOnly' => true, 'message_log' => $this->getEmailLogPath(), + 'message_log_format' => $this->getEmailLogFormat(), ], ], ] @@ -94,8 +95,12 @@ public function testEmailVerification(): void ); // Extract the link from the provided message: - $email = file_get_contents($this->getEmailLogPath()); - preg_match('/You can verify your email address with this link: <(http.*)>/', $email, $matches); + $email = $this->getLoggedEmail(); + preg_match( + '/You can verify your email address with this link: <(http.*)>/', + $email->getBody()->getBody(), + $matches + ); $verifyLink = $matches[1]; // Follow the verification link: @@ -136,6 +141,7 @@ public function testEmailAddressChange(): void 'Mail' => [ 'testOnly' => true, 'message_log' => $this->getEmailLogPath(), + 'message_log_format' => $this->getEmailLogFormat(), ], ], ] @@ -170,15 +176,21 @@ public function testEmailAddressChange(): void ); // Confirm that messages went to both new and old email addresses, and extract the verify link: - $email = file_get_contents($this->getEmailLogPath()); - $this->assertStringContainsString('To: changed@example.com', $email); - $this->assertStringContainsString('To: username1@ignore.com', $email); + $email = $this->getLoggedEmail(0); + $this->assertEquals('To: changed@example.com', $email->getHeaders()->get('to')->toString()); + preg_match( + '/You can verify your email address with this link: <(http.*)>/', + $email->getBody()->getBody(), + $matches + ); + $verifyLink = $matches[1]; + + $notifyEmail = $this->getLoggedEmail(1); + $this->assertEquals('To: username1@ignore.com', $notifyEmail->getHeaders()->get('to')->toString()); $this->assertStringContainsString( 'A request was just made to change your email address at Library Catalog.', - $email + $notifyEmail->getBody()->getBody() ); - preg_match('/You can verify your email address with this link: <(http.*)>/', $email, $matches); - $verifyLink = $matches[1]; // Follow the verification link: $session->visit($verifyLink); diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/UrlShortenerTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/UrlShortenerTest.php index 84f7897161b..5c2355eea91 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/UrlShortenerTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/UrlShortenerTest.php @@ -57,6 +57,7 @@ public function testDatabaseDrivenShortening(): void 'email_action' => 'enabled', 'testOnly' => true, 'message_log' => $this->getEmailLogPath(), + 'message_log_format' => $this->getEmailLogFormat(), 'url_shortener' => 'database', ], ], @@ -76,8 +77,8 @@ public function testDatabaseDrivenShortening(): void $this->assertEquals('Message Sent', $this->findCssAndGetText($page, '.modal .alert-success')); // Extract the link from the provided message: - $email = file_get_contents($this->getEmailLogPath()); - preg_match('/Link: <(http.*)>/', $email, $matches); + $email = $this->getLoggedEmail(); + preg_match('/Link: <(http.*)>/', $email->getBody()->getBody(), $matches); $shortLink = $matches[1]; $this->assertNotEquals($searchUrl, $shortLink); $this->assertStringContainsString('/short', $shortLink); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php index 1906d9ff29e..bcd9510822d 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php @@ -29,8 +29,8 @@ namespace VuFindTest\Mailer; -use Laminas\Mail\Address; -use Laminas\Mail\AddressList; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; use VuFind\Mailer\Factory as MailerFactory; use VuFind\Mailer\Mailer; use VuFindTest\Container\MockContainer; @@ -49,6 +49,7 @@ class MailerTest extends \PHPUnit\Framework\TestCase { use \VuFindTest\Feature\ConfigPluginManagerTrait; + use \VuFindTest\Feature\ReflectionTrait; /** * Test that the factory configures the object correctly. @@ -61,26 +62,29 @@ public function testFactoryConfiguration() 'Mail' => [ 'host' => 'vufindtest.localhost', 'port' => 123, - 'connection_time_limit' => 600, 'name' => 'foo', 'username' => 'vufinduser', 'password' => 'vufindpass', ], ]; + $configDsn = [ + 'Mail' => [ + 'dsn' => 'esmtp://foo@bar/', + ], + ]; $cm = $this->getMockConfigPluginManager(compact('config')); $sm = new MockContainer($this); $sm->set(\VuFind\Config\PluginManager::class, $cm); $factory = new MailerFactory(); - $mailer = $factory($sm, Mailer::class); - $options = $mailer->getTransport()->getOptions(); - $this->assertEquals('vufindtest.localhost', $options->getHost()); - $this->assertEquals('foo', $options->getName()); - $this->assertEquals(123, $options->getPort()); - $this->assertEquals(600, $options->getConnectionTimeLimit()); - $this->assertEquals('login', $options->getConnectionClass()); + + $this->assertEquals( + 'smtp://vufinduser:vufindpass@vufindtest.localhost:123?local_domain=foo', + $this->callMethod($factory, 'getDSN', [$config]) + ); + $this->assertEquals( - ['username' => 'vufinduser', 'password' => 'vufindpass'], - $options->getConnectionConfig() + 'esmtp://foo@bar/', + $this->callMethod($factory, 'getDSN', [$configDsn]) ); } @@ -92,14 +96,12 @@ public function testFactoryConfiguration() public function testSend() { $callback = function ($message): bool { - return '' == $message->getTo()->current()->toString() - && '' == $message->getFrom()->current()->toString() - && 'body' == $message->getBody() + return 'to@example.com' == $message->getTo()[0]->toString() + && 'from@example.com' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); $mailer->send('to@example.com', 'from@example.com', 'subject', 'body'); } @@ -111,16 +113,13 @@ public function testSend() public function testSendWithAddressObjectInSender() { $callback = function ($message): bool { - $fromString = $message->getFrom()->current()->toString(); - return '' == $message->getTo()->current()->toString() - && 'Sender TextName ' == $fromString - && 'body' == $message->getBody() + return 'to@example.com' == $message->getTo()[0]->toString() + && '"Sender TextName" ' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); + $mailer = $this->getMailer($callback); $address = new Address('from@example.com', 'Sender TextName'); - $mailer = new Mailer($transport); $mailer->send('to@example.com', $address, 'subject', 'body'); } @@ -132,36 +131,33 @@ public function testSendWithAddressObjectInSender() public function testSendWithAddressObjectInRecipient() { $callback = function ($message): bool { - return 'Recipient TextName ' == $message->getTo()->current()->toString() - && '' == $message->getFrom()->current()->toString() - && 'body' == $message->getBody() + return '"Recipient TextName" ' == $message->getTo()[0]->toString() + && 'from@example.com' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); + $mailer = $this->getMailer($callback); $address = new Address('to@example.com', 'Recipient TextName'); - $mailer = new Mailer($transport); $mailer->send($address, 'from@example.com', 'subject', 'body'); } /** - * Test sending an email using an address list object for the To field. + * Test sending an email using an address list for the To field. * * @return void */ - public function testSendWithAddressListObjectInRecipient() + public function testSendWithAddressListInRecipient() { $callback = function ($message): bool { - return 'Recipient TextName ' == $message->getTo()->current()->toString() - && '' == $message->getFrom()->current()->toString() - && 'body' == $message->getBody() + return '"Recipient TextName" ' == $message->getTo()[0]->toString() + && 'from@example.com' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); - $list = new AddressList(); - $list->add(new Address('to@example.com', 'Recipient TextName')); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); + $list = [ + new Address('to@example.com', 'Recipient TextName'), + ]; $mailer->send($list, 'from@example.com', 'subject', 'body'); } @@ -173,18 +169,15 @@ public function testSendWithAddressListObjectInRecipient() public function testSendWithFromOverride() { $callback = function ($message): bool { - $fromString = $message->getFrom()->current()->toString(); - return '' == $message->getTo()->current()->toString() - && '' == $message->getReplyTo()->current()->toString() - && 'me ' == $fromString - && 'body' == $message->getBody() + return 'to@example.com' == $message->getTo()[0]->toString() + && 'me@example.com' == $message->getReplyTo()[0]->toString() + && '"me" ' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); - $address = new Address('me@example.com'); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); $mailer->setFromAddressOverride('no-reply@example.com'); + $address = new Address('me@example.com'); $mailer->send('to@example.com', $address, 'subject', 'body'); } @@ -196,17 +189,14 @@ public function testSendWithFromOverride() public function testSendWithReplyTo() { $callback = function ($message): bool { - $fromString = $message->getFrom()->current()->toString(); - return '' == $message->getTo()->current()->toString() - && '' == $message->getReplyTo()->current()->toString() - && '' == $fromString - && 'body' == $message->getBody() + return 'to@example.com' == $message->getTo()[0]->toString() + && 'reply-to@example.com' == $message->getReplyTo()[0]->toString() + && 'me@example.com' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); + $mailer = $this->getMailer($callback); $address = new Address('me@example.com'); - $mailer = new Mailer($transport); $mailer->send('to@example.com', $address, 'subject', 'body', null, 'reply-to@example.com'); } @@ -219,18 +209,16 @@ public function testSendWithReplyTo() public function testSendWithFromOverrideAndReplyTo() { $callback = function ($message): bool { - $fromString = $message->getFrom()->current()->toString(); - return '' == $message->getTo()->current()->toString() - && '' == $message->getReplyTo()->current()->toString() - && 'me ' == $fromString - && 'body' == $message->getBody() + $fromString = $message->getFrom()[0]->toString(); + return 'to@example.com' == $message->getTo()[0]->toString() + && 'reply-to@example.com' == $message->getReplyTo()[0]->toString() + && '"me" ' == $fromString + && 'body' == $message->getBody()->getBody() && 'subject' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); - $address = new Address('me@example.com'); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); $mailer->setFromAddressOverride('no-reply@example.com'); + $address = new Address('me@example.com'); $mailer->send('to@example.com', $address, 'subject', 'body', null, 'reply-to@example.com'); } @@ -245,9 +233,8 @@ public function testBadTo() $this->expectExceptionMessage('Invalid Recipient Email Address'); $this->expectExceptionCode(\VuFind\Exception\Mail::ERROR_INVALID_RECIPIENT); - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $mailer = new Mailer($transport); - $mailer->send('bad@bad', 'from@example.com', 'subject', 'body'); + $mailer = $this->getMailer(); + $mailer->send('bad@.bad', 'from@example.com', 'subject', 'body'); } /** @@ -261,15 +248,14 @@ public function testBadReplyTo() $this->expectExceptionMessage('Invalid Reply-To Email Address'); $this->expectExceptionCode(\VuFind\Exception\Mail::ERROR_INVALID_REPLY_TO); - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $mailer = new Mailer($transport); + $mailer = $this->getMailer(); $mailer->send( 'good@good.com', 'from@example.com', 'subject', 'body', null, - 'bad@bad' + 'bad@.bad' ); } @@ -284,8 +270,7 @@ public function testEmptyTo() $this->expectExceptionMessage('Invalid Recipient Email Address'); $this->expectExceptionCode(\VuFind\Exception\Mail::ERROR_INVALID_RECIPIENT); - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $mailer = new Mailer($transport); + $mailer = $this->getMailer(); $mailer->send('', 'from@example.com', 'subject', 'body'); } @@ -300,8 +285,7 @@ public function testTooManyRecipients() $this->expectExceptionMessage('Too Many Email Recipients'); $this->expectExceptionCode(\VuFind\Exception\Mail::ERROR_TOO_MANY_RECIPIENTS); - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $mailer = new Mailer($transport); + $mailer = $this->getMailer(); $mailer->send('one@test.com;two@test.com', 'from@example.com', 'subject', 'body'); } @@ -316,25 +300,8 @@ public function testBadFrom() $this->expectExceptionMessage('Invalid Sender Email Address'); $this->expectExceptionCode(\VuFind\Exception\Mail::ERROR_INVALID_SENDER); - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $mailer = new Mailer($transport); - $mailer->send('to@example.com', 'bad@bad', 'subject', 'body'); - } - - /** - * Test bad from address in Address object. - * - * @return void - */ - public function testBadFromInAddressObject() - { - $this->expectException(\VuFind\Exception\Mail::class); - $this->expectExceptionMessage('Invalid Sender Email Address'); - $this->expectExceptionCode(\VuFind\Exception\Mail::ERROR_INVALID_SENDER); - - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $mailer = new Mailer($transport); - $mailer->send('to@example.com', new Address('bad@bad'), 'subject', 'body'); + $mailer = $this->getMailer(); + $mailer->send('to@example.com', 'bad@.bad', 'subject', 'body'); } /** @@ -347,7 +314,7 @@ public function testTransportException() $this->expectException(\VuFind\Exception\Mail::class); $this->expectExceptionMessage('Boom'); - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); + $transport = $this->createMock(MailerInterface::class); $transport->expects($this->once())->method('send')->will($this->throwException(new \Exception('Boom'))); $mailer = new Mailer($transport); $mailer->send('to@example.com', 'from@example.com', 'subject', 'body'); @@ -397,17 +364,15 @@ public function testSendLink() $callback = function ($message): bool { $to = $message->getTo(); - return $to->has('to@example.com') - && $to->has('to2@example.com') + return 'to@example.com' === $to[0]->toString() + && 'to2@example.com' === $to[1]->toString() && 2 == count($to) - && '' == $message->getFrom()->current()->toString() - && '' == $message->getCc()->current()->toString() - && 'body' == $message->getBody() + && 'from@example.com' == $message->getFrom()[0]->toString() + && 'cc@example.com' == $message->getCc()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'Library Catalog Search Result' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); $mailer->setMaxRecipients(2); $mailer->sendLink( 'to@example.com;to2@example.com', @@ -443,30 +408,15 @@ public function testSendRecord() ->will($this->returnValue('body')); $callback = function ($message): bool { - return '' == $message->getTo()->current()->toString() - && '' == $message->getFrom()->current()->toString() - && 'body' == $message->getBody() + return 'to@example.com' == $message->getTo()[0]->toString() + && 'from@example.com' == $message->getFrom()[0]->toString() + && 'body' == $message->getBody()->getBody() && 'Library Catalog Record: breadcrumb' == $message->getSubject(); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); $mailer->sendRecord('to@example.com', 'from@example.com', 'message', $driver, $view); } - /** - * Test connection reset - * - * @return void - */ - public function testResetConnection() - { - $transport = $this->createMock(\Laminas\Mail\Transport\Smtp::class); - $transport->expects($this->once())->method('disconnect'); - $mailer = new Mailer($transport); - $mailer->resetConnection(); - } - /** * Test sending an email using with text part and html part and multipart content type. * @@ -478,19 +428,31 @@ public function testSendMimeMessageWithMultipartAlternativeContentType() $html = 'htmlhtml body part'; $text = 'this is the text part'; $callback = function ($message) use ($html, $text): bool { - $fromString = $message->getFrom()->current()->toString(); - return '' == $message->getTo()->current()->toString() - && 'Sender TextName ' == $fromString + return 'to@example.com' == $message->getTo()[0]->toString() + && '"Sender TextName" ' == $message->getFrom()[0]->toString() && 'subject' == $message->getSubject() - && 0 <= strpos($message->getBody()->getParts()[0]->getContent(), $html) - && 0 <= strpos($message->getBody()->getParts()[0]->getContent(), $text) - && 'multipart/alternative' == $message->getHeaders()->get('Content-Type')->getType(); + && str_contains($message->getBody()->getParts()[0]->getBody(), $text) + && str_contains($message->getBody()->getParts()[1]->getBody(), $html); }; - $transport = $this->createMock(\Laminas\Mail\Transport\TransportInterface::class); - $transport->expects($this->once())->method('send')->with($this->callback($callback)); $address = new Address('from@example.com', 'Sender TextName'); - $mailer = new Mailer($transport); + $mailer = $this->getMailer($callback); $body = $mailer->buildMultipartBody($text, $html); $mailer->send('to@example.com', $address, 'subject', $body); } + + /** + * Create mailer with a mock transport + * + * @param ?callable $callback Mock send method result callback + * + * @return Mailer + */ + protected function getMailer($callback = null) + { + $transport = $this->createMock(MailerInterface::class); + if ($callback) { + $transport->expects($this->once())->method('send')->with($this->callback($callback)); + } + return new Mailer($transport); + } }