Permalink
Browse files

Added Email notification support

  • Loading branch information...
richturner committed Sep 28, 2018
1 parent 91a8a1a commit 26bb531a91029efdfc171e0aebad070136702033
Showing with 1,050 additions and 309 deletions.
  1. +1 −0 client/src/main/resources/org/openremote/app/client/i18n/ManagerMessages.properties
  2. +2 −1 gradle.properties
  3. +1 −0 manager/build.gradle
  4. +326 −0 manager/src/main/java/org/openremote/manager/notification/EmailNotificationHandler.java
  5. +7 −2 manager/src/main/java/org/openremote/manager/notification/NotificationHandler.java
  6. +248 −235 manager/src/main/java/org/openremote/manager/notification/NotificationService.java
  7. +14 −14 manager/src/main/java/org/openremote/manager/notification/PushNotificationHandler.java
  8. +1 −5 manager/src/main/java/org/openremote/manager/security/ManagerKeycloakIdentityProvider.java
  9. +18 −22 manager/src/main/java/org/openremote/manager/setup/AbstractKeycloakSetup.java
  10. +2 −0 manager/src/main/java/org/openremote/manager/setup/builtin/KeycloakDemoSetup.java
  11. +9 −0 model/src/main/java/org/openremote/model/Constants.java
  12. +1 −0 model/src/main/java/org/openremote/model/ValueHolder.java
  13. +3 −1 model/src/main/java/org/openremote/model/attribute/AttributeType.java
  14. +9 −4 model/src/main/java/org/openremote/model/attribute/AttributeValueType.java
  15. +278 −0 model/src/main/java/org/openremote/model/notification/EmailNotificationMessage.java
  16. +9 −8 profile/deploy.yml
  17. +11 −4 test/src/test/groovy/org/openremote/test/console/ConsoleTest.groovy
  18. +98 −6 test/src/test/groovy/org/openremote/test/notification/NotificationTest.groovy
  19. +12 −7 test/src/test/groovy/org/openremote/test/rules/residence/ResidenceNotifyAlarmTriggerTest.groovy
@@ -240,6 +240,7 @@ validationFailure[VALUE_MISMATCH]=Value must be of type ''{0}''.
validationFailure[VALUE_PERCENTAGE_OUT_OF_RANGE]=Percentage value must be a number between 0 and 100.
validationFailure[VALUE_TEMPERATURE_OUT_OF_RANGE]=Temperature out of scale''s range.
validationFailure[VALUE_INVALID_COLOR_FORMAT]=Invalid color format.
validationFailure[VALUE_INVALID_EMAIL_FORMAT]=Invalid email format.
validationFailure[VALUE_NUMBER_OUT_OF_RANGE]=Number out of scale''s range.
validationFailure[META_ITEM_NAME_IS_REQUIRED]=Meta item name is required.
validationFailure[META_ITEM_VALUE_IS_REQUIRED]=Meta item value must be a valid ''{0}''.
View
@@ -44,4 +44,5 @@ ical4jVersion = 2.1.5
jafamaVersion = 2.3.1
friendlyIdVersion = 1.0.1
geotoolsVersion = 19.1
firebaseAdminVersion = 6.1.0
firebaseAdminVersion = 6.1.0
simpleJavaMailVersion = 5.0.4
View
@@ -17,6 +17,7 @@ dependencies {
compile "net.jafama:jafama:$jafamaVersion"
compile "org.geotools:gt-main:$geotoolsVersion"
compile "com.google.firebase:firebase-admin:$firebaseAdminVersion"
compile "org.simplejavamail:simple-java-mail:$simpleJavaMailVersion"
}
jar {
@@ -0,0 +1,326 @@
/*
* Copyright 2018, OpenRemote Inc.
*
* See the CONTRIBUTORS.txt file in the distribution for a
* full listing of individual contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.openremote.manager.notification;
import org.openremote.container.Container;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.model.AbstractValueHolder;
import org.openremote.model.asset.Asset;
import org.openremote.model.attribute.AttributeType;
import org.openremote.model.notification.AbstractNotificationMessage;
import org.openremote.model.notification.EmailNotificationMessage;
import org.openremote.model.notification.Notification;
import org.openremote.model.notification.NotificationSendResult;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.BaseAssetQuery;
import org.openremote.model.query.UserQuery;
import org.openremote.model.query.filter.*;
import org.openremote.model.security.User;
import org.openremote.model.util.TextUtil;
import org.simplejavamail.email.Email;
import org.simplejavamail.email.EmailBuilder;
import org.simplejavamail.email.EmailPopulatingBuilder;
import org.simplejavamail.email.Recipient;
import org.simplejavamail.mailer.Mailer;
import org.simplejavamail.mailer.MailerBuilder;
import org.simplejavamail.mailer.config.TransportStrategy;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static org.openremote.container.util.MapAccess.getBoolean;
import static org.openremote.container.util.MapAccess.getInteger;
import static org.openremote.model.Constants.*;
import static org.openremote.model.query.BaseAssetQuery.Include.ONLY_ID_AND_NAME_AND_ATTRIBUTES;
public class EmailNotificationHandler implements NotificationHandler {
private static final Logger LOG = Logger.getLogger(EmailNotificationHandler.class.getName());
protected String defaultFrom;
protected Mailer mailer;
protected ManagerIdentityService managerIdentityService;
protected AssetStorageService assetStorageService;
// Keep 100 user email addresses in cache for quick lookup
protected LinkedHashMap<String, EmailNotificationMessage.Recipient> userEmails = new LinkedHashMap<String, EmailNotificationMessage.Recipient>(100) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, EmailNotificationMessage.Recipient> eldest) {
return size() > 100;
}
};
// Keep 1000 asset email addresses in cache for quick lookup
protected LinkedHashMap<String, EmailNotificationMessage.Recipient> assetEmails = new LinkedHashMap<String, EmailNotificationMessage.Recipient>(1000) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, EmailNotificationMessage.Recipient> eldest) {
return size() > 1000;
}
};
@Override
public void init(Container container) throws Exception {
managerIdentityService = container.getService(ManagerIdentityService.class);
assetStorageService = container.getService(AssetStorageService.class);
// Configure SMTP
String host = container.getConfig().getOrDefault(SETUP_EMAIL_HOST, null);
int port = getInteger(container.getConfig(), SETUP_EMAIL_PORT, SETUP_EMAIL_PORT_DEFAULT);
String user = container.getConfig().getOrDefault(SETUP_EMAIL_USER, null);
String password = container.getConfig().getOrDefault(SETUP_EMAIL_PASSWORD, null);
defaultFrom = container.getConfig().getOrDefault(SETUP_EMAIL_FROM, SETUP_EMAIL_FROM_DEFAULT);
if (!TextUtil.isNullOrEmpty(host)) {
MailerBuilder.MailerRegularBuilder mailerBuilder = MailerBuilder.withSMTPServer(host, port, user, password);
boolean startTls = getBoolean(container.getConfig(), SETUP_EMAIL_TLS, SETUP_EMAIL_TLS_DEFAULT);
mailerBuilder.withTransportStrategy(startTls ? TransportStrategy.SMTP_TLS : TransportStrategy.SMTP);
mailer = mailerBuilder.buildMailer();
try {
mailer.testConnection();
} catch (Exception e) {
LOG.log(Level.SEVERE, "Failed to connect to SMTP server so disabling email notifications");
mailer = null;
}
}
}
@Override
public void start(Container container) throws Exception {
}
@Override
public void stop(Container container) throws Exception {
}
@Override
public boolean isValid() {
return mailer != null;
}
@Override
public String getTypeName() {
return EmailNotificationMessage.TYPE;
}
@Override
public boolean isMessageValid(AbstractNotificationMessage message) {
return (message instanceof EmailNotificationMessage);
// if (!(message instanceof EmailNotificationMessage)) {
// LOG.warning("Invalid message: '" + message.getClass().getSimpleName() + "' is not an instance of PushNotificationMessage");
// return false;
// }
//
// EmailNotificationMessage emailMessage = (EmailNotificationMessage) message;
// if (emailMessage.getFrom() == null || (
// (emailMessage.getTo() == null || emailMessage.getTo().isEmpty())
// && (emailMessage.getCc() == null || emailMessage.getCc().isEmpty())
// && (emailMessage.getBcc() == null || emailMessage.getBcc().isEmpty()))) {
// LOG.warning("Invalid message: must contain a from and at least one recipient");
// return false;
// }
//
// return true;
}
@Override
public Notification.Targets mapTarget(Notification.Source source, String sourceId, Notification.TargetType targetType, String targetId, AbstractNotificationMessage message) {
switch (targetType) {
case TENANT:
// Find all users in this tenant
User[] users = managerIdentityService
.getIdentityProvider()
.getUsers(new UserQuery().tenant(new TenantPredicate(targetId)));
if (users.length == 0) {
LOG.fine("No users found in target realm: " + targetId);
return null;
}
Arrays.stream(users).forEach(user -> {
EmailNotificationMessage.Recipient recipient =
new EmailNotificationMessage.Recipient(user.getFullName(), user.getEmail());
userEmails.put(user.getId(), recipient);
});
return new Notification.Targets(
Notification.TargetType.USER,
Arrays.stream(users).map(User::getId).collect(Collectors.toList()));
case USER:
// Nothing to do here
return new Notification.Targets(targetType, targetId);
case ASSET:
// Find descendant assets with email attribute
List<Asset> assets = assetStorageService.findAll(
new AssetQuery()
.select(new BaseAssetQuery.Select(
ONLY_ID_AND_NAME_AND_ATTRIBUTES,
false,
AttributeType.EMAIL.getName()))
.path(new PathPredicate(targetId))
.attributes(new AttributePredicate(
new StringPredicate(AttributeType.EMAIL.getName()),
new ValueNotEmptyPredicate())));
if (assets.isEmpty()) {
LOG.fine("No assets with email attribute descendants of target asset");
return null;
}
assets.forEach(asset -> {
EmailNotificationMessage.Recipient recipient =
new EmailNotificationMessage.Recipient(
asset.getName(),
asset.getAttribute(AttributeType.EMAIL)
.flatMap(AbstractValueHolder::getValueAsString)
.orElse(null));
assetEmails.put(asset.getId(), recipient);
});
return new Notification.Targets(
Notification.TargetType.ASSET,
assets.stream().map(Asset::getId).collect(Collectors.toList()));
}
return null;
}
@Override
public NotificationSendResult sendMessage(long id, Notification.Source source, String sourceId, Notification.TargetType targetType, String targetId, AbstractNotificationMessage message) {
// Check handler is valid
if (!isValid()) {
LOG.warning("SMTP invalid configuration so ignoring");
return NotificationSendResult.failure("SMTP invalid configuration so ignoring");
}
EmailNotificationMessage.Recipient recipient;
switch (targetType) {
case USER:
recipient = getUserRecipient(targetId);
break;
case ASSET:
recipient = getAssetRecipient(targetId);
break;
default:
LOG.warning("Target type not supported: " + targetType);
return NotificationSendResult.failure("Target type not supported: " + targetType);
}
if (recipient == null || TextUtil.isNullOrEmpty(recipient.getAddress())) {
LOG.warning("No recipient found for " + targetType.name().toLowerCase() + ": " + targetId);
return NotificationSendResult.failure("No recipient found for " + targetType.name().toLowerCase() + ": " + targetId);
}
EmailNotificationMessage emailNotificationMessage = (EmailNotificationMessage) message;
// Set from based on source if not already set
if (emailNotificationMessage.getFrom() == null) {
emailNotificationMessage.setFrom(defaultFrom);
}
// Override any to addresses with target recipient
emailNotificationMessage.setTo(recipient);
return sendMessage(buildEmail(id, emailNotificationMessage));
}
public NotificationSendResult sendMessage(Email email) {
try {
mailer.sendMail(email);
return NotificationSendResult.success();
} catch (Exception e) {
LOG.log(Level.WARNING, "Email send failed: " + e.getMessage(), e);
return NotificationSendResult.failure("Email send failed: " + e.getMessage());
}
}
protected EmailNotificationMessage.Recipient getUserRecipient(String userId) {
if (userEmails.containsKey(userId)) {
return userEmails.get(userId);
}
User[] users = managerIdentityService.getIdentityProvider().getUsers(Collections.singletonList(userId));
if (users == null || users.length == 0) {
return null;
}
return new EmailNotificationMessage.Recipient(users[0].getFullName(), users[0].getEmail());
}
protected EmailNotificationMessage.Recipient getAssetRecipient(String assetId) {
if (assetEmails.containsKey(assetId)) {
return assetEmails.get(assetId);
}
Asset asset = assetStorageService.find(assetId);
if (asset == null) {
return null;
}
return new EmailNotificationMessage.Recipient(asset.getName(), asset.getAttribute(AttributeType.EMAIL)
.flatMap(AbstractValueHolder::getValueAsString)
.orElse(null));
}
protected Email buildEmail(long id, EmailNotificationMessage emailNotificationMessage) {
EmailPopulatingBuilder emailBuilder = EmailBuilder.startingBlank()
.from(convertRecipient(emailNotificationMessage.getFrom()))
.withReplyTo(convertRecipient(emailNotificationMessage.getReplyTo()))
.withSubject(emailNotificationMessage.getSubject())
.withPlainText(emailNotificationMessage.getText())
.withHTMLText(emailNotificationMessage.getHtml());
if (emailNotificationMessage.getTo() != null) {
emailBuilder.to(
emailNotificationMessage.getTo().stream()
.map(this::convertRecipient).collect(Collectors.toList()));
}
if (emailNotificationMessage.getCc() != null) {
emailBuilder.cc(
emailNotificationMessage.getCc().stream()
.map(this::convertRecipient).collect(Collectors.toList()));
}
if (emailNotificationMessage.getBcc() != null) {
emailBuilder.bcc(
emailNotificationMessage.getBcc().stream()
.map(this::convertRecipient).collect(Collectors.toList()));
}
// Use the notification ID as the message ID
emailBuilder.fixingMessageId("<" + id + "@openremote.io>");
return emailBuilder.buildEmail();
}
protected Recipient convertRecipient(EmailNotificationMessage.Recipient recipient) {
return recipient == null ? null : new Recipient(recipient.getName(), recipient.getAddress(), null);
}
}
@@ -43,6 +43,11 @@
*/
String getTypeName();
/**
* Indicates if this handler has a valid configuration and is usable.
*/
boolean isValid();
/**
* Allows the handler to validate the specified {@link AbstractNotificationMessage}.
*/
@@ -56,7 +61,7 @@
* {@link Notification.TargetType#ASSET} where {@link AssetType} equals {@link AssetType#CONSOLE} then the handler
* needs to find all console assets that belong to the specified tenant).
*/
Notification.Targets mapTarget(Notification.TargetType targetType, String targetId, AbstractNotificationMessage message);
Notification.Targets mapTarget(Notification.Source source, String sourceId, Notification.TargetType targetType, String targetId, AbstractNotificationMessage message);
/**
* Send the specified {@link AbstractNotificationMessage} to the target; the target supplied would be a target
@@ -67,5 +72,5 @@
* The ID can be used by the {@link NotificationHandler} to update the delivered and/or acknowledged status of the notification
* by calling {@link NotificationService#setNotificationDelivered} or {@link NotificationService#setNotificationAcknowleged}
*/
NotificationSendResult sendMessage(long id, Notification.TargetType targetType, String targetId, AbstractNotificationMessage message);
NotificationSendResult sendMessage(long id, Notification.Source source, String sourceId, Notification.TargetType targetType, String targetId, AbstractNotificationMessage message);
}
Oops, something went wrong.

0 comments on commit 26bb531

Please sign in to comment.