Files
cacti/docs/notification-api.md
Thomas Vincent 66a2808cc9 Let one notification address several recipients (#7782)
* feat(notify): let one notification address several recipients

Symfony calls a channel once per recipient, so a caller that needs a single
message sent to several people could not express it. The email 'to' option
folds the extra addresses into the same mailer() call and drops any that
match the recipient.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(notify): compare the local part of an address exactly

RFC 5321 leaves local part case to the receiving host, so folding the whole
address deduplicated OPS@example.com and ops@example.com into one mailbox.
Only the domain folds now.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-09-02 21:01:03 -07:00

3.2 KiB

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:

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:

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, to, cc, bcc, reply_to, html, text, attachments, headers, and expand_ids.

Symfony calls a channel once per recipient, so passing several recipients sends several messages. When one message has to reach several people, pass one recipient and put the rest in the to option:

api_notification_send(
	'Devices rebooted',
	$body,
	'first@example.com',
	options: ['email' => ['to' => 'second@example.com,third@example.com']]
);

Addresses matching the recipient are dropped, so the recipient is never listed twice. Matching compares the domain without regard to case and the local part exactly, because RFC 5321 leaves local part case to the receiving host.

Cores released before this option ignore it and deliver to the recipient only. Probe for it before joining addresses:

if (function_exists('api_notification_email_supports_multiple_to')
	&& api_notification_email_supports_multiple_to()) {
	// safe to pass the 'to' option
}

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:

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:

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.