diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a5dbb7..35b0db6c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* [PR-333](https://github.com/itk-dev/hoeringsportal/pull/333) + Sent notification mails on citizen proposal creation and publication + ## [3.1.0] - 2023-08-04 - Citizen proposal * [PR-331](https://github.com/itk-dev/hoeringsportal/pull/331) diff --git a/composer.json b/composer.json index 26256eaf..d32a1676 100755 --- a/composer.json +++ b/composer.json @@ -52,6 +52,16 @@ } } }, + "drupal/hoeringsportal_citizen_proposal": { + "type": "path", + "url": "web/modules/custom/hoeringsportal_citizen_proposal", + "options": { + "symlink": false, + "versions": { + "drupal/hoeringsportal_citizen_proposal": "1.0-dev" + } + } + }, "drupal/hoeringsportal_citizen_proposal_archiving": { "type": "path", "url": "web/modules/custom/hoeringsportal_citizen_proposal_archiving", @@ -100,6 +110,7 @@ "drupal/entityqueue": "^1.0@alpha", "drupal/field_group": "^3.0", "drupal/flag": "^4.0@alpha", + "drupal/hoeringsportal_citizen_proposal": "1.0-dev", "drupal/hoeringsportal_citizen_proposal_archiving": "1.0-dev", "drupal/hoeringsportal_data": "^1.0", "drupal/hoeringsportal_deskpro": "^1.0", diff --git a/composer.lock b/composer.lock index 5cabfbdf..2218df84 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": "91665df1e6c8571f67eae6e4069486ea", + "content-hash": "ad93a7db3976405d97d049079e3800e1", "packages": [ { "name": "asm89/stack-cors", @@ -3334,6 +3334,39 @@ "source": "https://git.drupalcode.org/project/flag" } }, + { + "name": "drupal/hoeringsportal_citizen_proposal", + "version": "1.0-dev", + "dist": { + "type": "path", + "url": "web/modules/custom/hoeringsportal_citizen_proposal", + "reference": "b271aaea66b060d03c05bfff6e1af0cae74f6e97" + }, + "require": { + "drupal/entity_events": "^2.0", + "drupal/symfony_mailer": "^1.3", + "drush/drush": "^11" + }, + "type": "drupal-custom-module", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^11" + } + } + }, + "license": [ + "GPL-2.0+" + ], + "description": "Citizen proposal", + "keywords": [ + "Drupal" + ], + "transport-options": { + "symlink": false, + "relative": true + } + }, { "name": "drupal/hoeringsportal_citizen_proposal_archiving", "version": "1.0-dev", @@ -4508,6 +4541,58 @@ "source": "https://git.drupalcode.org/project/search_autocomplete" } }, + { + "name": "drupal/symfony_mailer", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/symfony_mailer.git", + "reference": "1.3.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/symfony_mailer-1.3.0.zip", + "reference": "1.3.0", + "shasum": "60170ef1e9a11e89d2458f48dc556625fc97ab76" + }, + "require": { + "drupal/core": "^9.4 || ^10", + "html2text/html2text": "^4.0.1", + "symfony/mailer": "^5.3 || ^6.0", + "tijsverkoyen/css-to-inline-styles": "^2.2" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "1.3.0", + "datestamp": "1690969900", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^11" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "AdamPS", + "homepage": "https://www.drupal.org/user/2650563" + } + ], + "description": "Symfony Mailer", + "homepage": "https://www.drupal.org/project/symfony_mailer", + "support": { + "source": "https://git.drupalcode.org/project/symfony_mailer" + } + }, { "name": "drupal/system_status", "version": "2.9.0", @@ -5644,6 +5729,47 @@ ], "time": "2023-04-17T16:00:37+00:00" }, + { + "name": "html2text/html2text", + "version": "4.3.1", + "source": { + "type": "git", + "url": "https://github.com/mtibben/html2text.git", + "reference": "61ad68e934066a6f8df29a3d23a6460536d0855c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mtibben/html2text/zipball/61ad68e934066a6f8df29a3d23a6460536d0855c", + "reference": "61ad68e934066a6f8df29a3d23a6460536d0855c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-mbstring": "For best performance", + "symfony/polyfill-mbstring": "If you can't install ext-mbstring" + }, + "type": "library", + "autoload": { + "psr-4": { + "Html2Text\\": [ + "src/", + "test/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Converts HTML to formatted plain text", + "support": { + "issues": "https://github.com/mtibben/html2text/issues", + "source": "https://github.com/mtibben/html2text/tree/4.3.1" + }, + "time": "2020-04-16T23:44:31+00:00" + }, { "name": "itk-dev/composer-virtualenv", "version": "1.0.0", @@ -7671,6 +7797,56 @@ }, "time": "2021-11-05T16:50:12+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-client", "version": "1.0.2", @@ -8411,6 +8587,72 @@ ], "time": "2022-11-05T17:10:16+00:00" }, + { + "name": "symfony/css-selector", + "version": "v4.4.44", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/bd0a6737e48de45b4b0b7b6fc98c78404ddceaed", + "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v4.4.44" + }, + "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": "2022-06-27T13:16:42+00:00" + }, { "name": "symfony/debug", "version": "v4.4.44", @@ -9330,6 +9572,82 @@ ], "time": "2023-02-01T08:01:31+00:00" }, + { + "name": "symfony/mailer", + "version": "v5.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/6330cd465dfd8b7a07515757a1c37069075f7b0b", + "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=7.2.5", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.2.6|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "require-dev": { + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.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/v5.4.22" + }, + "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": "2023-03-10T10:15:32+00:00" + }, { "name": "symfony/mime", "version": "v5.4.13", @@ -11154,6 +11472,59 @@ ], "time": "2022-08-02T15:47:23+00:00" }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.6", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5 || ^7.0 || ^8.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6" + }, + "time": "2023-01-03T09:29:04+00:00" + }, { "name": "twig/twig", "version": "v2.15.5", @@ -15561,72 +15932,6 @@ ], "time": "2022-07-25T12:56:14+00:00" }, - { - "name": "symfony/css-selector", - "version": "v4.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/bd0a6737e48de45b4b0b7b6fc98c78404ddceaed", - "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed", - "shasum": "" - }, - "require": { - "php": ">=7.1.3", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Converts CSS selectors to XPath expressions", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/css-selector/tree/v4.4.44" - }, - "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": "2022-06-27T13:16:42+00:00" - }, { "name": "symfony/dom-crawler", "version": "v4.4.45", @@ -15921,6 +16226,7 @@ "drupal/email_registration": 5, "drupal/entityqueue": 15, "drupal/flag": 15, + "drupal/hoeringsportal_citizen_proposal": 20, "drupal/hoeringsportal_citizen_proposal_archiving": 20, "drupal/inline_entity_form": 5, "drupal/toolbar_visibility": 20 diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index 4aa4bcea..45c990fb 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -89,6 +89,7 @@ module: search: 0 search_autocomplete: 0 serialization: 0 + symfony_mailer: 0 system: 0 system_status: 0 taxonomy: 0 diff --git a/config/sync/filter.format.email_html.yml b/config/sync/filter.format.email_html.yml new file mode 100644 index 00000000..da98bbda --- /dev/null +++ b/config/sync/filter.format.email_html.yml @@ -0,0 +1,10 @@ +uuid: bf704798-b10e-4239-8d8f-8268557ff87c +langcode: da +status: true +dependencies: { } +_core: + default_config_hash: u5MXordtBE3SIs1x4qEUEBUFctQ0jjSn2vbPtgxvXuM +name: 'Email HTML' +format: email_html +weight: 0 +filters: { } diff --git a/config/sync/symfony_mailer.mailer_policy._.yml b/config/sync/symfony_mailer.mailer_policy._.yml new file mode 100644 index 00000000..324d68a6 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy._.yml @@ -0,0 +1,15 @@ +uuid: 8f00b079-62e0-4eee-8d86-5127205241ad +langcode: da +status: true +dependencies: { } +_core: + default_config_hash: oUtf135g-IPzmggtfsOOX5ZINxbSe5Kno64BN2NUKn0 +id: _ +configuration: + mailer_url_to_absolute: { } + mailer_inline_css: { } + mailer_wrap_and_convert: + plain: false + swiftmailer: false + email_theme: + theme: _active_fallback diff --git a/config/sync/symfony_mailer.mailer_policy.hoeringsportal_citizen_proposal.proposal_created.yml b/config/sync/symfony_mailer.mailer_policy.hoeringsportal_citizen_proposal.proposal_created.yml new file mode 100644 index 00000000..b865a91d --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.hoeringsportal_citizen_proposal.proposal_created.yml @@ -0,0 +1,13 @@ +uuid: 35259a5e-4fa3-40e8-b45c-0fb92d142992 +langcode: da +status: true +dependencies: + module: + - hoeringsportal_citizen_proposal +id: hoeringsportal_citizen_proposal.proposal_created +configuration: + email_subject: + value: 'Tak for dit borgerforslag på [site:url]' + email_theme: + theme: _active_fallback + mailer_url_to_absolute: { } diff --git a/config/sync/symfony_mailer.mailer_policy.symfony_mailer.test.yml b/config/sync/symfony_mailer.mailer_policy.symfony_mailer.test.yml new file mode 100644 index 00000000..977f3d5b --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.symfony_mailer.test.yml @@ -0,0 +1,20 @@ +uuid: 6f22f21d-59a0-44df-b848-e8c8c474386a +langcode: da +status: true +dependencies: { } +_core: + default_config_hash: kkNORryWq16MbI0VC3ObIOgBuWNVx3PtGq-hDSsxgIw +id: symfony_mailer.test +configuration: + email_body: + content: + value: |- +

You have mail

+

This is a test email from [site:name].

+

Have a great {{ day }}!

+

+ Drupal Symfony Mailer +

+ format: email_html + email_subject: + value: 'Test email from [site:name]' diff --git a/config/sync/symfony_mailer.mailer_policy.update.status_notify.yml b/config/sync/symfony_mailer.mailer_policy.update.status_notify.yml new file mode 100644 index 00000000..ce680870 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.update.status_notify.yml @@ -0,0 +1,26 @@ +uuid: 8dda16ea-18a8-4648-ae94-52135a66e6d7 +langcode: en +status: true +dependencies: + module: + - update +id: update.status_notify +configuration: + email_body: + content: + value: |- +

You need to take action to secure your server {{ site_name }}.

+ + +

See the available updates page for more information. + {% if update_manager %} + You can automatically install your updates using the Update manager. + {% endif %} + You can change your settings for what update notifications you receive.

+ format: email_html + email_subject: + value: 'New release(s) available for {{ site_name }}' diff --git a/config/sync/symfony_mailer.mailer_policy.user.cancel_confirm.yml b/config/sync/symfony_mailer.mailer_policy.user.cancel_confirm.yml new file mode 100644 index 00000000..f61c38bf --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.cancel_confirm.yml @@ -0,0 +1,18 @@ +uuid: 242d7849-2d9c-43ba-ae5f-cd8d7e40ebf3 +langcode: en +status: true +dependencies: + module: + - user +id: user.cancel_confirm +configuration: + email_subject: + value: 'Account cancellation request for [user:display-name] at [site:name]' + email_body: + content: + value: |- +

[user:display-name],

+

A request has been made to cancel your account at [site:name]. + You may now use this link to cancel your account.

+

Note: The cancellation of your account is not reversible. This link expires in one day and nothing will happen if it is not used.

+ format: email_html diff --git a/config/sync/symfony_mailer.mailer_policy.user.password_reset.yml b/config/sync/symfony_mailer.mailer_policy.user.password_reset.yml new file mode 100644 index 00000000..5a0470b0 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.password_reset.yml @@ -0,0 +1,18 @@ +uuid: 232f5a54-1681-41f3-905f-2e8c7b1fa94e +langcode: en +status: true +dependencies: + module: + - user +id: user.password_reset +configuration: + email_subject: + value: 'Replacement login information for [user:display-name] at [site:name]' + email_body: + content: + value: |- +

[user:display-name],

+

A request has been made to reset the password for your account at [site:name]. + You may now use this link to log in. + This link can only be used once to log in and will lead you to a page where you can set your password. It expires after one day and nothing will happen if it's not used.

+ format: email_html diff --git a/config/sync/symfony_mailer.mailer_policy.user.register_admin_created.yml b/config/sync/symfony_mailer.mailer_policy.user.register_admin_created.yml new file mode 100644 index 00000000..9910002e --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.register_admin_created.yml @@ -0,0 +1,23 @@ +uuid: b35a513e-f123-4e02-8d31-7c6ac3b624bf +langcode: en +status: true +dependencies: + module: + - user +id: user.register_admin_created +configuration: + email_subject: + value: 'An administrator created an account for you at [site:name]' + email_body: + content: + value: |- +

[user:display-name],

+

A site administrator at [site:name] has created an account for you. + You may now use this link to log in. It can be used only once and will lead you to a page where you can set your password.

+ +

After setting your password, you will be able to log in in the future using:

+ + format: email_html diff --git a/config/sync/symfony_mailer.mailer_policy.user.register_no_approval_required.yml b/config/sync/symfony_mailer.mailer_policy.user.register_no_approval_required.yml new file mode 100644 index 00000000..75342490 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.register_no_approval_required.yml @@ -0,0 +1,23 @@ +uuid: a9b24b24-6258-40e2-94cd-24c7625b7348 +langcode: en +status: true +dependencies: + module: + - user +id: user.register_no_approval_required +configuration: + email_subject: + value: 'Account details for [user:display-name] at [site:name]' + email_body: + content: + value: |- +

[user:display-name],

+

Thank you for registering at [site:name]. + You may now use this link to log in. It can be used only once and will lead you to a page where you can set your password.

+ +

After setting your password, you will be able to log in in the future using:

+ + format: email_html diff --git a/config/sync/symfony_mailer.mailer_policy.user.register_pending_approval.yml b/config/sync/symfony_mailer.mailer_policy.user.register_pending_approval.yml new file mode 100644 index 00000000..9a4197d3 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.register_pending_approval.yml @@ -0,0 +1,18 @@ +uuid: 07693ea8-f1e9-40de-aa68-3d176a886f5c +langcode: en +status: true +dependencies: + module: + - user +id: user.register_pending_approval +configuration: + email_subject: + value: 'Account details for [user:display-name] at [site:name] (pending admin approval)' + email_body: + content: + value: |- +

[user:display-name],

+

Thank you for registering at [site:name]. + Your application for an account is currently pending approval. + Once it has been approved, you will receive another email containing information about how to log in, set your password, and other details.

+ format: email_html diff --git a/config/sync/symfony_mailer.mailer_policy.user.register_pending_approval_admin.yml b/config/sync/symfony_mailer.mailer_policy.user.register_pending_approval_admin.yml new file mode 100644 index 00000000..377e289a --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.register_pending_approval_admin.yml @@ -0,0 +1,21 @@ +uuid: edc4da89-0bab-4c38-8154-112d04676a88 +langcode: en +status: true +dependencies: + module: + - user +id: user.register_pending_approval_admin +configuration: + email_subject: + value: 'Account details for [user:display-name] at [site:name] (pending admin approval)' + email_body: + content: + value: |- +

[user:display-name] has applied for an account at [site:name]. + You may now use this link to approve the request.

+ format: email_html + email_to: + addresses: + - + value: '' + display: '' diff --git a/config/sync/symfony_mailer.mailer_policy.user.status_activated.yml b/config/sync/symfony_mailer.mailer_policy.user.status_activated.yml new file mode 100644 index 00000000..4673c24e --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.status_activated.yml @@ -0,0 +1,23 @@ +uuid: b637189a-9902-449a-984f-318df7244fe6 +langcode: en +status: true +dependencies: + module: + - user +id: user.status_activated +configuration: + email_subject: + value: 'Your account details for [user:display-name] at [site:name] ([site:url])' + email_body: + content: + value: |- +

[user:display-name],

+

Your account at [site:name] has been activated. + You may now use this link to log in. It can be used only once and will lead you to a page where you can set your password.

+ +

After setting your password, you will be able to log in in the future using:

+ + format: email_html diff --git a/config/sync/symfony_mailer.mailer_policy.user.status_blocked.yml b/config/sync/symfony_mailer.mailer_policy.user.status_blocked.yml new file mode 100644 index 00000000..e99b55af --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.status_blocked.yml @@ -0,0 +1,18 @@ +uuid: 2724e3fc-8802-4646-80b3-4088bea82e4b +langcode: en +status: true +dependencies: + module: + - user +id: user.status_blocked +configuration: + email_subject: + value: 'Account details for [user:display-name] at [site:name] (blocked)' + email_body: + content: + value: |- +

[user:display-name],

+

Your account on [site:name] has been blocked.

+ format: email_html + email_skip_sending: + message: 'Notification disabled in settings' diff --git a/config/sync/symfony_mailer.mailer_policy.user.status_canceled.yml b/config/sync/symfony_mailer.mailer_policy.user.status_canceled.yml new file mode 100644 index 00000000..f048bf8f --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user.status_canceled.yml @@ -0,0 +1,18 @@ +uuid: 204c6b0a-fda7-4fd2-8296-e61abbce5f3a +langcode: en +status: true +dependencies: + module: + - user +id: user.status_canceled +configuration: + email_subject: + value: 'Account details for [user:display-name] at [site:name] (canceled)' + email_body: + content: + value: |- +

[user:display-name],

+

Your account on [site:name] has been canceled.

+ format: email_html + email_skip_sending: + message: 'Notification disabled in settings' diff --git a/config/sync/symfony_mailer.mailer_policy.user_registrationpassword.register_confirmation_with_pass.yml b/config/sync/symfony_mailer.mailer_policy.user_registrationpassword.register_confirmation_with_pass.yml new file mode 100644 index 00000000..b047baf9 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_policy.user_registrationpassword.register_confirmation_with_pass.yml @@ -0,0 +1,19 @@ +uuid: cf411036-bb07-472f-9a6d-24cf856dd606 +langcode: en +status: true +dependencies: { } +id: user_registrationpassword.register_confirmation_with_pass +configuration: + email_subject: + value: 'Account details for [user:display-name] at [site:name]' + email_body: + content: + value: |- +

[user:display-name],

+

Thank you for registering at [site:name]. + You may now use this link to log in. It can be used only once and you will be able to log in in the future using:

+ + format: email_html diff --git a/config/sync/symfony_mailer.mailer_transport.mailhog.yml b/config/sync/symfony_mailer.mailer_transport.mailhog.yml new file mode 100644 index 00000000..abb09896 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_transport.mailhog.yml @@ -0,0 +1,18 @@ +uuid: 19bddb9b-6e38-4452-9745-8e2bcdf2bd5d +langcode: da +status: true +dependencies: { } +id: mailhog +label: MailHog +plugin: smtp +configuration: + user: '' + pass: '' + host: mailhog + port: 1025 + query: + verify_peer: false + local_domain: '' + restart_threshold: null + restart_threshold_sleep: null + ping_threshold: null diff --git a/config/sync/symfony_mailer.mailer_transport.sendmail.yml b/config/sync/symfony_mailer.mailer_transport.sendmail.yml new file mode 100644 index 00000000..58a99ab8 --- /dev/null +++ b/config/sync/symfony_mailer.mailer_transport.sendmail.yml @@ -0,0 +1,12 @@ +uuid: 30e5e39a-1843-41a1-92cd-ced9234d67be +langcode: da +status: true +dependencies: { } +_core: + default_config_hash: JlhtZGzrKvb_1TFIVW-o2lEEoeutUxHkdpZ1WzIkJMY +id: sendmail +label: Sendmail +plugin: sendmail +configuration: + query: + command: '' diff --git a/config/sync/symfony_mailer.settings.yml b/config/sync/symfony_mailer.settings.yml new file mode 100644 index 00000000..3f94ceb4 --- /dev/null +++ b/config/sync/symfony_mailer.settings.yml @@ -0,0 +1,7 @@ +_core: + default_config_hash: 4qVz27mvzjCZOQkd03vi4Ipqcf8m53p6xPVW-G0tXIA +langcode: da +default_transport: mailhog +override: + update: 1 + user: 2 diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/README.md b/web/modules/custom/hoeringsportal_citizen_proposal/README.md index c3c62267..e5fcb18f 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/README.md +++ b/web/modules/custom/hoeringsportal_citizen_proposal/README.md @@ -27,3 +27,26 @@ $settings['proposal_period_length'] = '+180 days'; // The required votes for a proposal to pass. $settings['proposal_support_required'] = '5000'; ``` + +## Mails + +We use [Drupal Symfony Mailer](https://www.drupal.org/project/symfony_mailer) +and a custom mail builder, +[CitizenEmailBuilder](src/Plugin/EmailBuilder/CitizenEmailBuilder.php), to get +the recipient email address from the proposal (node). + +An confirmation email is sent to the citizen when a new proposal has been added +and an editor gets a mail notification as well. + +When a proposal is published an email is sent to the citizen. + +Mail subjects and contents are edited on `/admin/citizen_proposal#edit-emails`. + +### Testing and debugging email + +The Drush command `hoeringsportal-citizen-proposal:test-mail:send` can be used +to debug emails: + +```sh +drush hoeringsportal-citizen-proposal:test-mail:send --help +``` diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/composer.json b/web/modules/custom/hoeringsportal_citizen_proposal/composer.json new file mode 100644 index 00000000..89699f3b --- /dev/null +++ b/web/modules/custom/hoeringsportal_citizen_proposal/composer.json @@ -0,0 +1,27 @@ +{ + "name": "drupal/hoeringsportal_citizen_proposal", + "type": "drupal-custom-module", + "description": "Citizen proposal", + "keywords": ["Drupal"], + "license": "GPL-2.0+", + "minimum-stability": "dev", + "repositories": { + "drupal": { + "type": "composer", + "url": "https://packages.drupal.org/8", + "exclude": ["drupal/toolbar_visibility"] + } + }, + "require": { + "drupal/entity_events": "^2.0", + "drupal/symfony_mailer": "^1.3", + "drush/drush": "^11" + }, + "extra": { + "drush": { + "services": { + "drush.services.yml": "^11" + } + } + } +} diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/drush.services.yml b/web/modules/custom/hoeringsportal_citizen_proposal/drush.services.yml index 00e781d6..c1bfbe32 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/drush.services.yml +++ b/web/modules/custom/hoeringsportal_citizen_proposal/drush.services.yml @@ -4,3 +4,10 @@ services: - { name: drush.command } arguments: - '@Drupal\hoeringsportal_citizen_proposal\Helper\Helper' + + Drupal\hoeringsportal_citizen_proposal\Commands\TestMailCommand: + tags: + - { name: drush.command } + arguments: + - '@Drupal\hoeringsportal_citizen_proposal\Helper\Helper' + - '@Drupal\hoeringsportal_citizen_proposal\Helper\MailHelper' diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.info.yml b/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.info.yml index 262813a5..e4974979 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.info.yml +++ b/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.info.yml @@ -5,3 +5,4 @@ package: ITK core_version_requirement: ^8.8 || ^9 dependencies: - twig_tweak:twig_tweak + - drupal:symfony_mailer diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.install b/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.install index 784a7664..9d233aa4 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.install +++ b/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.install @@ -174,3 +174,12 @@ function hoeringsportal_citizen_proposal_update_9002(): void { ], ]); } + +/** + * Enables System Mailer module. + */ +function hoeringsportal_citizen_proposal_update_9003(): void { + \Drupal::service('module_installer')->install([ + 'symfony_mailer', + ]); +} diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.services.yml b/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.services.yml index 58e4260a..d9752e42 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.services.yml +++ b/web/modules/custom/hoeringsportal_citizen_proposal/hoeringsportal_citizen_proposal.services.yml @@ -14,3 +14,11 @@ services: - '@entity_type.manager' - '@datetime.time' - '@logger.channel.hoeringsportal_citizen_proposal' + + Drupal\hoeringsportal_citizen_proposal\Helper\MailHelper: + arguments: + - '@Drupal\hoeringsportal_citizen_proposal\Helper\Helper' + - '@email_factory' + - '@logger.channel.hoeringsportal_citizen_proposal' + tags: + - {name: event_subscriber} diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/src/Commands/TestMailCommand.php b/web/modules/custom/hoeringsportal_citizen_proposal/src/Commands/TestMailCommand.php new file mode 100644 index 00000000..e7d58c7c --- /dev/null +++ b/web/modules/custom/hoeringsportal_citizen_proposal/src/Commands/TestMailCommand.php @@ -0,0 +1,73 @@ +helper->loadCitizenProposal($proposalId); + // Overwrite the proposal author. + $proposal->get('field_author_email')->setValue($recipient); + + $eventType = 'create' === $event ? EntityEventType::INSERT : EntityEventType::UPDATE; + if (EntityEventType::UPDATE === $eventType) { + // Simulate that proposal changes from unpublished to published. + $proposal->original = clone $proposal; + $proposal->original->setUnpublished(); + $proposal->setPublished(); + } + $emails = $this->mailHelper->sendMails($proposal, $eventType); + + foreach ($emails as $email) { + if ($error = $email->getError()) { + $this->writeln($error); + } + else { + $this->writeln(sprintf('To: %s', implode( + ', ', + array_map( + static fn(AddressInterface $address) => $address->getEmail(), + $email->getTo() + ) + ))); + $this->writeln(sprintf('Subject: %s', $email->getSubject())); + $this->writeln('HMTL:'); + $this->writeln($email->getHtmlBody()); + $this->writeln(str_repeat('-', 80)); + $this->writeln('Text:'); + $this->writeln($email->getTextBody()); + } + } + } + +} diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/src/Form/ProposalAdminForm.php b/web/modules/custom/hoeringsportal_citizen_proposal/src/Form/ProposalAdminForm.php index 8aa7d97e..0a6abf92 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/src/Form/ProposalAdminForm.php +++ b/web/modules/custom/hoeringsportal_citizen_proposal/src/Form/ProposalAdminForm.php @@ -5,6 +5,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\State\State; +use Drupal\hoeringsportal_citizen_proposal\Helper\MailHelper; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -223,6 +224,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => $adminFormStateValues['sidebar_text']['value'] ?? '', ]; + $this->buildEmailsForm($form, $adminFormStateValues); + $form['actions']['#type'] = 'actions'; $form['actions']['submit'] = [ '#type' => 'submit', @@ -233,6 +236,65 @@ public function buildForm(array $form, FormStateInterface $form_state) { return $form; } + /** + * Build emails form. + * + * @param array $form + * The form. + * @param array $adminFormStateValues + * The admin form state values. + * + * @return array + * The form. + */ + private function buildEmailsForm(array &$form, array $adminFormStateValues): array { + $form['emails'] = [ + '#type' => 'details', + '#tree' => TRUE, + '#open' => TRUE, + '#title' => $this + ->t('Emails'), + ]; + + $form['emails']['description'] = [ + '#markup' => $this->t('You can use tokens in email subject and both tokens and Twig in email content.'), + ]; + + $form['emails']['email_editor'] = [ + '#type' => 'email', + '#title' => $this->t('Editor email address'), + '#required' => TRUE, + '#default_value' => $adminFormStateValues['emails']['email_editor'] ?? '', + ]; + + foreach ([ + MailHelper::MAILER_SUBTYPE_PROPOSAL_CREATED_CITIZEN => $this->t('Proposal created (citizen)'), + MailHelper::MAILER_SUBTYPE_PROPOSAL_CREATED_EDITOR => $this->t('Proposal created (editor)'), + MailHelper::MAILER_SUBTYPE_PROPOSAL_PUBLISHED_CITIZEN => $this->t('Proposal published (citizen)'), + ] as $key => $title) { + $form['emails'][$key] = [ + '#type' => 'fieldset', + '#title' => $title, + + 'subject' => [ + '#type' => 'textfield', + '#title' => $this->t('Subject'), + '#required' => TRUE, + '#default_value' => $adminFormStateValues['emails'][$key]['subject'] ?? '', + ], + 'content' => [ + '#type' => 'text_format', + '#title' => $this->t('Content'), + '#required' => TRUE, + '#format' => $adminFormStateValues['emails'][$key]['content']['format'] ?? 'email_html', + '#default_value' => $adminFormStateValues['emails'][$key]['content']['value'] ?? '', + ], + ]; + } + + return $form; + } + /** * {@inheritdoc} */ diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/Helper.php b/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/Helper.php index 6c9f94ed..4e550143 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/Helper.php +++ b/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/Helper.php @@ -3,6 +3,7 @@ namespace Drupal\hoeringsportal_citizen_proposal\Helper; use Drupal\Component\Datetime\TimeInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Database\Connection; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityInterface; @@ -19,6 +20,7 @@ use Drupal\Core\Url; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; use Drupal\hoeringsportal_citizen_proposal\Exception\RuntimeException; +use Drupal\hoeringsportal_citizen_proposal\Form\ProposalAdminForm; use Drupal\node\Entity\Node; use Drupal\node\NodeInterface; use Psr\Log\LoggerAwareInterface; @@ -112,6 +114,23 @@ public function preprocessForm(&$variables): void { $variables['admin_form_state_values'] = $this->state->get('citizen_proposal_admin_form_values'); } + /** + * Get admin form value. + * + * @return string|array|null + * The value if any. Otherwise the default value. + */ + public function getAdminFormValue(string|array $key, string $default = NULL) { + $adminFormStateValues = $this->state->get(ProposalAdminForm::ADMIN_FORM_VALUES_STATE_KEY) ?: []; + $value = NestedArray::getValue($adminFormStateValues, (array) $key); + + if (is_string($value)) { + $value = trim($value); + } + + return $value ?: $default; + } + /** * Preprocess citizen proposal nodes. */ diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/MailHelper.php b/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/MailHelper.php new file mode 100644 index 00000000..0e8f1c04 --- /dev/null +++ b/web/modules/custom/hoeringsportal_citizen_proposal/src/Helper/MailHelper.php @@ -0,0 +1,96 @@ +setLogger($logger); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + EntityEventType::INSERT => 'handle', + EntityEventType::UPDATE => 'handle', + ]; + } + + /** + * Event handler (cf. self::getSubscribedEvents()). + * + * @see self::getSubscribedEvents() + */ + public function handle(EntityEvent $event) { + $entity = $event->getEntity(); + if ($entity instanceof NodeInterface + && $this->citizenProposalHelper->isCitizenProposal($entity)) { + $this->sendMails($entity, $event->getEventType()); + } + } + + /** + * Send mail. + * + * @return \Drupal\symfony_mailer\EmailInterface[] + * The emails. + */ + public function sendMails(NodeInterface $proposal, string $eventType): array { + $emails = []; + + if (EntityEventType::INSERT === $eventType) { + $emails[] = $this->emailFactory->sendTypedEmail(self::MAILER_TYPE, + self::MAILER_SUBTYPE_PROPOSAL_CREATED_CITIZEN, $proposal); + $emails[] = $this->emailFactory->sendTypedEmail(self::MAILER_TYPE, + self::MAILER_SUBTYPE_PROPOSAL_CREATED_EDITOR, $proposal); + } + elseif (EntityEventType::UPDATE === $eventType) { + /** @var ?NodeInterface $proposalOriginal */ + $proposalOriginal = $proposal->original; + if ($proposal->isPublished() && !$proposalOriginal?->isPublished()) { + $emails[] = $this->emailFactory->sendTypedEmail(self::MAILER_TYPE, + self::MAILER_SUBTYPE_PROPOSAL_PUBLISHED_CITIZEN, $proposal); + } + } + + return $emails; + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = []) { + $this->logger->log($level, $message, $context); + } + +} diff --git a/web/modules/custom/hoeringsportal_citizen_proposal/src/Plugin/EmailBuilder/CitizenEmailBuilder.php b/web/modules/custom/hoeringsportal_citizen_proposal/src/Plugin/EmailBuilder/CitizenEmailBuilder.php new file mode 100644 index 00000000..940f2c10 --- /dev/null +++ b/web/modules/custom/hoeringsportal_citizen_proposal/src/Plugin/EmailBuilder/CitizenEmailBuilder.php @@ -0,0 +1,113 @@ +get(Helper::class), + $container->get('twig'), + $configuration, + $plugin_id, + $plugin_definition + ); + } + + /** + * {@inheritdoc} + */ + public function createParams(EmailInterface $email, NodeInterface $node = NULL) { + if (NULL === $node + || !$this->citizenProposalHelper->isCitizenProposal($node)) { + throw new \TypeError('Node must be a citizen proposal'); + } + $email->setParam('node', $node); + } + + /** + * {@inheritdoc} + */ + public function fromArray(EmailFactoryInterface $factory, array $message) { + return $factory->newTypedEmail($message['module'], $message['key'], $message['params']['node']); + } + + /** + * {@inheritdoc} + */ + public function build(EmailInterface $email) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $email->getParam('node'); + $to = $node->get('field_author_email')->value; + $email->setTo($to); + if (MailHelper::MAILER_SUBTYPE_PROPOSAL_CREATED_EDITOR === $email->getSubType()) { + $to = $this->citizenProposalHelper->getAdminFormValue([ + 'emails', + 'email_editor', + ]); + $email->setTo($to); + } + $email->setVariable('node', $node); + + $subType = $email->getSubType(); + $config = $this->citizenProposalHelper->getAdminFormValue([ + 'emails', + $subType, + ]); + if (!isset($config['subject'], $config['content']['value'])) { + throw new RuntimeException(sprintf('Email %s/%s not configured.', $email->getType(), $subType)); + } + ['subject' => $subject, 'content' => $content] = $config; + $content['value'] = $this->twig->renderInline($content['value'], $email->getVariables()); + + $email + ->setSubject($subject, TRUE) + ->setBody([ + '#type' => 'processed_text', + '#text' => $content['value'], + '#format' => $content['format'] ?? filter_default_format(), + ]); + + parent::build($email); + } + +} diff --git a/web/modules/custom/hoeringsportal_citizen_proposal_archiving/src/Helper/Helper.php b/web/modules/custom/hoeringsportal_citizen_proposal_archiving/src/Helper/Helper.php index 8b4083b4..2c209c06 100644 --- a/web/modules/custom/hoeringsportal_citizen_proposal_archiving/src/Helper/Helper.php +++ b/web/modules/custom/hoeringsportal_citizen_proposal_archiving/src/Helper/Helper.php @@ -80,8 +80,8 @@ public static function getSubscribedEvents() { */ public function update(EntityEvent $event) { $entity = $event->getEntity(); - if ($entity instanceof NodeInterface && - 'citizen_proposal' === $entity->bundle()) { + if ($entity instanceof NodeInterface + && $this->citizenProposalHelper->isCitizenProposal($entity)) { $this->createJob($entity); } } diff --git a/web/themes/custom/hoeringsportal/templates/email/citizen-proposal/email-wrap--hoeringsportal-citizen-proposal.html.twig b/web/themes/custom/hoeringsportal/templates/email/citizen-proposal/email-wrap--hoeringsportal-citizen-proposal.html.twig new file mode 100644 index 00000000..2a398f22 --- /dev/null +++ b/web/themes/custom/hoeringsportal/templates/email/citizen-proposal/email-wrap--hoeringsportal-citizen-proposal.html.twig @@ -0,0 +1,32 @@ +{# @see web/modules/contrib/symfony_mailer/templates/email-wrap.html.twig #} +{% + set classes = [ + 'email-type-' ~ type|clean_class, + 'email-sub-type-' ~ sub_type|clean_class, + ] +%} + +{% if is_html %} + + + + + + + + + + +
+ Aarhus Kommune Logo +
+
+ {{ body }} +
+
+ + + +{% else %} +{{ body }} +{% endif %}