mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
Add plugin-extensible notification API (#7775)
This commit is contained in:
1 parent
a8e1afba73
commit
4d021a93e3
16 files changed
+1153
-3
No files matched your search
@@ -0,0 +1,72 @@
|
||||
name: Notification API Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/notification-api-coverage.yml'
|
||||
- '.github/actions/setup-php-composer/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'lib/CactiEmailChannel.php'
|
||||
- 'lib/CactiNotification.php'
|
||||
- 'lib/CactiRecipient.php'
|
||||
- 'lib/api_notification.php'
|
||||
- 'phpunit-notification-coverage.xml'
|
||||
- 'tests/Unit/Core/CactiNotificationApiTest.php'
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/notification-api-coverage.yml'
|
||||
- '.github/actions/setup-php-composer/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'lib/CactiEmailChannel.php'
|
||||
- 'lib/CactiNotification.php'
|
||||
- 'lib/CactiRecipient.php'
|
||||
- 'lib/api_notification.php'
|
||||
- 'phpunit-notification-coverage.xml'
|
||||
- 'tests/Unit/Core/CactiNotificationApiTest.php'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: notification-api-coverage-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ALLOW_SUPERUSER: 1
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Notification API 100% coverage
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup locked PHP 8.1 dependencies
|
||||
uses: ./.github/actions/setup-php-composer
|
||||
with:
|
||||
php-version: '8.1'
|
||||
coverage: xdebug
|
||||
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring
|
||||
dependency-mode: locked-notification-api-coverage
|
||||
install-args: --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Enforce line coverage
|
||||
env:
|
||||
XDEBUG_MODE: coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
include/vendor/bin/pest \
|
||||
--configuration=phpunit-notification-coverage.xml \
|
||||
--coverage \
|
||||
--coverage-text \
|
||||
--min=100
|
||||
@@ -59,6 +59,7 @@
|
||||
"symfony/filesystem": "^6.4",
|
||||
"symfony/http-foundation": "^6.4",
|
||||
"symfony/mailer": "^6.4",
|
||||
"symfony/notifier": "^6.4",
|
||||
"symfony/validator": "^6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
Generated
+85
-1
@@ -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": "af5d264d94fc6720a43169e60d4d650e",
|
||||
"content-hash": "6795c9d58ef8594663378567eaf66aa1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
@@ -2534,6 +2534,90 @@
|
||||
],
|
||||
"time": "2026-07-29T07:59:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/notifier",
|
||||
"version": "v6.4.34",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/notifier.git",
|
||||
"reference": "16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/notifier/zipball/16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9",
|
||||
"reference": "16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/event-dispatcher": "<5.4",
|
||||
"symfony/event-dispatcher-contracts": "<2.5",
|
||||
"symfony/http-client-contracts": "<2.5",
|
||||
"symfony/http-kernel": "<5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^6.4|^7.0",
|
||||
"symfony/event-dispatcher-contracts": "^2.5|^3",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/http-client-contracts": "^2.5|^3",
|
||||
"symfony/http-foundation": "^5.4|^6.0|^7.0",
|
||||
"symfony/messenger": "^5.4|^6.0|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\": ""
|
||||
},
|
||||
"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": "Sends notifications via one or more channels (email, SMS, ...)",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notification",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/notifier/tree/v6.4.34"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-18T07:27:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.37.0",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Notification API
|
||||
|
||||
Cacti provides a plugin-extensible notification API backed by Symfony
|
||||
Notifier. The API uses Cacti's existing mail settings for the built-in email
|
||||
channel, including native mail, sendmail, SMTP, and OAuth2 delivery.
|
||||
|
||||
This is an internal PHP API for Cacti core and plugins. It is not an HTTP
|
||||
endpoint.
|
||||
|
||||
## Send a notification
|
||||
|
||||
Load the API and pass a subject, content, and one or more recipients:
|
||||
|
||||
```php
|
||||
require_once(CACTI_PATH_LIBRARY . '/api_notification.php');
|
||||
|
||||
$sent = api_notification_send(
|
||||
'Device unavailable',
|
||||
'The core router is not responding.',
|
||||
'Operations <ops@example.com>'
|
||||
);
|
||||
```
|
||||
|
||||
The recipient argument accepts the same email forms as Cacti's mailer, a
|
||||
Symfony `RecipientInterface`, or a list of Symfony recipients. The function
|
||||
returns `true` when every selected channel accepts every recipient. Delivery
|
||||
errors are logged to the `NOTIFIER` facility and return `false`.
|
||||
|
||||
Email-specific options preserve the capabilities of Cacti's mailer:
|
||||
|
||||
```php
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
|
||||
api_notification_send(
|
||||
'Nightly report',
|
||||
'<h1>Report complete</h1>',
|
||||
'reports@example.com',
|
||||
options: [
|
||||
'importance' => Notification::IMPORTANCE_LOW,
|
||||
'email' => [
|
||||
'html' => true,
|
||||
'text' => 'Report complete',
|
||||
'cc' => 'audit@example.com',
|
||||
'reply_to' => 'noreply@example.com',
|
||||
'attachments' => $attachments,
|
||||
'headers' => ['X-Cacti-Source' => 'reports'],
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
Supported email options are `from`, `cc`, `bcc`, `reply_to`, `html`, `text`,
|
||||
`attachments`, `headers`, and `expand_ids`.
|
||||
|
||||
## Add a plugin channel
|
||||
|
||||
Plugins can register the `notification_channels` hook. The hook receives and
|
||||
must return an array keyed by channel name. Each value must implement Symfony's
|
||||
`ChannelInterface`:
|
||||
|
||||
```php
|
||||
function plugin_name_notification_channels(array $channels) : array {
|
||||
$channels['chat'] = new PluginChatChannel();
|
||||
|
||||
return $channels;
|
||||
}
|
||||
```
|
||||
|
||||
Call the API with the registered channel name and an appropriate Symfony
|
||||
recipient:
|
||||
|
||||
```php
|
||||
api_notification_send(
|
||||
'Device unavailable',
|
||||
'The core router is not responding.',
|
||||
$recipient,
|
||||
['email', 'chat']
|
||||
);
|
||||
```
|
||||
|
||||
Channel implementations control their own credentials and configuration.
|
||||
Secrets must remain in Cacti settings or another protected runtime store, not
|
||||
in notification content or source files.
|
||||
+78
@@ -799,6 +799,84 @@ return array(
|
||||
'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHtmlBodyContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php',
|
||||
'Symfony\\Component\\Mime\\Test\\Constraint\\EmailSubjectContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailSubjectContains.php',
|
||||
'Symfony\\Component\\Mime\\Test\\Constraint\\EmailTextBodyContains' => $vendorDir . '/symfony/mime/Test/Constraint/EmailTextBodyContains.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\AbstractChannel' => $vendorDir . '/symfony/notifier/Channel/AbstractChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\BrowserChannel' => $vendorDir . '/symfony/notifier/Channel/BrowserChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChannelInterface' => $vendorDir . '/symfony/notifier/Channel/ChannelInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChannelPolicy' => $vendorDir . '/symfony/notifier/Channel/ChannelPolicy.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChannelPolicyInterface' => $vendorDir . '/symfony/notifier/Channel/ChannelPolicyInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChatChannel' => $vendorDir . '/symfony/notifier/Channel/ChatChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\EmailChannel' => $vendorDir . '/symfony/notifier/Channel/EmailChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\PushChannel' => $vendorDir . '/symfony/notifier/Channel/PushChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\SmsChannel' => $vendorDir . '/symfony/notifier/Channel/SmsChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Chatter' => $vendorDir . '/symfony/notifier/Chatter.php',
|
||||
'Symfony\\Component\\Notifier\\ChatterInterface' => $vendorDir . '/symfony/notifier/ChatterInterface.php',
|
||||
'Symfony\\Component\\Notifier\\DataCollector\\NotificationDataCollector' => $vendorDir . '/symfony/notifier/DataCollector/NotificationDataCollector.php',
|
||||
'Symfony\\Component\\Notifier\\EventListener\\NotificationLoggerListener' => $vendorDir . '/symfony/notifier/EventListener/NotificationLoggerListener.php',
|
||||
'Symfony\\Component\\Notifier\\EventListener\\SendFailedMessageToNotifierListener' => $vendorDir . '/symfony/notifier/EventListener/SendFailedMessageToNotifierListener.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\FailedMessageEvent' => $vendorDir . '/symfony/notifier/Event/FailedMessageEvent.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\MessageEvent' => $vendorDir . '/symfony/notifier/Event/MessageEvent.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\NotificationEvents' => $vendorDir . '/symfony/notifier/Event/NotificationEvents.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\SentMessageEvent' => $vendorDir . '/symfony/notifier/Event/SentMessageEvent.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/notifier/Exception/ExceptionInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\FlashMessageImportanceMapperException' => $vendorDir . '/symfony/notifier/Exception/FlashMessageImportanceMapperException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\IncompleteDsnException' => $vendorDir . '/symfony/notifier/Exception/IncompleteDsnException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/notifier/Exception/InvalidArgumentException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\LengthException' => $vendorDir . '/symfony/notifier/Exception/LengthException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\LogicException' => $vendorDir . '/symfony/notifier/Exception/LogicException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\MissingRequiredOptionException' => $vendorDir . '/symfony/notifier/Exception/MissingRequiredOptionException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\MultipleExclusiveOptionsUsedException' => $vendorDir . '/symfony/notifier/Exception/MultipleExclusiveOptionsUsedException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\RuntimeException' => $vendorDir . '/symfony/notifier/Exception/RuntimeException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\TransportException' => $vendorDir . '/symfony/notifier/Exception/TransportException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\TransportExceptionInterface' => $vendorDir . '/symfony/notifier/Exception/TransportExceptionInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\UnsupportedMessageTypeException' => $vendorDir . '/symfony/notifier/Exception/UnsupportedMessageTypeException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\UnsupportedSchemeException' => $vendorDir . '/symfony/notifier/Exception/UnsupportedSchemeException.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\AbstractFlashMessageImportanceMapper' => $vendorDir . '/symfony/notifier/FlashMessage/AbstractFlashMessageImportanceMapper.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\BootstrapFlashMessageImportanceMapper' => $vendorDir . '/symfony/notifier/FlashMessage/BootstrapFlashMessageImportanceMapper.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\DefaultFlashMessageImportanceMapper' => $vendorDir . '/symfony/notifier/FlashMessage/DefaultFlashMessageImportanceMapper.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\FlashMessageImportanceMapperInterface' => $vendorDir . '/symfony/notifier/FlashMessage/FlashMessageImportanceMapperInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\ChatMessage' => $vendorDir . '/symfony/notifier/Message/ChatMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\EmailMessage' => $vendorDir . '/symfony/notifier/Message/EmailMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\FromNotificationInterface' => $vendorDir . '/symfony/notifier/Message/FromNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\MessageInterface' => $vendorDir . '/symfony/notifier/Message/MessageInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\MessageOptionsInterface' => $vendorDir . '/symfony/notifier/Message/MessageOptionsInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\NullMessage' => $vendorDir . '/symfony/notifier/Message/NullMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\PushMessage' => $vendorDir . '/symfony/notifier/Message/PushMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\SentMessage' => $vendorDir . '/symfony/notifier/Message/SentMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\SmsMessage' => $vendorDir . '/symfony/notifier/Message/SmsMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Messenger\\MessageHandler' => $vendorDir . '/symfony/notifier/Messenger/MessageHandler.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\ChatNotificationInterface' => $vendorDir . '/symfony/notifier/Notification/ChatNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface' => $vendorDir . '/symfony/notifier/Notification/EmailNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\Notification' => $vendorDir . '/symfony/notifier/Notification/Notification.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\PushNotificationInterface' => $vendorDir . '/symfony/notifier/Notification/PushNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\SmsNotificationInterface' => $vendorDir . '/symfony/notifier/Notification/SmsNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notifier' => $vendorDir . '/symfony/notifier/Notifier.php',
|
||||
'Symfony\\Component\\Notifier\\NotifierInterface' => $vendorDir . '/symfony/notifier/NotifierInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\EmailRecipientInterface' => $vendorDir . '/symfony/notifier/Recipient/EmailRecipientInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\EmailRecipientTrait' => $vendorDir . '/symfony/notifier/Recipient/EmailRecipientTrait.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\NoRecipient' => $vendorDir . '/symfony/notifier/Recipient/NoRecipient.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\Recipient' => $vendorDir . '/symfony/notifier/Recipient/Recipient.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\RecipientInterface' => $vendorDir . '/symfony/notifier/Recipient/RecipientInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\SmsRecipientInterface' => $vendorDir . '/symfony/notifier/Recipient/SmsRecipientInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\SmsRecipientTrait' => $vendorDir . '/symfony/notifier/Recipient/SmsRecipientTrait.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationCount' => $vendorDir . '/symfony/notifier/Test/Constraint/NotificationCount.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationIsQueued' => $vendorDir . '/symfony/notifier/Test/Constraint/NotificationIsQueued.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationSubjectContains' => $vendorDir . '/symfony/notifier/Test/Constraint/NotificationSubjectContains.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationTransportIsEqual' => $vendorDir . '/symfony/notifier/Test/Constraint/NotificationTransportIsEqual.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\TransportFactoryTestCase' => $vendorDir . '/symfony/notifier/Test/TransportFactoryTestCase.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\TransportTestCase' => $vendorDir . '/symfony/notifier/Test/TransportTestCase.php',
|
||||
'Symfony\\Component\\Notifier\\Texter' => $vendorDir . '/symfony/notifier/Texter.php',
|
||||
'Symfony\\Component\\Notifier\\TexterInterface' => $vendorDir . '/symfony/notifier/TexterInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Transport' => $vendorDir . '/symfony/notifier/Transport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\AbstractTransport' => $vendorDir . '/symfony/notifier/Transport/AbstractTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\AbstractTransportFactory' => $vendorDir . '/symfony/notifier/Transport/AbstractTransportFactory.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\Dsn' => $vendorDir . '/symfony/notifier/Transport/Dsn.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\FailoverTransport' => $vendorDir . '/symfony/notifier/Transport/FailoverTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\NullTransport' => $vendorDir . '/symfony/notifier/Transport/NullTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\NullTransportFactory' => $vendorDir . '/symfony/notifier/Transport/NullTransportFactory.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\RoundRobinTransport' => $vendorDir . '/symfony/notifier/Transport/RoundRobinTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\TransportFactoryInterface' => $vendorDir . '/symfony/notifier/Transport/TransportFactoryInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\TransportInterface' => $vendorDir . '/symfony/notifier/Transport/TransportInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\Transports' => $vendorDir . '/symfony/notifier/Transport/Transports.php',
|
||||
'Symfony\\Component\\Validator\\Attribute\\HasNamedArguments' => $vendorDir . '/symfony/validator/Attribute/HasNamedArguments.php',
|
||||
'Symfony\\Component\\Validator\\Command\\DebugCommand' => $vendorDir . '/symfony/validator/Command/DebugCommand.php',
|
||||
'Symfony\\Component\\Validator\\Constraint' => $vendorDir . '/symfony/validator/Constraint.php',
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ return array(
|
||||
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
|
||||
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
|
||||
'Symfony\\Component\\Validator\\' => array($vendorDir . '/symfony/validator'),
|
||||
'Symfony\\Component\\Notifier\\' => array($vendorDir . '/symfony/notifier'),
|
||||
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'),
|
||||
'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'),
|
||||
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
||||
|
||||
+83
@@ -37,6 +37,7 @@ class ComposerStaticInit4457df976b6743d4cf060b8bfc645118
|
||||
'Symfony\\Contracts\\Service\\' => 26,
|
||||
'Symfony\\Contracts\\EventDispatcher\\' => 34,
|
||||
'Symfony\\Component\\Validator\\' => 28,
|
||||
'Symfony\\Component\\Notifier\\' => 27,
|
||||
'Symfony\\Component\\Mime\\' => 23,
|
||||
'Symfony\\Component\\Mailer\\' => 25,
|
||||
'Symfony\\Component\\HttpFoundation\\' => 33,
|
||||
@@ -139,6 +140,10 @@ class ComposerStaticInit4457df976b6743d4cf060b8bfc645118
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/validator',
|
||||
),
|
||||
'Symfony\\Component\\Notifier\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/notifier',
|
||||
),
|
||||
'Symfony\\Component\\Mime\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/mime',
|
||||
@@ -1047,6 +1052,84 @@ class ComposerStaticInit4457df976b6743d4cf060b8bfc645118
|
||||
'Symfony\\Component\\Mime\\Test\\Constraint\\EmailHtmlBodyContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailHtmlBodyContains.php',
|
||||
'Symfony\\Component\\Mime\\Test\\Constraint\\EmailSubjectContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailSubjectContains.php',
|
||||
'Symfony\\Component\\Mime\\Test\\Constraint\\EmailTextBodyContains' => __DIR__ . '/..' . '/symfony/mime/Test/Constraint/EmailTextBodyContains.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\AbstractChannel' => __DIR__ . '/..' . '/symfony/notifier/Channel/AbstractChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\BrowserChannel' => __DIR__ . '/..' . '/symfony/notifier/Channel/BrowserChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChannelInterface' => __DIR__ . '/..' . '/symfony/notifier/Channel/ChannelInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChannelPolicy' => __DIR__ . '/..' . '/symfony/notifier/Channel/ChannelPolicy.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChannelPolicyInterface' => __DIR__ . '/..' . '/symfony/notifier/Channel/ChannelPolicyInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\ChatChannel' => __DIR__ . '/..' . '/symfony/notifier/Channel/ChatChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\EmailChannel' => __DIR__ . '/..' . '/symfony/notifier/Channel/EmailChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\PushChannel' => __DIR__ . '/..' . '/symfony/notifier/Channel/PushChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Channel\\SmsChannel' => __DIR__ . '/..' . '/symfony/notifier/Channel/SmsChannel.php',
|
||||
'Symfony\\Component\\Notifier\\Chatter' => __DIR__ . '/..' . '/symfony/notifier/Chatter.php',
|
||||
'Symfony\\Component\\Notifier\\ChatterInterface' => __DIR__ . '/..' . '/symfony/notifier/ChatterInterface.php',
|
||||
'Symfony\\Component\\Notifier\\DataCollector\\NotificationDataCollector' => __DIR__ . '/..' . '/symfony/notifier/DataCollector/NotificationDataCollector.php',
|
||||
'Symfony\\Component\\Notifier\\EventListener\\NotificationLoggerListener' => __DIR__ . '/..' . '/symfony/notifier/EventListener/NotificationLoggerListener.php',
|
||||
'Symfony\\Component\\Notifier\\EventListener\\SendFailedMessageToNotifierListener' => __DIR__ . '/..' . '/symfony/notifier/EventListener/SendFailedMessageToNotifierListener.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\FailedMessageEvent' => __DIR__ . '/..' . '/symfony/notifier/Event/FailedMessageEvent.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\MessageEvent' => __DIR__ . '/..' . '/symfony/notifier/Event/MessageEvent.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\NotificationEvents' => __DIR__ . '/..' . '/symfony/notifier/Event/NotificationEvents.php',
|
||||
'Symfony\\Component\\Notifier\\Event\\SentMessageEvent' => __DIR__ . '/..' . '/symfony/notifier/Event/SentMessageEvent.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/notifier/Exception/ExceptionInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\FlashMessageImportanceMapperException' => __DIR__ . '/..' . '/symfony/notifier/Exception/FlashMessageImportanceMapperException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\IncompleteDsnException' => __DIR__ . '/..' . '/symfony/notifier/Exception/IncompleteDsnException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/notifier/Exception/InvalidArgumentException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\LengthException' => __DIR__ . '/..' . '/symfony/notifier/Exception/LengthException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/notifier/Exception/LogicException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\MissingRequiredOptionException' => __DIR__ . '/..' . '/symfony/notifier/Exception/MissingRequiredOptionException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\MultipleExclusiveOptionsUsedException' => __DIR__ . '/..' . '/symfony/notifier/Exception/MultipleExclusiveOptionsUsedException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/notifier/Exception/RuntimeException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\TransportException' => __DIR__ . '/..' . '/symfony/notifier/Exception/TransportException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\TransportExceptionInterface' => __DIR__ . '/..' . '/symfony/notifier/Exception/TransportExceptionInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\UnsupportedMessageTypeException' => __DIR__ . '/..' . '/symfony/notifier/Exception/UnsupportedMessageTypeException.php',
|
||||
'Symfony\\Component\\Notifier\\Exception\\UnsupportedSchemeException' => __DIR__ . '/..' . '/symfony/notifier/Exception/UnsupportedSchemeException.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\AbstractFlashMessageImportanceMapper' => __DIR__ . '/..' . '/symfony/notifier/FlashMessage/AbstractFlashMessageImportanceMapper.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\BootstrapFlashMessageImportanceMapper' => __DIR__ . '/..' . '/symfony/notifier/FlashMessage/BootstrapFlashMessageImportanceMapper.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\DefaultFlashMessageImportanceMapper' => __DIR__ . '/..' . '/symfony/notifier/FlashMessage/DefaultFlashMessageImportanceMapper.php',
|
||||
'Symfony\\Component\\Notifier\\FlashMessage\\FlashMessageImportanceMapperInterface' => __DIR__ . '/..' . '/symfony/notifier/FlashMessage/FlashMessageImportanceMapperInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\ChatMessage' => __DIR__ . '/..' . '/symfony/notifier/Message/ChatMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\EmailMessage' => __DIR__ . '/..' . '/symfony/notifier/Message/EmailMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\FromNotificationInterface' => __DIR__ . '/..' . '/symfony/notifier/Message/FromNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\MessageInterface' => __DIR__ . '/..' . '/symfony/notifier/Message/MessageInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\MessageOptionsInterface' => __DIR__ . '/..' . '/symfony/notifier/Message/MessageOptionsInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\NullMessage' => __DIR__ . '/..' . '/symfony/notifier/Message/NullMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\PushMessage' => __DIR__ . '/..' . '/symfony/notifier/Message/PushMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\SentMessage' => __DIR__ . '/..' . '/symfony/notifier/Message/SentMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Message\\SmsMessage' => __DIR__ . '/..' . '/symfony/notifier/Message/SmsMessage.php',
|
||||
'Symfony\\Component\\Notifier\\Messenger\\MessageHandler' => __DIR__ . '/..' . '/symfony/notifier/Messenger/MessageHandler.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\ChatNotificationInterface' => __DIR__ . '/..' . '/symfony/notifier/Notification/ChatNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface' => __DIR__ . '/..' . '/symfony/notifier/Notification/EmailNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\Notification' => __DIR__ . '/..' . '/symfony/notifier/Notification/Notification.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\PushNotificationInterface' => __DIR__ . '/..' . '/symfony/notifier/Notification/PushNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notification\\SmsNotificationInterface' => __DIR__ . '/..' . '/symfony/notifier/Notification/SmsNotificationInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Notifier' => __DIR__ . '/..' . '/symfony/notifier/Notifier.php',
|
||||
'Symfony\\Component\\Notifier\\NotifierInterface' => __DIR__ . '/..' . '/symfony/notifier/NotifierInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\EmailRecipientInterface' => __DIR__ . '/..' . '/symfony/notifier/Recipient/EmailRecipientInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\EmailRecipientTrait' => __DIR__ . '/..' . '/symfony/notifier/Recipient/EmailRecipientTrait.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\NoRecipient' => __DIR__ . '/..' . '/symfony/notifier/Recipient/NoRecipient.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\Recipient' => __DIR__ . '/..' . '/symfony/notifier/Recipient/Recipient.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\RecipientInterface' => __DIR__ . '/..' . '/symfony/notifier/Recipient/RecipientInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\SmsRecipientInterface' => __DIR__ . '/..' . '/symfony/notifier/Recipient/SmsRecipientInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Recipient\\SmsRecipientTrait' => __DIR__ . '/..' . '/symfony/notifier/Recipient/SmsRecipientTrait.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationCount' => __DIR__ . '/..' . '/symfony/notifier/Test/Constraint/NotificationCount.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationIsQueued' => __DIR__ . '/..' . '/symfony/notifier/Test/Constraint/NotificationIsQueued.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationSubjectContains' => __DIR__ . '/..' . '/symfony/notifier/Test/Constraint/NotificationSubjectContains.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\Constraint\\NotificationTransportIsEqual' => __DIR__ . '/..' . '/symfony/notifier/Test/Constraint/NotificationTransportIsEqual.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\TransportFactoryTestCase' => __DIR__ . '/..' . '/symfony/notifier/Test/TransportFactoryTestCase.php',
|
||||
'Symfony\\Component\\Notifier\\Test\\TransportTestCase' => __DIR__ . '/..' . '/symfony/notifier/Test/TransportTestCase.php',
|
||||
'Symfony\\Component\\Notifier\\Texter' => __DIR__ . '/..' . '/symfony/notifier/Texter.php',
|
||||
'Symfony\\Component\\Notifier\\TexterInterface' => __DIR__ . '/..' . '/symfony/notifier/TexterInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Transport' => __DIR__ . '/..' . '/symfony/notifier/Transport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\AbstractTransport' => __DIR__ . '/..' . '/symfony/notifier/Transport/AbstractTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\AbstractTransportFactory' => __DIR__ . '/..' . '/symfony/notifier/Transport/AbstractTransportFactory.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\Dsn' => __DIR__ . '/..' . '/symfony/notifier/Transport/Dsn.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\FailoverTransport' => __DIR__ . '/..' . '/symfony/notifier/Transport/FailoverTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\NullTransport' => __DIR__ . '/..' . '/symfony/notifier/Transport/NullTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\NullTransportFactory' => __DIR__ . '/..' . '/symfony/notifier/Transport/NullTransportFactory.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\RoundRobinTransport' => __DIR__ . '/..' . '/symfony/notifier/Transport/RoundRobinTransport.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\TransportFactoryInterface' => __DIR__ . '/..' . '/symfony/notifier/Transport/TransportFactoryInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\TransportInterface' => __DIR__ . '/..' . '/symfony/notifier/Transport/TransportInterface.php',
|
||||
'Symfony\\Component\\Notifier\\Transport\\Transports' => __DIR__ . '/..' . '/symfony/notifier/Transport/Transports.php',
|
||||
'Symfony\\Component\\Validator\\Attribute\\HasNamedArguments' => __DIR__ . '/..' . '/symfony/validator/Attribute/HasNamedArguments.php',
|
||||
'Symfony\\Component\\Validator\\Command\\DebugCommand' => __DIR__ . '/..' . '/symfony/validator/Command/DebugCommand.php',
|
||||
'Symfony\\Component\\Validator\\Constraint' => __DIR__ . '/..' . '/symfony/validator/Constraint.php',
|
||||
|
||||
+87
@@ -2639,6 +2639,93 @@
|
||||
],
|
||||
"install-path": "../symfony/mime"
|
||||
},
|
||||
{
|
||||
"name": "symfony/notifier",
|
||||
"version": "v6.4.34",
|
||||
"version_normalized": "6.4.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/notifier.git",
|
||||
"reference": "16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/notifier/zipball/16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9",
|
||||
"reference": "16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/event-dispatcher": "<5.4",
|
||||
"symfony/event-dispatcher-contracts": "<2.5",
|
||||
"symfony/http-client-contracts": "<2.5",
|
||||
"symfony/http-kernel": "<5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^6.4|^7.0",
|
||||
"symfony/event-dispatcher-contracts": "^2.5|^3",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/http-client-contracts": "^2.5|^3",
|
||||
"symfony/http-foundation": "^5.4|^6.0|^7.0",
|
||||
"symfony/messenger": "^5.4|^6.0|^7.0"
|
||||
},
|
||||
"time": "2026-02-18T07:27:25+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\": ""
|
||||
},
|
||||
"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": "Sends notifications via one or more channels (email, SMS, ...)",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notification",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/notifier/tree/v6.4.34"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"install-path": "../symfony/notifier"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.37.0",
|
||||
|
||||
+9
@@ -388,6 +388,15 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/notifier' => array(
|
||||
'pretty_version' => 'v6.4.34',
|
||||
'version' => '6.4.34.0',
|
||||
'reference' => '16a6179e8e6d6e3c71e592e59cdd649f86f1a6f9',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/notifier',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-ctype' => array(
|
||||
'pretty_version' => 'v1.37.0',
|
||||
'version' => '1.37.0.0',
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Notifier\Channel\ChannelInterface;
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\RuntimeException;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
/**
|
||||
* Delivers Symfony notifications through Cacti's configured mailer.
|
||||
*/
|
||||
final class CactiEmailChannel implements ChannelInterface {
|
||||
private Closure $mailer;
|
||||
|
||||
public function __construct(?callable $mailer = null) {
|
||||
if ($mailer === null) {
|
||||
$mailer = static fn (...$arguments) : string => (string) call_user_func_array('mailer', $arguments);
|
||||
}
|
||||
|
||||
$this->mailer = Closure::fromCallable($mailer);
|
||||
}
|
||||
|
||||
public function notify(Notification $notification, RecipientInterface $recipient, ?string $transportName = null) : void {
|
||||
if (!$recipient instanceof EmailRecipientInterface || trim($recipient->getEmail()) === '') {
|
||||
throw new LogicException('The Cacti email channel needs an email recipient.');
|
||||
}
|
||||
|
||||
if ($transportName !== null) {
|
||||
throw new LogicException('The Cacti email channel uses the transport selected in Cacti settings.');
|
||||
}
|
||||
|
||||
$options = $notification instanceof CactiNotification ? $notification->getOptions('email') : [];
|
||||
$name = $recipient instanceof CactiRecipient ? $recipient->getName() : '';
|
||||
$to = [['email' => $recipient->getEmail(), 'name' => $name]];
|
||||
$html = (bool) ($options['html'] ?? false);
|
||||
$text = (string) ($options['text'] ?? ($html ? '' : $notification->getContent()));
|
||||
$attachments = $options['attachments'] ?? [];
|
||||
$headers = $options['headers'] ?? [];
|
||||
|
||||
if (!is_array($attachments)) {
|
||||
throw new LogicException('The Cacti email attachments option must be an array.');
|
||||
}
|
||||
|
||||
if (!is_array($headers)) {
|
||||
throw new LogicException('The Cacti email headers option must be an array.');
|
||||
}
|
||||
|
||||
$error = ($this->mailer)(
|
||||
$options['from'] ?? '',
|
||||
$to,
|
||||
$options['cc'] ?? null,
|
||||
$options['bcc'] ?? null,
|
||||
$options['reply_to'] ?? null,
|
||||
$notification->getSubject(),
|
||||
$notification->getContent(),
|
||||
$text,
|
||||
$attachments,
|
||||
$headers,
|
||||
$html,
|
||||
(bool) ($options['expand_ids'] ?? false)
|
||||
);
|
||||
|
||||
if ($error !== '') {
|
||||
throw new RuntimeException(trim(strip_tags((string) $error)));
|
||||
}
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, RecipientInterface $recipient) : bool {
|
||||
if (!$recipient instanceof EmailRecipientInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trim($recipient->getEmail()) !== '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
|
||||
/**
|
||||
* A notification carrying optional channel-specific Cacti delivery data.
|
||||
*/
|
||||
final class CactiNotification extends Notification {
|
||||
/** @var array<string, mixed> */
|
||||
private array $options;
|
||||
private bool $sealed = false;
|
||||
|
||||
/**
|
||||
* @param list<string> $channels
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(string $subject, string $content, array $channels = ['email'], array $options = []) {
|
||||
parent::__construct($subject, $channels);
|
||||
|
||||
parent::content($content);
|
||||
|
||||
if (isset($options['importance'])) {
|
||||
parent::importance((string) $options['importance']);
|
||||
}
|
||||
|
||||
$this->options = $options;
|
||||
$this->sealed = true;
|
||||
}
|
||||
|
||||
public function subject(string $subject) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::subject($subject);
|
||||
}
|
||||
|
||||
public function content(string $content) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::content($content);
|
||||
}
|
||||
|
||||
public function importance(string $importance) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::importance($importance);
|
||||
}
|
||||
|
||||
public function importanceFromLogLevelName(string $level) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::importanceFromLogLevelName($level);
|
||||
}
|
||||
|
||||
public function emoji(string $emoji) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::emoji($emoji);
|
||||
}
|
||||
|
||||
public function exception(Throwable $exception) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::exception($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $channels
|
||||
*/
|
||||
public function channels(array $channels) : static {
|
||||
$this->assertMutable();
|
||||
|
||||
return parent::channels($channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(string $channel) : array {
|
||||
$options = $this->options[$channel] ?? [];
|
||||
|
||||
return is_array($options) ? $options : [];
|
||||
}
|
||||
|
||||
private function assertMutable() : void {
|
||||
if ($this->sealed) {
|
||||
throw new LogicException('Cacti notifications are immutable after construction.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
|
||||
/**
|
||||
* Symfony recipient with the display name used by Cacti mail delivery.
|
||||
*/
|
||||
final class CactiRecipient extends Recipient {
|
||||
public function __construct(string $email = '', string $phone = '', private string $name = '') {
|
||||
parent::__construct($email, $phone);
|
||||
}
|
||||
|
||||
public function getName() : string {
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Notifier\Channel\ChannelInterface;
|
||||
use Symfony\Component\Notifier\Notifier;
|
||||
use Symfony\Component\Notifier\NotifierInterface;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
function api_notification_dependencies_available() : bool {
|
||||
return interface_exists(ChannelInterface::class)
|
||||
&& class_exists(Notifier::class)
|
||||
&& interface_exists(NotifierInterface::class)
|
||||
&& class_exists(Notification::class)
|
||||
&& interface_exists(EmailRecipientInterface::class)
|
||||
&& interface_exists(RecipientInterface::class);
|
||||
}
|
||||
|
||||
if (api_notification_dependencies_available()) {
|
||||
require_once __DIR__ . '/CactiNotification.php';
|
||||
require_once __DIR__ . '/CactiRecipient.php';
|
||||
require_once __DIR__ . '/CactiEmailChannel.php';
|
||||
}
|
||||
|
||||
function api_notification_assert_dependencies() : void {
|
||||
if (!api_notification_dependencies_available()) {
|
||||
throw new RuntimeException('Symfony Notifier dependencies are unavailable. Run Composer install to restore the vendor tree.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Cacti's notification channels, extended by installed plugins.
|
||||
*
|
||||
* Plugins may register the `notification_channels` hook and return the channel
|
||||
* map with additional Symfony ChannelInterface instances.
|
||||
*
|
||||
* @return array<string, ChannelInterface>
|
||||
*/
|
||||
function api_notification_channels() : array {
|
||||
api_notification_assert_dependencies();
|
||||
|
||||
$channels = ['email' => new CactiEmailChannel()];
|
||||
|
||||
if (function_exists('api_plugin_hook_function')) {
|
||||
$channels = api_plugin_hook_function('notification_channels', $channels);
|
||||
}
|
||||
|
||||
return api_notification_validate_channels($channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ChannelInterface>
|
||||
*/
|
||||
function api_notification_validate_channels(mixed $channels) : array {
|
||||
api_notification_assert_dependencies();
|
||||
|
||||
if (!is_array($channels)) {
|
||||
throw new InvalidArgumentException('The notification_channels hook must return an array.');
|
||||
}
|
||||
|
||||
foreach ($channels as $name => $channel) {
|
||||
if (!is_string($name) || $name === '' || !$channel instanceof ChannelInterface) {
|
||||
throw new InvalidArgumentException('Notification channels must be named Symfony ChannelInterface instances.');
|
||||
}
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes Cacti email values and Symfony recipients for notification use.
|
||||
*
|
||||
* @return list<RecipientInterface>
|
||||
*/
|
||||
function api_notification_recipients(array|string|RecipientInterface $recipients) : array {
|
||||
api_notification_assert_dependencies();
|
||||
|
||||
if ($recipients instanceof RecipientInterface) {
|
||||
return [$recipients];
|
||||
}
|
||||
|
||||
if (is_array($recipients) && array_filter($recipients, fn (mixed $recipient) : bool => $recipient instanceof RecipientInterface) !== []) {
|
||||
foreach ($recipients as $recipient) {
|
||||
if (!$recipient instanceof RecipientInterface) {
|
||||
throw new InvalidArgumentException('Symfony recipients cannot be mixed with email address values.');
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($recipients);
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
$emails = parse_email_details($recipients);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
if (($email['email'] ?? '') !== '') {
|
||||
$normalized[] = new CactiRecipient(trim($email['email']), '', trim($email['name'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification through Symfony Notifier and Cacti delivery channels.
|
||||
*
|
||||
* The options array accepts `importance` plus channel-specific arrays. Email
|
||||
* options are: from, cc, bcc, reply_to, html, text, attachments, headers and
|
||||
* expand_ids.
|
||||
*
|
||||
* @param list<string> $channels
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
function api_notification_send(string $subject, string $content, array|string|RecipientInterface $recipients,
|
||||
array $channels = ['email'], array $options = [], ?NotifierInterface $notifier = null) : bool {
|
||||
try {
|
||||
api_notification_assert_dependencies();
|
||||
|
||||
$normalized = api_notification_recipients($recipients);
|
||||
|
||||
if ($normalized === []) {
|
||||
throw new InvalidArgumentException('At least one notification recipient is required.');
|
||||
}
|
||||
|
||||
$notification = new CactiNotification($subject, $content, $channels, $options);
|
||||
|
||||
$notifier ??= new Notifier(api_notification_channels());
|
||||
$notifier->send($notification, ...$normalized);
|
||||
|
||||
return true;
|
||||
} catch (Throwable $e) {
|
||||
$logSubject = str_replace(["\r", "\n"], ' ', $subject);
|
||||
$logError = str_replace(["\r", "\n"], ' ', $e->getMessage());
|
||||
cacti_log(sprintf("ERROR: Notification '%s' failed: %s", $logSubject, $logError), false, 'NOTIFIER');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -5562,8 +5562,15 @@ function admin_email(string $subject, string $message) : bool {
|
||||
$to = $admin_details['email_address'];
|
||||
}
|
||||
|
||||
// If we get any message back then we have failed
|
||||
$result = empty(send_mail($to, $from, $subject, $fin_message, html: true, expandIds: true));
|
||||
require_once(CACTI_PATH_LIBRARY . '/api_notification.php');
|
||||
|
||||
$result = api_notification_send($subject, $fin_message, $to, options: [
|
||||
'email' => [
|
||||
'from' => $from,
|
||||
'html' => true,
|
||||
'expand_ids' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<phpunit bootstrap="tests/bootstrap-unit.php" beStrictAboutOutputDuringTests="false">
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<env name="CACTI_TEST_BOOTSTRAP" value="1"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<file>lib/CactiEmailChannel.php</file>
|
||||
<file>lib/CactiNotification.php</file>
|
||||
<file>lib/CactiRecipient.php</file>
|
||||
<file>lib/api_notification.php</file>
|
||||
</include>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuite name="Notification API coverage">
|
||||
<file>tests/Unit/Core/CactiNotificationApiTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use Symfony\Component\Notifier\Exception\LogicException;
|
||||
use Symfony\Component\Notifier\Exception\RuntimeException;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\NotifierInterface;
|
||||
use Symfony\Component\Notifier\Recipient\NoRecipient;
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/lib/api_notification.php';
|
||||
|
||||
test('the notification API gates local classes behind Symfony dependencies', function () {
|
||||
$source = file_get_contents(CACTI_PATH_LIBRARY . '/api_notification.php');
|
||||
|
||||
expect($source)->toContain('if (api_notification_dependencies_available()) {')
|
||||
->and(strpos($source, 'if (api_notification_dependencies_available()) {'))
|
||||
->toBeLessThan(strpos($source, "require_once __DIR__ . '/CactiNotification.php';"))
|
||||
->and($source)->toContain('api_notification_assert_dependencies();');
|
||||
});
|
||||
|
||||
final class RecordingNotifier implements NotifierInterface {
|
||||
public ?Notification $notification = null;
|
||||
|
||||
/** @var list<RecipientInterface> */
|
||||
public array $recipients = [];
|
||||
|
||||
public function send(Notification $notification, RecipientInterface ...$recipients) : void {
|
||||
$this->notification = $notification;
|
||||
$this->recipients = $recipients;
|
||||
}
|
||||
}
|
||||
|
||||
test('Cacti notifications retain content and channel options', function () {
|
||||
$notification = new CactiNotification('Disk warning', 'Disk is full', ['email', 'chat'], [
|
||||
'email' => ['html' => true],
|
||||
'chat' => 'invalid options',
|
||||
]);
|
||||
|
||||
expect($notification->getSubject())->toBe('Disk warning')
|
||||
->and($notification->getContent())->toBe('Disk is full')
|
||||
->and($notification->getChannels(new Recipient('ops@example.com')))->toBe(['email', 'chat'])
|
||||
->and($notification->getOptions('email'))->toBe(['html' => true])
|
||||
->and($notification->getOptions('chat'))->toBe([])
|
||||
->and($notification->getOptions('missing'))->toBe([]);
|
||||
});
|
||||
|
||||
test('Cacti notifications cannot be mutated after construction', function () {
|
||||
$notification = new CactiNotification('Original', 'Body', ['email'], [
|
||||
'importance' => Notification::IMPORTANCE_URGENT,
|
||||
]);
|
||||
|
||||
expect($notification->getImportance())->toBe(Notification::IMPORTANCE_URGENT)
|
||||
->and(fn () => $notification->subject('Changed'))->toThrow(\LogicException::class, 'immutable')
|
||||
->and(fn () => $notification->content('Changed'))->toThrow(\LogicException::class, 'immutable')
|
||||
->and(fn () => $notification->importance(Notification::IMPORTANCE_LOW))->toThrow(\LogicException::class, 'immutable')
|
||||
->and(fn () => $notification->channels(['chat']))->toThrow(\LogicException::class, 'immutable');
|
||||
});
|
||||
|
||||
test('Cacti recipients retain their display name', function () {
|
||||
$recipient = new CactiRecipient('ops@example.com', '+15551234567', 'Operations');
|
||||
|
||||
expect($recipient->getEmail())->toBe('ops@example.com')
|
||||
->and($recipient->getPhone())->toBe('+15551234567')
|
||||
->and($recipient->getName())->toBe('Operations');
|
||||
});
|
||||
|
||||
test('the email channel delegates all Cacti mail options', function () {
|
||||
$args = [];
|
||||
$channel = new CactiEmailChannel(function (...$received) use (&$args) : string {
|
||||
$args = $received;
|
||||
|
||||
return '';
|
||||
});
|
||||
$recipient = new CactiRecipient('ops@example.com', '', 'Operations');
|
||||
$notification = new CactiNotification('Alert', '<b>Down</b>', ['email'], [
|
||||
'email' => [
|
||||
'from' => 'cacti@example.com',
|
||||
'cc' => 'cc@example.com',
|
||||
'bcc' => 'bcc@example.com',
|
||||
'reply_to' => 'reply@example.com',
|
||||
'html' => true,
|
||||
'text' => 'Down',
|
||||
'attachments' => ['report.txt'],
|
||||
'headers' => ['X-Cacti' => 'yes'],
|
||||
'expand_ids' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$channel->notify($notification, $recipient);
|
||||
|
||||
expect($channel->supports($notification, $recipient))->toBeTrue()
|
||||
->and($channel->supports($notification, new NoRecipient()))->toBeFalse()
|
||||
->and($channel->supports($notification, new Recipient('', '+15551234567')))->toBeFalse()
|
||||
->and($args)->toBe([
|
||||
'cacti@example.com',
|
||||
[['email' => 'ops@example.com', 'name' => 'Operations']],
|
||||
'cc@example.com',
|
||||
'bcc@example.com',
|
||||
'reply@example.com',
|
||||
'Alert',
|
||||
'<b>Down</b>',
|
||||
'Down',
|
||||
['report.txt'],
|
||||
['X-Cacti' => 'yes'],
|
||||
true,
|
||||
true,
|
||||
]);
|
||||
});
|
||||
|
||||
test('the email channel supplies safe defaults for standard notifications', function () {
|
||||
$args = [];
|
||||
$channel = new CactiEmailChannel(function (...$received) use (&$args) : string {
|
||||
$args = $received;
|
||||
|
||||
return '';
|
||||
});
|
||||
$notification = (new Notification('Alert', ['email']))->content('Plain text');
|
||||
|
||||
$channel->notify($notification, new Recipient('ops@example.com'));
|
||||
|
||||
expect($args[0])->toBe('')
|
||||
->and($args[6])->toBe('Plain text')
|
||||
->and($args[7])->toBe('Plain text')
|
||||
->and($args[10])->toBeFalse()
|
||||
->and($args[11])->toBeFalse();
|
||||
});
|
||||
|
||||
test('the email channel rejects unsupported recipients and transport overrides', function () {
|
||||
$channel = new CactiEmailChannel(fn () : string => '');
|
||||
$notification = new Notification('Alert', ['email']);
|
||||
|
||||
expect(fn () => $channel->notify($notification, new NoRecipient()))
|
||||
->toThrow(LogicException::class, 'needs an email recipient')
|
||||
->and(fn () => $channel->notify($notification, new Recipient('ops@example.com'), 'backup'))
|
||||
->toThrow(LogicException::class, 'transport selected in Cacti settings');
|
||||
});
|
||||
|
||||
test('the email channel rejects malformed mailer collection options', function () {
|
||||
$channel = new CactiEmailChannel(fn () : string => '');
|
||||
$recipient = new Recipient('ops@example.com');
|
||||
|
||||
$badAttachments = new CactiNotification('Alert', 'Body', ['email'], [
|
||||
'email' => ['attachments' => 'report.txt'],
|
||||
]);
|
||||
$badHeaders = new CactiNotification('Alert', 'Body', ['email'], [
|
||||
'email' => ['headers' => 'X-Cacti: yes'],
|
||||
]);
|
||||
|
||||
expect(fn () => $channel->notify($badAttachments, $recipient))
|
||||
->toThrow(LogicException::class, 'attachments option must be an array')
|
||||
->and(fn () => $channel->notify($badHeaders, $recipient))
|
||||
->toThrow(LogicException::class, 'headers option must be an array');
|
||||
});
|
||||
|
||||
test('the email channel surfaces sanitized mailer failures', function () {
|
||||
$channel = new CactiEmailChannel(fn () : string => '<b>SMTP unavailable</b>');
|
||||
|
||||
expect(fn () => $channel->notify(new Notification('Alert'), new Recipient('ops@example.com')))
|
||||
->toThrow(RuntimeException::class, 'SMTP unavailable');
|
||||
});
|
||||
|
||||
test('notification recipients accept Cacti email forms and Symfony recipients', function () {
|
||||
$direct = new Recipient('direct@example.com');
|
||||
$list = [new Recipient('one@example.com'), new Recipient('two@example.com')];
|
||||
|
||||
expect(api_notification_recipients($direct))->toBe([$direct])
|
||||
->and(api_notification_recipients($list))->toBe($list);
|
||||
|
||||
$emails = api_notification_recipients('Operations <ops@example.com>,admin@example.com');
|
||||
|
||||
expect($emails)->toHaveCount(2)
|
||||
->and($emails[0])->toBeInstanceOf(CactiRecipient::class)
|
||||
->and($emails[0]->getEmail())->toBe('ops@example.com')
|
||||
->and($emails[0]->getName())->toBe('Operations')
|
||||
->and($emails[1]->getEmail())->toBe('admin@example.com');
|
||||
|
||||
expect(fn () => api_notification_recipients([$direct, 'mixed@example.com']))
|
||||
->toThrow(InvalidArgumentException::class, 'cannot be mixed');
|
||||
});
|
||||
|
||||
test('the public API dispatches through an injected Symfony notifier', function () {
|
||||
$notifier = new RecordingNotifier();
|
||||
|
||||
expect(api_notification_send(
|
||||
'Disk warning',
|
||||
'Disk is full',
|
||||
'Operations <ops@example.com>',
|
||||
['email'],
|
||||
['importance' => Notification::IMPORTANCE_URGENT],
|
||||
$notifier
|
||||
))->toBeTrue()
|
||||
->and($notifier->notification)->toBeInstanceOf(CactiNotification::class)
|
||||
->and($notifier->notification?->getImportance())->toBe(Notification::IMPORTANCE_URGENT)
|
||||
->and($notifier->recipients)->toHaveCount(1)
|
||||
->and($notifier->recipients[0])->toBeInstanceOf(CactiRecipient::class);
|
||||
});
|
||||
|
||||
test('the default channel map exposes Cacti email delivery', function () {
|
||||
$channels = api_notification_channels();
|
||||
|
||||
expect($channels)->toHaveKey('email')
|
||||
->and($channels['email'])->toBeInstanceOf(CactiEmailChannel::class);
|
||||
});
|
||||
|
||||
test('plugin channel maps must contain named Symfony channels', function () {
|
||||
$channel = new CactiEmailChannel(fn () : string => '');
|
||||
|
||||
expect(api_notification_validate_channels(['email' => $channel]))->toBe(['email' => $channel])
|
||||
->and(fn () => api_notification_validate_channels(null))
|
||||
->toThrow(InvalidArgumentException::class, 'must return an array')
|
||||
->and(fn () => api_notification_validate_channels(['invalid']))
|
||||
->toThrow(InvalidArgumentException::class, 'must be named');
|
||||
});
|
||||
|
||||
test('the public API fails closed for an unknown default channel', function () {
|
||||
expect(api_notification_send('Alert', 'Unknown channel', 'ops@example.com', ['missing']))->toBeFalse();
|
||||
});
|
||||
|
||||
test('the public API fails closed when no recipient is usable', function () {
|
||||
expect(api_notification_send('Alert', 'No recipient', [], notifier: new RecordingNotifier()))->toBeFalse();
|
||||
});
|
||||
Reference in new issue
Block a user