diff --git a/LibreNMS/Alert/Transport/Mail.php b/LibreNMS/Alert/Transport/Mail.php index 324578fd17..76ff0660a9 100644 --- a/LibreNMS/Alert/Transport/Mail.php +++ b/LibreNMS/Alert/Transport/Mail.php @@ -46,7 +46,7 @@ class Mail extends Transport $msg = preg_replace("/(?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', diff --git a/LibreNMS/Data/Store/Rrd.php b/LibreNMS/Data/Store/Rrd.php index b732fffb19..92dae88252 100644 --- a/LibreNMS/Data/Store/Rrd.php +++ b/LibreNMS/Data/Store/Rrd.php @@ -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 diff --git a/LibreNMS/Exceptions/RrdGraphException.php b/LibreNMS/Exceptions/RrdGraphException.php index 3317ac6ff9..3c55e1cd5c 100644 --- a/LibreNMS/Exceptions/RrdGraphException.php +++ b/LibreNMS/Exceptions/RrdGraphException.php @@ -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, + ); + } } diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index 74e390195b..c40256c99d 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -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 = "
"; + $cmd_output .= "

RRDTool Command

"; + $cmd_output .= "
";
+            $cmd_output .= escapeshellcmd('rrdtool ' . Rrd::buildCommand('graph', Config::get('temp_dir') . '/' . strgen(), $rrd_options));
+            $cmd_output .= '
'; + try { + $cmd_output .= Rrd::graph($rrd_options); + } catch (RrdGraphException $e) { + $cmd_output .= "

RRDTool Output

"; + $cmd_output .= "
";
+                $cmd_output .= $e->getMessage();
+                $cmd_output .= '
'; + } + $cmd_output .= '
'; + + 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 '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 << + + + + $text + + + + +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; + } } diff --git a/LibreNMS/Util/Mail.php b/LibreNMS/Util/Mail.php index ff1406e328..738af32c18 100644 --- a/LibreNMS/Util/Mail.php +++ b/LibreNMS/Util/Mail.php @@ -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; + } } diff --git a/app/Http/Controllers/GraphController.php b/app/Http/Controllers/GraphController.php index 52833b513e..a73d06e283 100644 --- a/app/Http/Controllers/GraphController.php +++ b/app/Http/Controllers/GraphController.php @@ -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); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 95334dc4d1..63b69a967b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -80,8 +80,8 @@ class AppServiceProvider extends ServiceProvider return "'; ?>"; }); - Blade::directive('graphImage', function ($vars, $base64 = false) { - return ""; + Blade::directive('graphImage', function ($vars, $flags = 0) { + return ""; }); } diff --git a/doc/Alerting/Transports.md b/doc/Alerting/Transports.md index 66adf8d6d8..410f51a1d0 100644 --- a/doc/Alerting/Transports.md +++ b/doc/Alerting/Transports.md @@ -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:** diff --git a/html/mix-manifest.json b/html/mix-manifest.json index 5c089fa24f..439f47be48 100644 --- a/html/mix-manifest.json +++ b/html/mix-manifest.json @@ -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" } diff --git a/includes/html/functions.inc.php b/includes/html/functions.inc.php index 58407da339..929f516cb9 100644 --- a/includes/html/functions.inc.php +++ b/includes/html/functions.inc.php @@ -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 << - - - - $text - - - - -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() diff --git a/includes/html/graphs/application/nginx_req.inc.php b/includes/html/graphs/application/nginx_req.inc.php index 7b44cb469d..e4d13e2d99 100644 --- a/includes/html/graphs/application/nginx_req.inc.php +++ b/includes/html/graphs/application/nginx_req.inc.php @@ -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'; } diff --git a/includes/html/graphs/graph.inc.php b/includes/html/graphs/graph.inc.php index 21f6dea89c..2de8b81602 100644 --- a/includes/html/graphs/graph.inc.php +++ b/includes/html/graphs/graph.inc.php @@ -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'); diff --git a/misc/config_definitions.json b/misc/config_definitions.json index 6ae7182f84..0cd182425b 100644 --- a/misc/config_definitions.json +++ b/misc/config_definitions.json @@ -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", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index adc501dd7c..0b1a711817 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index f3d98bab5f..a1c7c4d2f7 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -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',