Email Transport: embed graphs by default (#14270)

* Email embed graphs

* Allow attachment for non-html
Add setting to webui
Correct $auth setting

* Cleanups, throw RrdGraphException instead of returning an error image.
Generate the error image later, giving more control.
Reduce code duplication a little

* Style and lint fixes
Change to flags

* Add baseline for lint errors I don't know how to resolve

* oopsie, changed the code after generating the baseline

* Tiny cleanups.  Make set DeviceCache primary, it is free.

* Docs.

* email_html note

* Allow control of graph embed at the email transport level to override the global config.

* Allow control of graph embed at the email transport level to override the global config.

* Add INLINE_BASE64 to make it easier to create inline image tags
This commit is contained in:
Tony Murray 2022-09-05 20:41:55 -05:00 committed by GitHub
parent ec8629fb63
commit 302a989d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 351 additions and 121 deletions

View File

@ -46,7 +46,7 @@ class Mail extends Transport
$msg = preg_replace("/(?<!\r)\n/", "\r\n", $obj['msg']);
}
return \LibreNMS\Util\Mail::send($email, $obj['title'], $msg, $html);
return \LibreNMS\Util\Mail::send($email, $obj['title'], $msg, $html, $this->config['attach-graph'] ?? null);
}
public static function configTemplate()
@ -59,6 +59,13 @@ class Mail extends Transport
'descr' => 'Email address of contact',
'type' => 'text',
],
[
'title' => 'Include Graphs',
'name' => 'attach-graph',
'descr' => 'Include graph image data in the email. Will be embedded if html5, otherwise attached. Template must use @signedGraphTag',
'type' => 'checkbox',
'default' => true,
],
],
'validation' => [
'email' => 'required|email',

View File

@ -559,7 +559,6 @@ class Rrd extends BaseDatastore
* @param string $options
* @return string
*
* @throws \LibreNMS\Exceptions\FileExistsException
* @throws \LibreNMS\Exceptions\RrdGraphException
*/
public function graph(string $options): string
@ -568,9 +567,13 @@ class Rrd extends BaseDatastore
$process->setTimeout(300);
$process->setIdleTimeout(300);
$command = $this->buildCommand('graph', '-', $options);
$process->setInput($command . "\nquit");
$process->run();
try {
$command = $this->buildCommand('graph', '-', $options);
$process->setInput($command . "\nquit");
$process->run();
} catch (FileExistsException $e) {
throw new RrdGraphException($e->getMessage(), 'File Exists');
}
$feedback_position = strrpos($process->getOutput(), 'OK ');
if ($feedback_position !== false) {
@ -584,6 +587,9 @@ class Rrd extends BaseDatastore
$position += strlen($search);
throw new RrdGraphException(
substr($process->getOutput(), $position),
null,
null,
null,
$process->getExitCode(),
substr($process->getOutput(), 0, $position)
);
@ -591,7 +597,7 @@ class Rrd extends BaseDatastore
// only error text was returned
$error = trim($process->getOutput() . PHP_EOL . $process->getErrorOutput());
throw new RrdGraphException($error, $process->getExitCode(), '');
throw new RrdGraphException($error, null, null, null, $process->getExitCode());
}
private function getImageEnd(string $type): string

View File

@ -26,19 +26,48 @@
namespace LibreNMS\Exceptions;
use Exception;
use LibreNMS\Util\Graph;
class RrdGraphException extends Exception
{
/** @var string */
protected $image_output;
/** @var string|null */
private $short_text;
/** @var int|string|null */
private $width;
/** @var int|string|null */
private $height;
public function __construct($error, $exit_code, $image_output)
/**
* @param string $error
* @param string|null $short_text
* @param int|string|null $width
* @param int|string|null $height
* @param int $exit_code
* @param string $image_output
*/
public function __construct($error, $short_text = null, $width = null, $height = null, $exit_code = 0, $image_output = '')
{
parent::__construct($error, $exit_code);
$this->short_text = $short_text;
$this->image_output = $image_output;
$this->width = $width;
$this->height = $height;
}
public function getImage()
public function getImage(): string
{
return $this->image_output;
}
public function generateErrorImage(): string
{
return Graph::error(
$this->getMessage(),
$this->short_text,
empty($this->width) ? 300 : (int) $this->width,
empty($this->height) ? null : (int) $this->height,
);
}
}

View File

@ -25,12 +25,147 @@
namespace LibreNMS\Util;
use App\Facades\DeviceCache;
use App\Models\Device;
use Illuminate\Support\Facades\Auth;
use LibreNMS\Config;
use LibreNMS\Exceptions\RrdGraphException;
use Rrd;
class Graph
{
public static function getTypes()
const BASE64_OUTPUT = 1; // BASE64 encoded image data
const INLINE_BASE64 = 2; // img src inline base64 image
const COMMAND_ONLY = 4; // just print the command
/**
* Fetch a graph image (as string) based on the given $vars
* Optionally, override the output format to base64
*
* @param array|string $vars
* @param int $flags Flags for controlling graph generating options.
* @return string
*
* @throws \LibreNMS\Exceptions\RrdGraphException
*/
public static function get($vars, int $flags = 0): string
{
define('IGNORE_ERRORS', true);
chdir(base_path());
include_once base_path('includes/dbFacile.php');
include_once base_path('includes/common.php');
include_once base_path('includes/html/functions.inc.php');
include_once base_path('includes/rewrites.php');
// handle possible graph url input
if (is_string($vars)) {
$vars = Url::parseLegacyPathVars($vars);
}
[$type, $subtype] = extract_graph_type($vars['type']);
$graph_title = '';
if (isset($vars['device'])) {
$device = device_by_id_cache(is_numeric($vars['device']) ? $vars['device'] : getidbyname($vars['device']));
DeviceCache::setPrimary($device['device_id']);
//set default graph title
$graph_title = DeviceCache::getPrimary()->displayName();
}
// variables for included graphs
$width = $vars['width'] ?? 400;
$height = $vars['height'] ?? $width / 3;
$title = $vars['title'] ?? '';
$vertical = $vars['vertical'] ?? '';
$legend = $vars['legend'] ?? false;
$output = $vars['output'] ?? 'default';
$from = parse_at_time($vars['from'] ?? '-1d');
$to = empty($vars['to']) ? time() : parse_at_time($vars['to']);
$period = ($to - $from);
$prev_from = ($from - $period);
$graph_image_type = $vars['graph_type'] ?? Config::get('webui.graph_type');
Config::set('webui.graph_type', $graph_image_type); // set in case accessed elsewhere
$rrd_options = '';
$rrd_filename = null;
$auth = Auth::guest(); // if user not logged in, assume we authenticated via signed url, allow_unauth_graphs or allow_unauth_graphs_cidr
require base_path("/includes/html/graphs/$type/auth.inc.php");
if (! $auth) {
// We are unauthenticated :(
throw new RrdGraphException('No Authorization', 'No Auth', $width, $height);
}
if (is_customoid_graph($type, $subtype)) {
$unit = $vars['unit'];
require base_path('/includes/html/graphs/customoid/customoid.inc.php');
} elseif (is_file(base_path("/includes/html/graphs/$type/$subtype.inc.php"))) {
require base_path("/includes/html/graphs/$type/$subtype.inc.php");
} else {
throw new RrdGraphException("{$type}_$subtype template missing", "{$type}_$subtype missing", $width, $height);
}
if ($graph_image_type === 'svg') {
$rrd_options .= ' --imgformat=SVG';
if ($width < 350) {
$rrd_options .= ' -m 0.75 -R light';
}
}
// command output requested
if ($flags & self::COMMAND_ONLY) {
$cmd_output = "<div class='infobox'>";
$cmd_output .= "<p style='font-size: 16px; font-weight: bold;'>RRDTool Command</p>";
$cmd_output .= "<pre class='rrd-pre'>";
$cmd_output .= escapeshellcmd('rrdtool ' . Rrd::buildCommand('graph', Config::get('temp_dir') . '/' . strgen(), $rrd_options));
$cmd_output .= '</pre>';
try {
$cmd_output .= Rrd::graph($rrd_options);
} catch (RrdGraphException $e) {
$cmd_output .= "<p style='font-size: 16px; font-weight: bold;'>RRDTool Output</p>";
$cmd_output .= "<pre class='rrd-pre'>";
$cmd_output .= $e->getMessage();
$cmd_output .= '</pre>';
}
$cmd_output .= '</div>';
return $cmd_output;
}
if (empty($rrd_options)) {
throw new RrdGraphException('Graph Definition Error', 'Def Error', $width, $height);
}
// Generating the graph!
try {
$image_data = Rrd::graph($rrd_options);
// output the graph int the desired format
if (Debug::isEnabled()) {
return '<img src="data:' . self::imageType($graph_image_type) . ';base64,' . base64_encode($image_data) . '" alt="graph" />';
} elseif ($flags & self::BASE64_OUTPUT || $output == 'base64') {
return base64_encode($image_data);
} elseif ($flags & self::INLINE_BASE64 || $output == 'inline-base64') {
return 'data:' . self::imageType($graph_image_type) . ';base64,' . base64_encode($image_data);
}
return $image_data; // raw data
} catch (RrdGraphException $e) {
// preserve original error if debug is enabled, otherwise make it a little more user friendly
if (Debug::isEnabled()) {
throw $e;
}
if (isset($rrd_filename) && ! Rrd::checkRrdExists($rrd_filename)) {
throw new RrdGraphException('No Data file' . basename($rrd_filename), 'No Data', $width, $height, $e->getCode(), $e->getImage());
}
throw new RrdGraphException('Error: ' . $e->getMessage(), 'Draw Error', $width, $height, $e->getCode(), $e->getImage());
}
}
public static function getTypes(): array
{
return ['device', 'port', 'application', 'munin', 'service'];
}
@ -42,7 +177,7 @@ class Graph
* @param Device $device
* @return array
*/
public static function getSubtypes($type, $device = null)
public static function getSubtypes($type, $device = null): array
{
$types = [];
@ -79,7 +214,7 @@ class Graph
* @param string $subtype
* @return bool
*/
public static function isMibGraph($type, $subtype)
public static function isMibGraph($type, $subtype): bool
{
return Config::get("graph_types.$type.$subtype.section") == 'mib';
}
@ -98,4 +233,70 @@ class Graph
return Config::get("os_group.$os_group.over", Config::get('os.default.over'));
}
/**
* Get the http content type of the image
*
* @param string $type svg or png
* @return string
*/
public static function imageType(string $type): string
{
return $type === 'svg' ? 'image/svg+xml' : 'image/png';
}
/**
* Create image to output text instead of a graph.
*
* @param string $text Error message to display
* @param string|null $short_text Error message for smaller graph images
* @param int $width Width of graph image (defaults to 300)
* @param int|null $height Height of graph image (defaults to width / 3)
* @param int[] $color Color of text, defaults to dark red
* @return string the generated image
*/
public static function error(string $text, ?string $short_text, int $width = 300, ?int $height = null, array $color = [128, 0, 0]): string
{
$type = Config::get('webui.graph_type');
$height = $height ?? $width / 3;
if ($short_text !== null && $width < 200) {
$text = $short_text;
}
if ($type === 'svg') {
$rgb = implode(', ', $color);
return <<<SVG
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
viewBox="0 0 $width $height"
preserveAspectRatio="xMinYMin">
<foreignObject x="0" y="0" width="$width" height="$height" transform="translate(0,0)">
<xhtml:div style="display:table; width:{$width}px; height:{$height}px; overflow:hidden;">
<xhtml:div style="display:table-cell; vertical-align:middle;">
<xhtml:div style="color:rgb($rgb); text-align:center; font-family:sans-serif; font-size:0.6em;">$text</xhtml:div>
</xhtml:div>
</xhtml:div>
</foreignObject>
</svg>
SVG;
}
$img = imagecreate($width, $height);
imagecolorallocatealpha($img, 255, 255, 255, 127); // transparent background
$px = (int) ((imagesx($img) - 7.5 * strlen($text)) / 2);
$font = $width < 200 ? 3 : 5;
imagestring($img, $font, $px, ($height / 2 - 8), $text, imagecolorallocate($img, ...$color));
// Output the image
ob_start();
imagepng($img);
$output = ob_get_clean();
ob_end_clean();
imagedestroy($img);
return $output;
}
}

View File

@ -27,6 +27,7 @@ namespace LibreNMS\Util;
use Exception;
use LibreNMS\Config;
use LibreNMS\Exceptions\RrdGraphException;
use PHPMailer\PHPMailer\PHPMailer;
class Mail
@ -70,7 +71,7 @@ class Mail
* @param bool $html
* @return bool|string
*/
public static function send($emails, $subject, $message, bool $html = false)
public static function send($emails, $subject, $message, bool $html = false, ?bool $embedGraphs = null)
{
if (is_array($emails) || ($emails = self::parseEmails($emails))) {
d_echo("Attempting to email $subject to: " . implode('; ', array_keys($emails)) . PHP_EOL);
@ -89,8 +90,11 @@ class Mail
$mail->CharSet = 'utf-8';
$mail->WordWrap = 76;
$mail->Body = $message;
if ($embedGraphs ?? Config::get('email_attach_graphs')) {
self::embedGraphs($mail);
}
if ($html) {
$mail->isHTML(true);
$mail->isHTML();
}
switch (strtolower(trim(Config::get('email_backend')))) {
case 'sendmail':
@ -124,4 +128,41 @@ class Mail
return 'No contacts found';
}
/**
* Search for generated graph links, generate them, attach them to the email and update the url to a cid link
*/
private static function embedGraphs(PHPMailer $mail): void
{
$body = $mail->Body;
// search for generated graphs
preg_match_all('/ class=\"librenms-graph\" src=\"(.*?)\"/', $body, $match);
foreach (array_values(array_unique($match[1])) as $attachment_id => $url) {
try {
$cid = "graph$attachment_id";
// fetch image, do not debug as it will return the wrong format.
$prev = Debug::isEnabled();
Debug::set(false);
$image = Graph::get(Url::parseLegacyPathVars($url));
Debug::set($prev);
// attach image
if (Config::get('webui.graph_type') == 'svg') {
$mail->addStringEmbeddedImage($image, $cid, "$cid.svg", PHPMailer::ENCODING_BASE64, 'image/svg+xml');
} else {
$mail->addStringEmbeddedImage($image, $cid, "$cid.png", PHPMailer::ENCODING_BASE64, 'image/png');
}
// update image tag to link to attached image
$body = str_replace($url, "cid:$cid", $body);
} catch (RrdGraphException|\PHPMailer\PHPMailer\Exception $e) {
report($e);
}
}
$mail->Body = $body;
}
}

View File

@ -5,39 +5,38 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use LibreNMS\Config;
use LibreNMS\Exceptions\RrdGraphException;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Graph;
use LibreNMS\Util\Url;
class GraphController extends Controller
{
/**
* @throws \LibreNMS\Exceptions\RrdGraphException
*/
public function __invoke(Request $request, string $path = ''): Response
{
define('IGNORE_ERRORS', true);
include_once base_path('includes/dbFacile.php');
include_once base_path('includes/common.php');
include_once base_path('includes/html/functions.inc.php');
include_once base_path('includes/rewrites.php');
$auth = \Auth::guest(); // if user not logged in, assume we authenticated via signed url, allow_unauth_graphs or allow_unauth_graphs_cidr
$vars = array_merge(Url::parseLegacyPathVars($request->path()), $request->except(['username', 'password']));
$vars['graph_type'] = $vars['graph_type'] ?? Config::get('webui.graph_type');
if (\Auth::check()) {
// only allow debug for logged in users
Debug::set(! empty($vars['debug']));
}
// TODO, import graph.inc.php code and call Rrd::graph() directly
chdir(base_path());
ob_start();
include base_path('includes/html/graphs/graph.inc.php');
$output = ob_get_clean();
ob_end_clean();
$headers = [
'Content-type' => Graph::imageType($vars['graph_type']),
];
$headers = [];
if (! Debug::isEnabled()) {
$headers['Content-type'] = (Config::get('webui.graph_type') == 'svg' ? 'image/svg+xml' : 'image/png');
try {
return response(Graph::get($vars), 200, Debug::isEnabled() ? [] : $headers);
} catch (RrdGraphException $e) {
if (Debug::isEnabled()) {
throw $e;
}
return response($e->generateErrorImage(), 500, $headers);
}
return response($output, 200, $headers);
}
}

View File

@ -80,8 +80,8 @@ class AppServiceProvider extends ServiceProvider
return "<?php echo '<img class=\"librenms-graph\" src=\"' . \LibreNMS\Util\Url::forExternalGraph($vars) . '\" />'; ?>";
});
Blade::directive('graphImage', function ($vars, $base64 = false) {
return "<?php echo \LibreNMS\Util\Graph::get(is_array($vars) ? $vars : \LibreNMS\Util\Url::parseLegacyPathVars($vars), $base64); ?>";
Blade::directive('graphImage', function ($vars, $flags = 0) {
return "<?php echo \LibreNMS\Util\Graph::get($vars, $flags); ?>";
});
}

View File

@ -381,8 +381,18 @@ LibreNMS database.
## Mail
The E-Mail transports uses the same email-configuration like the rest of LibreNMS.
As a small reminder, here is it's configuration directives including defaults:
The E-Mail transports uses the same email-configuration as the rest of LibreNMS.
As a small reminder, here is its configuration directives including defaults:
Emails will attach all graphs included with the @signedGraphTag directive.
If the email format is set to html, they will be embedded.
To disable attaching images, set email_attach_graphs to false.
!!! setting "alerting/email"
```bash
lnms config:set email_html true
lnms config:set email_attach_graphs false
```
**Example:**

View File

@ -5,12 +5,12 @@
"/css/app.css": "/css/app.css?id=bd093a6a2e2682bb59ef",
"/js/vendor.js": "/js/vendor.js?id=c5fd3d75a63757080dbb",
"/js/lang/de.js": "/js/lang/de.js?id=613b5ca9cd06ca15e384",
"/js/lang/en.js": "/js/lang/en.js?id=6b3807ebe10e3fa9fa40",
"/js/lang/en.js": "/js/lang/en.js?id=b4dc5539b25bf7a31718",
"/js/lang/fr.js": "/js/lang/fr.js?id=982d149de32e1867610c",
"/js/lang/it.js": "/js/lang/it.js?id=e24bb9bad83e288b4617",
"/js/lang/ru.js": "/js/lang/ru.js?id=f6b7c078755312a0907c",
"/js/lang/sr.js": "/js/lang/sr.js?id=388e38b41f63e3517506",
"/js/lang/uk.js": "/js/lang/uk.js?id=34f8698ff09b869db2f5",
"/js/lang/zh-CN.js": "/js/lang/zh-CN.js?id=4e081fbac70d969894bf",
"/js/lang/zh-TW.js": "/js/lang/zh-TW.js?id=ed26425647721a42ee9d",
"/js/lang/sr.js": "/js/lang/sr.js?id=17585a0e001293ade0e1"
"/js/lang/zh-TW.js": "/js/lang/zh-TW.js?id=ed26425647721a42ee9d"
}

View File

@ -11,7 +11,6 @@
*/
use LibreNMS\Config;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Number;
use LibreNMS\Util\Rewrite;
@ -439,44 +438,7 @@ function generate_port_image($args)
*/
function graph_error($text, $color = [128, 0, 0])
{
global $vars;
$type = Config::get('webui.graph_type');
if (! Debug::isEnabled()) {
header('Content-type: ' . get_image_type($type));
}
$width = (int) ($vars['width'] ?? 150);
$height = (int) ($vars['height'] ?? 60);
if ($type === 'svg') {
$rgb = implode(', ', $color);
echo <<<SVG
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
viewBox="0 0 $width $height"
preserveAspectRatio="xMinYMin">
<foreignObject x="0" y="0" width="$width" height="$height" transform="translate(0,0)">
<xhtml:div style="display:table; width:{$width}px; height:{$height}px; overflow:hidden;">
<xhtml:div style="display:table-cell; vertical-align:middle;">
<xhtml:div style="color:rgb($rgb); text-align:center; font-family:sans-serif; font-size:0.6em;">$text</xhtml:div>
</xhtml:div>
</xhtml:div>
</foreignObject>
</svg>
SVG;
} else {
$img = imagecreate($width, $height);
imagecolorallocatealpha($img, 255, 255, 255, 127); // transparent background
$px = ((imagesx($img) - 7.5 * strlen($text)) / 2);
$font = $width < 200 ? 3 : 5;
imagestring($img, $font, $px, ($height / 2 - 8), $text, imagecolorallocate($img, ...$color));
// Output the image
imagepng($img);
imagedestroy($img);
}
echo \LibreNMS\Util\Graph::error($text, null, 300, null, $color);
}
/**
@ -1035,7 +997,7 @@ function eventlog_severity($eventlog_severity)
*/
function get_image_type(string $type)
{
return $type === 'svg' ? 'image/svg+xml' : 'image/png';
return \LibreNMS\Util\Graph::imageType($type);
}
function get_oxidized_nodes_list()

View File

@ -16,6 +16,4 @@ if (Rrd::checkRrdExists($rrd_filename)) {
$rrd_options .= " GPRINT:a:LAST:'%6.2lf %s'";
$rrd_options .= " GPRINT:a:AVERAGE:'%6.2lf %s'";
$rrd_options .= " GPRINT:a:MAX:'%6.2lf %s\\n'";
} else {
$error_msg = 'Missing RRD';
}

View File

@ -42,13 +42,6 @@ if ($auth && is_customoid_graph($type, $subtype)) {
// Graph Template Missing");
}
if (! empty($error_msg)) {
// We have an error :(
graph_error($error_msg);
return;
}
if ($auth === null) {
// We are unauthenticated :(
graph_error($width < 200 ? 'No Auth' : 'No Authorization');
@ -83,13 +76,6 @@ if (! empty($command_only)) {
return;
}
// graph sent file not found flag
if (! empty($no_file)) {
graph_error($width < 200 ? 'No Data' : 'No Data file ' . $no_file);
return;
}
if (empty($rrd_options)) {
graph_error($width < 200 ? 'Def Error' : 'Graph Definition Error');

View File

@ -1427,6 +1427,13 @@
"value": "smtp"
}
},
"email_attach_graphs": {
"default": true,
"group": "alerting",
"section": "email",
"order": 4,
"type": "boolean"
},
"email_backend": {
"default": "mail",
"group": "alerting",

View File

@ -2740,31 +2740,6 @@ parameters:
count: 1
path: LibreNMS/Exceptions/JsonAppWrongVersionException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\RrdGraphException\\:\\:__construct\\(\\) has parameter \\$error with no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/RrdGraphException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\RrdGraphException\\:\\:__construct\\(\\) has parameter \\$exit_code with no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/RrdGraphException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\RrdGraphException\\:\\:__construct\\(\\) has parameter \\$image_output with no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/RrdGraphException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\RrdGraphException\\:\\:getImage\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/RrdGraphException.php
-
message: "#^Property LibreNMS\\\\Exceptions\\\\RrdGraphException\\:\\:\\$image_output has no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/RrdGraphException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\UnserializableRouteCache\\:\\:__construct\\(\\) has parameter \\$cli_php_version with no type specified\\.$#"
count: 1
@ -5616,7 +5591,12 @@ parameters:
path: LibreNMS/Util/Graph.php
-
message: "#^Method LibreNMS\\\\Util\\\\Graph\\:\\:getTypes\\(\\) has no return type specified\\.$#"
message: "#^Result of && is always false\\.$#"
count: 1
path: LibreNMS/Util/Graph.php
-
message: "#^Variable \\$rrd_filename in isset\\(\\) always exists and is always null\\.$#"
count: 1
path: LibreNMS/Util/Graph.php

View File

@ -625,6 +625,10 @@ return [
'description' => 'Auto TLS support',
'help' => 'Tries to use TLS before falling back to un-encrypted',
],
'email_attach_graphs' => [
'description' => 'Attach graph images',
'help' => 'This will generate a graph when the alert is raised and attach it and embed it in the email.',
],
'email_backend' => [
'description' => 'How to deliver mail',
'help' => 'The backend to use for sending email, can be mail, sendmail or SMTP',