From 91397cbce01aaa8c28eeaf891f52fc5283bf5fac Mon Sep 17 00:00:00 2001 From: johnathana Date: Thu, 1 Oct 2020 09:13:14 +0300 Subject: [PATCH] SMTP Mail Plugin --- README.md | 2 + build.sbt | 17 ++++- lenses-mail-alerts-plugin/README.md | 56 ++++++++++++++ .../plugin/mail/MailAlertConfig.scala | 9 +++ .../plugin/mail/MailAlertPlugin.scala | 75 +++++++++++++++++++ .../plugin/mail/MailAlertService.scala | 55 ++++++++++++++ .../alerting/plugin/mail/Metadata.scala | 6 ++ .../alerting/plugin/mail/TryUtils.scala | 18 +++++ 8 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 lenses-mail-alerts-plugin/README.md create mode 100644 lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertConfig.scala create mode 100644 lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertPlugin.scala create mode 100644 lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertService.scala create mode 100644 lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/Metadata.scala create mode 100644 lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/TryUtils.scala diff --git a/README.md b/README.md index 99e943a..67ebd1d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ some officially supported implementations. - defines an implementation for Prometheus Alertmanager - [`lenses-cloudwatch-plugin`](./lenses-cloudwatch-plugin) - defines an implementation for CloudWatch Events +- [`lenses-mail-alerts-plugin`](./lenses-mail-alerts-plugin) + - defines an implementation for SMTP emails All modules are published to Maven central. In addition, standalone JARs of each of plugin integration are available to download from Github releases, ready diff --git a/build.sbt b/build.sbt index a36dc44..13fef3c 100644 --- a/build.sbt +++ b/build.sbt @@ -62,6 +62,7 @@ ThisBuild / publishTo := sonatypePublishTo.value val scalaJava8Compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.9.0" val sl4fj = "org.slf4j" % "slf4j-api" % "1.7.30" val jslack = "com.github.seratch" % "jslack" % "1.0.26" +val mail = "javax.mail" % "mail" % "1.5.0-b01" val awsCloudWatchEvents = "software.amazon.awssdk" % "cloudwatchevents" % "2.9.26" val httpclient = "org.apache.httpcomponents" % "httpclient" % "4.5.9" val circeParser = "io.circe" %% "circe-parser" % "0.13.0" @@ -74,7 +75,7 @@ val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" // Root project lazy val root = (project in file(".")) .disablePlugins(AssemblyPlugin) - .aggregate(alertsPluginApi, slackAlertsPlugin, alertManagerPlugin, cloudWatchAlertsPlugin) + .aggregate(alertsPluginApi, slackAlertsPlugin, alertManagerPlugin, mailAlertsPlugin, cloudWatchAlertsPlugin) .settings( name := "lenses-alerts-plugin", ghreleaseRepoOrg := "lensesio", @@ -82,6 +83,7 @@ lazy val root = (project in file(".")) ghreleaseAssets := List( (slackAlertsPlugin / assembly / assemblyOutputPath).value, (alertManagerPlugin / assembly / assemblyOutputPath).value, + (mailAlertsPlugin / assembly / assemblyOutputPath).value, (cloudWatchAlertsPlugin / assembly / assemblyOutputPath).value, ), skip in publish := true @@ -127,6 +129,19 @@ lazy val alertManagerPlugin = (project in file("lenses-alertmanager-plugin")) assembly / assemblyJarName := s"${name.value}-standalone-${version.value}.jar", ) +lazy val mailAlertsPlugin = (project in file("lenses-mail-alerts-plugin")) + .disablePlugins(SbtGithubReleasePlugin) + .dependsOn(alertsPluginApi) + .settings( + name := "lenses-mail-alerts-plugin", + description := "Lenses.io Mail Alerts Plugin", + libraryDependencies += sl4fj % Provided, + libraryDependencies += mail, + libraryDependencies += logbackClassic % Test, + libraryDependencies += scalaTest % Test, + assembly / assemblyJarName := s"${name.value}-standalone-${version.value}.jar", + ) + lazy val cloudWatchAlertsPlugin = (project in file("lenses-cloudwatch-plugin")) .disablePlugins(SbtGithubReleasePlugin) .dependsOn(alertsPluginApi) diff --git a/lenses-mail-alerts-plugin/README.md b/lenses-mail-alerts-plugin/README.md new file mode 100644 index 0000000..76ed8a0 --- /dev/null +++ b/lenses-mail-alerts-plugin/README.md @@ -0,0 +1,56 @@ +## Lenses alert mail plugin + +This is a Lenses plugin allowing to send Lenses raised alerts by email. + +### Configuration + +In order to enable the plugin, the following settings have to be set in Lenses: + +``` + lenses.alerting.plugins = [ + { + class=io.lenses.alerting.plugin.mail.MailAlertPlugin + config={ + key1=value1 + key2=value2 + ... + } + } + ] +``` + +Available configuration options: + + |Configuration | Type | Description | + |-----------------|--------|--------------------------------------------------------------------| + | from-address | String | The sender mail address. | + | email-addresses | String | Comma separated address strings in RFC822 format. | + | username | String | The user name. | + | password | String | The password. | + | smtp-host | String | Host name of the SMTP mail server. | + | smtp-port | String | Port of the SMTP mail server. | + | smtp-auth | String | If true, attempt to authenticate the user using the AUTH command. | + | smtp-starttls | String | If true, enables the use of the STARTTLS command. | + + + +Google SMTP server example: + +``` + lenses.alerting.plugins = [ + { + class=io.lenses.alerting.plugin.mail.MailAlertPlugin + config={ + from-address="sender@gmail.com" + email-addresses="target1@mail.com, target2@mail.com" + username="user" + password="passwd" // Generate application-specific password as described here: https://support.google.com/mail/?p=InvalidSecondFactor + smtp-host="smtp.gmail.com" + smtp-port="587" + smtp-auth="true" + smtp-starttls="true" + } + } + ] +``` + diff --git a/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertConfig.scala b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertConfig.scala new file mode 100644 index 0000000..eaf71ff --- /dev/null +++ b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertConfig.scala @@ -0,0 +1,9 @@ +package io.lenses.alerting.plugin.mail + +import java.util.Properties + +case class MailAlertConfig(senderAddress: String, + emailAddresses: String, + user: String, + passwd: String, + mailProps: Properties) diff --git a/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertPlugin.scala b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertPlugin.scala new file mode 100644 index 0000000..81d5e23 --- /dev/null +++ b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertPlugin.scala @@ -0,0 +1,75 @@ +package io.lenses.alerting.plugin.mail + +import java.util +import java.util.Properties + +import io.lenses.alerting.plugin.javaapi.util.{Try => JTry} +import io.lenses.alerting.plugin.javaapi.{AlertingPlugin, AlertingService, ConfigEntry} +import io.lenses.alerting.plugin.mail.MailAlertsPlugin._ +import io.lenses.alerting.plugin.mail.TryUtils.TryExtension + +import scala.collection.JavaConverters._ +import scala.util.Try + +class MailAlertPlugin extends AlertingPlugin with Metadata { + + override val name: String = "Mail" + + override val description: String = "Plugin to support pushing Lenses alerts as emails" + + override def init(config: util.Map[String, String]): JTry[AlertingService] = Try { + val map = config.asScala + def getOrError(key: String): String = { + map.getOrElse(key, throw new IllegalArgumentException(s"Invalid configuration for Mail plugin'[$key]'")) + } + + val senderAddress = getOrError(SENDER_ADDRESS) + val emailAddresses = getOrError(EMAIL_ADDRESSES) + val username = getOrError(USERNAME) + val password = getOrError(PASSWORD) + + def buildMailProperties = { + val smtpHost = getOrError(SMTP_HOST) + val smtpPort = getOrError(SMTP_PORT) + val smtpAuth = getOrError(SMTP_AUTH) + val smtpStartTLS = getOrError(SMTP_STARTTLS) + + // https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html + val props = new Properties() + props.put("mail.smtp.host", smtpHost) + props.put("mail.smtp.port", smtpPort) + props.put("mail.smtp.auth", smtpAuth) + props.put("mail.smtp.starttls.enable", smtpStartTLS) + props + } + + val props: Properties = buildMailProperties + val as: AlertingService = new MailAlertService(name, description, MailAlertConfig(senderAddress, emailAddresses, username, password, props)) + as + }.asJava + + override def configKeys(): util.List[ConfigEntry] = { + import scala.collection.JavaConverters._ + List( + new ConfigEntry(SENDER_ADDRESS, "The sender mail address"), + new ConfigEntry(EMAIL_ADDRESSES,"Comma separated address strings in RFC822 format"), + new ConfigEntry(USERNAME,"Username"), + new ConfigEntry(PASSWORD,"Password"), + new ConfigEntry(SMTP_HOST ,"Host name of the SMTP mail server"), + new ConfigEntry(SMTP_PORT,"Port of the SMTP mail server"), + new ConfigEntry(SMTP_AUTH,"If true, attempt to authenticate the user using the AUTH command"), + new ConfigEntry(SMTP_STARTTLS,"If true, enables the use of the STARTTLS command"), + ).asJava + } +} + +object MailAlertsPlugin { + val SENDER_ADDRESS = "from-address" + val EMAIL_ADDRESSES = "email-addresses" + val USERNAME = "username" + val PASSWORD = "password" + val SMTP_HOST = "smtp-host" + val SMTP_PORT = "smtp-port" + val SMTP_AUTH = "smtp-auth" + val SMTP_STARTTLS = "smtp-starttls" +} diff --git a/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertService.scala b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertService.scala new file mode 100644 index 0000000..68d3ec2 --- /dev/null +++ b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/MailAlertService.scala @@ -0,0 +1,55 @@ +package io.lenses.alerting.plugin.mail + +import java.sql.Timestamp +import java.util +import java.util.Date + +import io.lenses.alerting.plugin.Alert +import io.lenses.alerting.plugin.javaapi.AlertingService +import io.lenses.alerting.plugin.javaapi.util.{Try => JTry} +import io.lenses.alerting.plugin.mail.TryUtils.TryExtension +import javax.mail.internet.{InternetAddress, MimeMessage} +import javax.mail.{Authenticator, Message, PasswordAuthentication, Session, Transport} + +import scala.util.Try + +class MailAlertService(override val name: String, + override val description: String, + config: MailAlertConfig) extends AlertingService with Metadata { + + val session: Session = Session.getInstance(config.mailProps, new Authenticator() { + override protected def getPasswordAuthentication = new PasswordAuthentication(config.user, config.passwd) + }) + + override def displayedInformation(): util.Map[String, String] = { + import scala.collection.JavaConverters._ + Map("Email Addresses" -> config.emailAddresses).asJava + } + + override def publish(alert: Alert): JTry[Alert] = { + Try { + val message = new MimeMessage(session) + message.setFrom(new InternetAddress(config.senderAddress)) + message.setRecipients(Message.RecipientType.TO, config.emailAddresses) + message.setSubject("Lenses Alert") + message.setSentDate(new Date()) + message.setText(alertToText(alert)) + + Transport.send(message) + alert + }.asJava + } + + private def alertToText(alert: Alert): String = { + val sb = new StringBuilder() + sb.append("AlertId: ").append(alert.alertId).append("\n") + sb.append("Category: ").append(alert.category).append("\n") + sb.append("Level: ").append(alert.level).append("\n") + sb.append("Timestamp: ").append(new Timestamp(alert.timestamp).toString).append("\n") + sb.append("Labels: ").append(alert.labels).append("\n") + sb.append("Tags: ").append(alert.tags).append("\n") + sb.append("Summary: ").append(alert.summary).append("\n") + alert.docs.ifPresent(docs => sb.append("Docs: ").append(docs).append("\n")) + sb.toString() + } +} diff --git a/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/Metadata.scala b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/Metadata.scala new file mode 100644 index 0000000..32030b6 --- /dev/null +++ b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/Metadata.scala @@ -0,0 +1,6 @@ +package io.lenses.alerting.plugin.mail + +trait Metadata { + def name(): String = "Mail" + def description(): String = "Send an email for each Lenses alert." +} diff --git a/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/TryUtils.scala b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/TryUtils.scala new file mode 100644 index 0000000..233b953 --- /dev/null +++ b/lenses-mail-alerts-plugin/src/main/scala/io/lenses/alerting/plugin/mail/TryUtils.scala @@ -0,0 +1,18 @@ +package io.lenses.alerting.plugin.mail + +import io.lenses.alerting.plugin.javaapi.util.{Failure => JFailure, Success => JSuccess, Try => JTry} + +import scala.util.{Failure, Success, Try} + +object TryUtils { + + implicit class TryExtension[T](val t: Try[T]) extends AnyVal { + def asJava: JTry[T] = { + t match { + case Success(value) => new JSuccess[T](value) + case Failure(exception) => new JFailure[T](exception) + } + } + } + +}