diff --git a/pom.xml b/pom.xml
index f6257b5bc1c..4d324b77cc6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,13 @@
test
+
+ com.google.apis
+ google-api-services-gmail
+ v1-rev23-1.19.1
+ test
+
+
org.codehaus.groovy
groovy-all
diff --git a/src/main/java/org/elasticsearch/alerts/AlertsPlugin.java b/src/main/java/org/elasticsearch/alerts/AlertsPlugin.java
index 33a64edc338..6770a5c46f4 100644
--- a/src/main/java/org/elasticsearch/alerts/AlertsPlugin.java
+++ b/src/main/java/org/elasticsearch/alerts/AlertsPlugin.java
@@ -5,6 +5,7 @@
*/
package org.elasticsearch.alerts;
+import org.elasticsearch.alerts.actions.email.service.InternalEmailService;
import org.elasticsearch.alerts.support.init.InitializingService;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.component.LifecycleComponent;
@@ -47,7 +48,8 @@ public class AlertsPlugin extends AbstractPlugin {
// the initialization service must be first in the list
// as other services may depend on one of the initialized
// constructs
- InitializingService.class);
+ InitializingService.class,
+ InternalEmailService.class);
}
@Override
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java b/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java
index b4794f59d33..2aa704a3036 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java
@@ -22,6 +22,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
+import java.util.Objects;
/**
*/
@@ -29,23 +30,23 @@ public class EmailAction extends Action {
public static final String TYPE = "email";
- private final Email.Builder email;
- private final Authentication auth;
- private final Profile profile;
- private final String account;
- private final Template subject;
- private final Template textBody;
- private final Template htmlBody;
- private final boolean attachPayload;
+ final Email emailPrototype;
+ final Authentication auth;
+ final Profile profile;
+ final String account;
+ final Template subject;
+ final Template textBody;
+ final Template htmlBody;
+ final boolean attachPayload;
- private final EmailService emailService;
+ final EmailService emailService;
- public EmailAction(ESLogger logger, EmailService emailService, Email.Builder email, Authentication auth, Profile profile,
+ public EmailAction(ESLogger logger, EmailService emailService, Email emailPrototype, Authentication auth, Profile profile,
String account, Template subject, Template textBody, Template htmlBody, boolean attachPayload) {
super(logger);
this.emailService = emailService;
- this.email = email;
+ this.emailPrototype = emailPrototype;
this.auth = auth;
this.profile = profile;
this.account = account;
@@ -64,6 +65,10 @@ public class EmailAction extends Action {
public Result execute(ExecutionContext ctx, Payload payload) throws IOException {
ImmutableMap model = templateModel(ctx, payload);
+ Email.Builder email = Email.builder()
+ .id(ctx.id())
+ .copyFrom(emailPrototype);
+
email.id(ctx.id());
email.subject(subject.render(model));
email.textBody(textBody.render(model));
@@ -73,7 +78,7 @@ public class EmailAction extends Action {
}
if (attachPayload) {
- Attachment.Bytes attachment = new Attachment.XContent.Yaml("payload", "payload.yml", "alert execution output", payload);
+ Attachment.Bytes attachment = new Attachment.XContent.Yaml("payload", "payload.yml", payload);
email.attach(attachment);
}
@@ -86,22 +91,71 @@ public class EmailAction extends Action {
}
}
+ @Override
+ public int hashCode() {
+ return Objects.hash(emailPrototype, auth, profile, account, subject, textBody, htmlBody, attachPayload);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ final EmailAction other = (EmailAction) obj;
+ return Objects.equals(this.emailPrototype, other.emailPrototype)
+ && Objects.equals(this.auth, other.auth)
+ && Objects.equals(this.profile, other.profile)
+ && Objects.equals(this.account, other.account)
+ && Objects.equals(this.subject, other.subject)
+ && Objects.equals(this.textBody, other.textBody)
+ && Objects.equals(this.htmlBody, other.htmlBody)
+ && Objects.equals(this.attachPayload, other.attachPayload);
+ }
+
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (account != null) {
- builder.field(EmailAction.Parser.ACCOUNT_FIELD.getPreferredName(), account);
+ builder.field(Parser.ACCOUNT_FIELD.getPreferredName(), account);
}
if (profile != null) {
- builder.field(EmailAction.Parser.PROFILE_FIELD.getPreferredName(), profile);
+ builder.field(Parser.PROFILE_FIELD.getPreferredName(), profile);
+ }
+ if (auth != null) {
+ builder.field(Parser.USER_FIELD.getPreferredName(), auth.user());
+ builder.field(Parser.PASSWORD_FIELD.getPreferredName(), auth.password());
+ }
+ builder.field(Parser.ATTACH_PAYLOAD_FIELD.getPreferredName(), attachPayload);
+ if (emailPrototype.from() != null) {
+ builder.field(Email.FROM_FIELD.getPreferredName(), emailPrototype.from());
+ }
+ if (emailPrototype.to() != null && !emailPrototype.to().isEmpty()) {
+ builder.field(Email.TO_FIELD.getPreferredName(), (ToXContent) emailPrototype.to());
+ }
+ if (emailPrototype.cc() != null && !emailPrototype.cc().isEmpty()) {
+ builder.field(Email.CC_FIELD.getPreferredName(), (ToXContent) emailPrototype.cc());
+ }
+ if (emailPrototype.bcc() != null && !emailPrototype.bcc().isEmpty()) {
+ builder.field(Email.BCC_FIELD.getPreferredName(), (ToXContent) emailPrototype.bcc());
+ }
+ if (emailPrototype.replyTo() != null && !emailPrototype.replyTo().isEmpty()) {
+ builder.field(Email.REPLY_TO_FIELD.getPreferredName(), (ToXContent) emailPrototype.replyTo());
}
- builder.field(Email.TO_FIELD.getPreferredName(), (ToXContent) email.to());
if (subject != null) {
builder.field(Email.SUBJECT_FIELD.getPreferredName(), subject);
}
if (textBody != null) {
builder.field(Email.TEXT_BODY_FIELD.getPreferredName(), textBody);
}
+ if (htmlBody != null) {
+ builder.field(Email.HTML_BODY_FIELD.getPreferredName(), htmlBody);
+ }
+ if (emailPrototype.priority() != null) {
+ builder.field(Email.PRIORITY_FIELD.getPreferredName(), emailPrototype.priority());
+ }
return builder.endObject();
}
@@ -136,7 +190,7 @@ public class EmailAction extends Action {
String password = null;
String account = null;
Profile profile = null;
- Email.Builder email = Email.builder();
+ Email.Builder email = Email.builder().id("prototype");
Template subject = null;
Template textBody = null;
Template htmlBody = null;
@@ -202,12 +256,9 @@ public class EmailAction extends Action {
}
}
- if (email.to() == null || email.to().isEmpty()) {
- throw new ActionSettingsException("could not parse email action. [to] was not found or was empty");
- }
+ Authentication auth = user != null ? new Authentication(user, password) : null;
- Authentication auth = new Authentication(user, password);
- return new EmailAction(logger, emailService, email, auth, profile, account, subject, textBody, htmlBody, attachPayload);
+ return new EmailAction(logger, emailService, email.build(), auth, profile, account, subject, textBody, htmlBody, attachPayload);
}
@Override
@@ -263,7 +314,6 @@ public class EmailAction extends Action {
super(type, success);
}
-
public static class Success extends Result {
private final EmailService.EmailSent sent;
@@ -297,6 +347,10 @@ public class EmailAction extends Action {
this.reason = reason;
}
+ public String reason() {
+ return reason;
+ }
+
@Override
protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException {
return builder.field("reason", reason);
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Account.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Account.java
index 20745ddaee8..b1c8296ebf2 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Account.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Account.java
@@ -43,8 +43,12 @@ public class Account {
// applying the defaults on missing emails fields
email = config.defaults.apply(email);
+ if (email.to == null) {
+ throw new EmailException("email must have [to] recipient");
+ }
+
Transport transport = session.getTransport(SMTP_PROTOCOL);
- String user = auth != null ? auth.username() : null;
+ String user = auth != null ? auth.user() : null;
if (user == null) {
user = config.smtp.user;
if (user == null) {
@@ -85,10 +89,10 @@ public class Account {
static final String SMTP_SETTINGS_PREFIX = "mail.smtp.";
- private final String name;
- private final Profile profile;
- private final Smtp smtp;
- private final EmailDefaults defaults;
+ final String name;
+ final Profile profile;
+ final Smtp smtp;
+ final EmailDefaults defaults;
public Config(String name, Settings settings) {
this.name = name;
@@ -106,16 +110,16 @@ public class Account {
static class Smtp {
- private final String host;
- private final int port;
- private final String user;
- private final String password;
- private final Properties properties;
+ final String host;
+ final int port;
+ final String user;
+ final String password;
+ final Properties properties;
public Smtp(Settings settings) {
- host = settings.get("host");
- port = settings.getAsInt("port", settings.getAsInt("localport", 25));
- user = settings.get("user", settings.get("from", settings.get("local_address", null)));
+ host = settings.get("host", settings.get("localaddress", settings.get("local_address")));
+ port = settings.getAsInt("port", settings.getAsInt("localport", settings.getAsInt("local_port", 25)));
+ user = settings.get("user", settings.get("from", null));
password = settings.get("password", null);
properties = loadSmtpProperties(settings);
}
@@ -165,7 +169,7 @@ public class Account {
* needed on each alert (e.g. if all the emails are always sent to the same recipients
* one could set those here and leave them out on the alert definition).
*/
- class EmailDefaults {
+ static class EmailDefaults {
final Email.Address from;
final Email.AddressList replyTo;
@@ -186,16 +190,59 @@ public class Account {
}
Email apply(Email email) {
- return Email.builder()
- .from(from)
- .replyTo(replyTo)
- .priority(priority)
- .to(to)
- .cc(cc)
- .bcc(bcc)
- .subject(subject)
- .copyFrom(email)
- .build();
+ Email.Builder builder = Email.builder().copyFrom(email);
+ if (email.from == null) {
+ builder.from(from);
+ }
+ if (email.replyTo == null) {
+ builder.replyTo(replyTo);
+ }
+ if (email.priority == null) {
+ builder.priority(priority);
+ }
+ if (email.to == null) {
+ builder.to(to);
+ }
+ if (email.cc == null) {
+ builder.cc(cc);
+ }
+ if (email.bcc == null) {
+ builder.bcc(bcc);
+ }
+ if (email.subject == null) {
+ builder.subject(subject);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ EmailDefaults that = (EmailDefaults) o;
+
+ if (bcc != null ? !bcc.equals(that.bcc) : that.bcc != null) return false;
+ if (cc != null ? !cc.equals(that.cc) : that.cc != null) return false;
+ if (from != null ? !from.equals(that.from) : that.from != null) return false;
+ if (priority != that.priority) return false;
+ if (replyTo != null ? !replyTo.equals(that.replyTo) : that.replyTo != null) return false;
+ if (subject != null ? !subject.equals(that.subject) : that.subject != null) return false;
+ if (to != null ? !to.equals(that.to) : that.to != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = from != null ? from.hashCode() : 0;
+ result = 31 * result + (replyTo != null ? replyTo.hashCode() : 0);
+ result = 31 * result + (priority != null ? priority.hashCode() : 0);
+ result = 31 * result + (to != null ? to.hashCode() : 0);
+ result = 31 * result + (cc != null ? cc.hashCode() : 0);
+ result = 31 * result + (bcc != null ? bcc.hashCode() : 0);
+ result = 31 * result + (subject != null ? subject.hashCode() : 0);
+ return result;
}
}
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Accounts.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Accounts.java
index c9a15893d28..9f70adff620 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Accounts.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Accounts.java
@@ -8,7 +8,6 @@ package org.elasticsearch.alerts.actions.email.service;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.Settings;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -21,31 +20,27 @@ public class Accounts {
private final Map accounts;
public Accounts(Settings settings, ESLogger logger) {
- settings = settings.getAsSettings("account");
- Map accounts = new HashMap<>();
- for (String name : settings.names()) {
- Account.Config config = new Account.Config(name, settings.getAsSettings(name));
+ Settings accountsSettings = settings.getAsSettings("account");
+ accounts = new HashMap<>();
+ for (String name : accountsSettings.names()) {
+ Account.Config config = new Account.Config(name, accountsSettings.getAsSettings(name));
Account account = new Account(config, logger);
accounts.put(name, account);
}
- if (accounts.isEmpty()) {
- this.accounts = Collections.emptyMap();
- this.defaultAccountName = null;
- } else {
- this.accounts = accounts;
- String defaultAccountName = settings.get("default_account");
- if (defaultAccountName == null) {
- Account account = accounts.values().iterator().next();
- logger.error("default account set to [{}]", account.name());
- this.defaultAccountName = account.name();
- } else if (!accounts.containsKey(defaultAccountName)) {
- Account account = accounts.values().iterator().next();
- this.defaultAccountName = account.name();
- logger.error("could not find configured default account [{}]. falling back on account [{}]", defaultAccountName, account.name());
+ String defaultAccountName = settings.get("default_account");
+ if (defaultAccountName == null) {
+ if (accounts.isEmpty()) {
+ this.defaultAccountName = null;
} else {
- this.defaultAccountName = defaultAccountName;
+ Account account = accounts.values().iterator().next();
+ logger.info("default account set to [{}]", account.name());
+ this.defaultAccountName = account.name();
}
+ } else if (!accounts.containsKey(defaultAccountName)) {
+ throw new EmailSettingsException("could not fine default account [" + defaultAccountName + "]");
+ } else {
+ this.defaultAccountName = defaultAccountName;
}
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Attachment.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Attachment.java
index 76c7a81fdab..a8ebdeb109c 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Attachment.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Attachment.java
@@ -6,7 +6,6 @@
package org.elasticsearch.alerts.actions.email.service;
import org.elasticsearch.alerts.actions.email.service.support.BodyPartSource;
-import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
@@ -26,16 +25,8 @@ import java.nio.file.Path;
*/
public abstract class Attachment extends BodyPartSource {
- public Attachment(String id) {
- super(id);
- }
-
- public Attachment(String id, String name) {
- super(id, name);
- }
-
- public Attachment(String id, String name, String description) {
- super(id, name, description);
+ protected Attachment(String id, String name, String contentType) {
+ super(id, name, contentType);
}
@Override
@@ -43,12 +34,26 @@ public abstract class Attachment extends BodyPartSource {
MimeBodyPart part = new MimeBodyPart();
part.setContentID(id);
part.setFileName(name);
- part.setDescription(description, Charsets.UTF_8.name());
part.setDisposition(Part.ATTACHMENT);
writeTo(part);
return part;
}
+ public abstract String type();
+
+ /**
+ * intentionally not emitting path as it may come as an information leak
+ */
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ return builder.startObject()
+ .field("type", type())
+ .field("id", id)
+ .field("name", name)
+ .field("content_type", contentType)
+ .endObject();
+ }
+
protected abstract void writeTo(MimeBodyPart part) throws MessagingException;
public static class File extends Attachment {
@@ -57,7 +62,6 @@ public abstract class Attachment extends BodyPartSource {
private final Path path;
private final DataSource dataSource;
- private final String contentType;
public File(String id, Path path) {
this(id, path.getFileName().toString(), path);
@@ -68,21 +72,13 @@ public abstract class Attachment extends BodyPartSource {
}
public File(String id, String name, Path path) {
- this(id, name, name, path);
+ this(id, name, path, fileTypeMap.getContentType(path.toFile()));
}
+
public File(String id, String name, Path path, String contentType) {
- this(id, name, name, path, contentType);
- }
-
- public File(String id, String name, String description, Path path) {
- this(id, name, description, path, null);
- }
-
- public File(String id, String name, String description, Path path, String contentType) {
- super(id, name, description);
+ super(id, name, contentType);
this.path = path;
this.dataSource = new FileDataSource(path.toFile());
- this.contentType = contentType;
}
public Path path() {
@@ -93,31 +89,9 @@ public abstract class Attachment extends BodyPartSource {
return TYPE;
}
- String contentType() {
- return contentType != null ? contentType : dataSource.getContentType();
- }
-
@Override
public void writeTo(MimeBodyPart part) throws MessagingException {
- DataSource dataSource = new FileDataSource(path.toFile());
- DataHandler handler = contentType != null ?
- new DataHandler(dataSource, contentType) :
- new DataHandler(dataSource);
- part.setDataHandler(handler);
- }
-
- /**
- * intentionally not emitting path as it may come as an information leak
- */
- @Override
- public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- return builder.startObject()
- .field("type", type())
- .field("id", id)
- .field("name", name)
- .field("description", description)
- .field("content_type", contentType())
- .endObject();
+ part.setDataHandler(new DataHandler(dataSource));
}
}
@@ -126,20 +100,18 @@ public abstract class Attachment extends BodyPartSource {
static final String TYPE = "bytes";
private final byte[] bytes;
- private final String contentType;
public Bytes(String id, byte[] bytes, String contentType) {
this(id, id, bytes, contentType);
}
- public Bytes(String id, String name, byte[] bytes, String contentType) {
- this(id, name, name, bytes, contentType);
+ public Bytes(String id, String name, byte[] bytes) {
+ this(id, name, bytes, fileTypeMap.getContentType(name));
}
- public Bytes(String id, String name, String description, byte[] bytes, String contentType) {
- super(id, name, description);
+ public Bytes(String id, String name, byte[] bytes, String contentType) {
+ super(id, name, contentType);
this.bytes = bytes;
- this.contentType = contentType;
}
public String type() {
@@ -150,27 +122,12 @@ public abstract class Attachment extends BodyPartSource {
return bytes;
}
- public String contentType() {
- return contentType;
- }
-
@Override
public void writeTo(MimeBodyPart part) throws MessagingException {
DataSource dataSource = new ByteArrayDataSource(bytes, contentType);
DataHandler handler = new DataHandler(dataSource);
part.setDataHandler(handler);
}
-
- @Override
- public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- return builder.startObject()
- .field("type", type())
- .field("id", id)
- .field("name", name)
- .field("description", description)
- .field("content_type", contentType)
- .endObject();
- }
}
public static class XContent extends Bytes {
@@ -180,11 +137,7 @@ public abstract class Attachment extends BodyPartSource {
}
protected XContent(String id, String name, ToXContent content, XContentType type) {
- this(id, name, name, content, type);
- }
-
- protected XContent(String id, String name, String description, ToXContent content, XContentType type) {
- super(id, name, description, bytes(name, content, type), mimeType(type));
+ super(id, name, bytes(name, content, type), mimeType(type));
}
static String mimeType(XContentType type) {
@@ -202,7 +155,7 @@ public abstract class Attachment extends BodyPartSource {
try {
XContentBuilder builder = XContentBuilder.builder(type.xContent());
content.toXContent(builder, ToXContent.EMPTY_PARAMS);
- return builder.bytes().array();
+ return builder.bytes().toBytes();
} catch (IOException ioe) {
throw new EmailException("could not create an xcontent attachment [" + name + "]", ioe);
}
@@ -218,10 +171,6 @@ public abstract class Attachment extends BodyPartSource {
super(id, name, content, XContentType.YAML);
}
- public Yaml(String id, String name, String description, ToXContent content) {
- super(id, name, description, content, XContentType.YAML);
- }
-
@Override
public String type() {
return "yaml";
@@ -238,10 +187,6 @@ public abstract class Attachment extends BodyPartSource {
super(id, name, content, XContentType.JSON);
}
- public Json(String id, String name, String description, ToXContent content) {
- super(id, name, description, content, XContentType.JSON);
- }
-
@Override
public String type() {
return "json";
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Authentication.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Authentication.java
index f9c483e653a..7541f870cd5 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Authentication.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Authentication.java
@@ -10,19 +10,39 @@ package org.elasticsearch.alerts.actions.email.service;
*/
public class Authentication {
- private final String username;
+ private final String user;
private final String password;
- public Authentication(String username, String password) {
- this.username = username;
+ public Authentication(String user, String password) {
+ this.user = user;
this.password = password;
}
- public String username() {
- return username;
+ public String user() {
+ return user;
}
public String password() {
return password;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Authentication that = (Authentication) o;
+
+ if (password != null ? !password.equals(that.password) : that.password != null) return false;
+ if (user != null ? !user.equals(that.user) : that.user != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = user != null ? user.hashCode() : 0;
+ result = 31 * result + (password != null ? password.hashCode() : 0);
+ return result;
+ }
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Email.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Email.java
index bf450c507ed..c44f89c16b1 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Email.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Email.java
@@ -20,16 +20,14 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
+import java.util.*;
/**
*
*/
public class Email implements ToXContent {
+ public static final ParseField ID_FIELD = new ParseField("id");
public static final ParseField FROM_FIELD = new ParseField("from");
public static final ParseField REPLY_TO_FIELD = new ParseField("reply_to");
public static final ParseField PRIORITY_FIELD = new ParseField("priority");
@@ -40,8 +38,6 @@ public class Email implements ToXContent {
public static final ParseField SUBJECT_FIELD = new ParseField("subject");
public static final ParseField TEXT_BODY_FIELD = new ParseField("text_body");
public static final ParseField HTML_BODY_FIELD = new ParseField("html_body");
- public static final ParseField ATTACHMENTS_FIELD = new ParseField("attachments");
- public static final ParseField INLINES_FIELD = new ParseField("inlines");
final String id;
final Address from;
@@ -65,7 +61,7 @@ public class Email implements ToXContent {
this.from = from;
this.replyTo = replyTo;
this.priority = priority;
- this.sentDate = sentDate;
+ this.sentDate = sentDate != null ? sentDate : new DateTime();
this.to = to;
this.cc = cc;
this.bcc = bcc;
@@ -76,6 +72,10 @@ public class Email implements ToXContent {
this.inlines = inlines;
}
+ public String id() {
+ return id;
+ }
+
public Address from() {
return from;
}
@@ -126,20 +126,46 @@ public class Email implements ToXContent {
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- return builder.startObject()
- .field(FROM_FIELD.getPreferredName(), from)
- .field(REPLY_TO_FIELD.getPreferredName(), (ToXContent) replyTo)
- .field(PRIORITY_FIELD.getPreferredName(), priority)
- .field(SENT_DATE_FIELD.getPreferredName(), sentDate)
- .field(TO_FIELD.getPreferredName(), (ToXContent) to)
- .field(CC_FIELD.getPreferredName(), (ToXContent) cc)
- .field(BCC_FIELD.getPreferredName(), (ToXContent) bcc)
- .field(SUBJECT_FIELD.getPreferredName(), subject)
- .field(TEXT_BODY_FIELD.getPreferredName(), textBody)
- .field(HTML_BODY_FIELD.getPreferredName(), htmlBody)
- .field(ATTACHMENTS_FIELD.getPreferredName(), attachments)
- .field(INLINES_FIELD.getPreferredName(), inlines)
- .endObject();
+ builder.startObject();
+ builder.field(ID_FIELD.getPreferredName(), id);
+ builder.field(FROM_FIELD.getPreferredName(), from);
+ if (replyTo != null) {
+ builder.field(REPLY_TO_FIELD.getPreferredName(), (ToXContent) replyTo);
+ }
+ if (priority != null) {
+ builder.field(PRIORITY_FIELD.getPreferredName(), priority);
+ }
+ builder.field(SENT_DATE_FIELD.getPreferredName(), sentDate);
+ builder.field(TO_FIELD.getPreferredName(), (ToXContent) to);
+ if (cc != null) {
+ builder.field(CC_FIELD.getPreferredName(), (ToXContent) cc);
+ }
+ if (bcc != null) {
+ builder.field(BCC_FIELD.getPreferredName(), (ToXContent) bcc);
+ }
+ builder.field(SUBJECT_FIELD.getPreferredName(), subject);
+ builder.field(TEXT_BODY_FIELD.getPreferredName(), textBody);
+ if (htmlBody != null) {
+ builder.field(HTML_BODY_FIELD.getPreferredName(), htmlBody);
+ }
+ return builder.endObject();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Email email = (Email) o;
+
+ if (!id.equals(email.id)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
}
public static Builder builder() {
@@ -154,7 +180,9 @@ public class Email implements ToXContent {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if ((token.isValue() || token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) && currentFieldName != null) {
- if (FROM_FIELD.match(currentFieldName)) {
+ if (ID_FIELD.match(currentFieldName)) {
+ email.id(parser.text());
+ } else if (FROM_FIELD.match(currentFieldName)) {
email.from(Address.parse(currentFieldName, token, parser));
} else if (REPLY_TO_FIELD.match(currentFieldName)) {
email.replyTo(AddressList.parse(currentFieldName, token, parser));
@@ -174,10 +202,6 @@ public class Email implements ToXContent {
email.textBody(parser.text());
} else if (HTML_BODY_FIELD.match(currentFieldName)) {
email.htmlBody(parser.text());
- } else if (ATTACHMENTS_FIELD.match(currentFieldName)) {
- //@TODO handle this
- } else if (INLINES_FIELD.match(currentFieldName)) {
- //@TODO handle this
} else {
throw new EmailException("could not parse email. unrecognized field [" + currentFieldName + "]");
}
@@ -293,7 +317,6 @@ public class Email implements ToXContent {
public Email build() {
assert id != null : "email id should not be null (should be set to the alert id";
- assert to != null && !to.isEmpty() : "email must have a [to] recipient";
return new Email(id, from, replyTo, priority, sentDate, to, cc, bcc, subject, textBody, htmlBody, attachments.build(), inlines.build());
}
@@ -427,6 +450,8 @@ public class Email implements ToXContent {
public static class AddressList implements Iterable, ToXContent {
+ public static final AddressList EMPTY = new AddressList(Collections.emptyList());
+
private final List addresses;
public AddressList(List addresses) {
@@ -446,6 +471,10 @@ public class Email implements ToXContent {
return addresses.toArray(new Address[addresses.size()]);
}
+ public int size() {
+ return addresses.size();
+ }
+
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startArray();
@@ -499,6 +528,23 @@ public class Email implements ToXContent {
throw new EmailException("could not parse [" + field + "] as address list. field must either be a string " +
"(comma-separated list of RFC822 encoded addresses) or an array of objects representing addresses");
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AddressList addresses1 = (AddressList) o;
+
+ if (!addresses.equals(addresses1.addresses)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return addresses.hashCode();
+ }
}
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/EmailService.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/EmailService.java
index 7cee9e3993d..0dbef98ce92 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/EmailService.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/EmailService.java
@@ -5,17 +5,11 @@
*/
package org.elasticsearch.alerts.actions.email.service;
-import org.elasticsearch.cluster.ClusterState;
-
/**
*
*/
public interface EmailService {
- void start(ClusterState state);
-
- void stop();
-
EmailSent send(Email email, Authentication auth, Profile profile);
EmailSent send(Email email, Authentication auth, Profile profile, String accountName);
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Inline.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Inline.java
index 13084a95170..8a925f20634 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Inline.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Inline.java
@@ -6,6 +6,9 @@
package org.elasticsearch.alerts.actions.email.service;
import org.elasticsearch.alerts.actions.email.service.support.BodyPartSource;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.inject.Provider;
import org.elasticsearch.common.xcontent.XContentBuilder;
import javax.activation.DataHandler;
@@ -14,7 +17,10 @@ import javax.activation.FileDataSource;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.internet.MimeBodyPart;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.nio.file.Path;
/**
@@ -22,26 +28,35 @@ import java.nio.file.Path;
*/
public abstract class Inline extends BodyPartSource {
- public Inline(String id) {
- super(id);
+ protected Inline(String id, String name, String contentType) {
+ super(id, name, contentType);
}
- public Inline(String id, String name) {
- super(id, name);
- }
-
- public Inline(String id, String name, String description) {
- super(id, name, description);
- }
+ public abstract String type();
@Override
public final MimeBodyPart bodyPart() throws MessagingException {
MimeBodyPart part = new MimeBodyPart();
part.setDisposition(Part.INLINE);
+ part.setContentID(id);
+ part.setFileName(name);
writeTo(part);
return part;
}
+ /**
+ * intentionally not emitting path as it may come as an information leak
+ */
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ return builder.startObject()
+ .field("type", type())
+ .field("id", id)
+ .field("name", name)
+ .field("content_type", contentType)
+ .endObject();
+ }
+
protected abstract void writeTo(MimeBodyPart part) throws MessagingException;
public static class File extends Inline {
@@ -50,32 +65,19 @@ public abstract class Inline extends BodyPartSource {
private final Path path;
private DataSource dataSource;
- private final String contentType;
public File(String id, Path path) {
this(id, path.getFileName().toString(), path);
}
- public File(String id, Path path, String contentType) {
- this(id, path.getFileName().toString(), path, contentType);
- }
-
public File(String id, String name, Path path) {
- this(id, name, name, path);
+ this(id, name, path, fileTypeMap.getContentType(path.toFile()));
}
+
public File(String id, String name, Path path, String contentType) {
- this(id, name, name, path, contentType);
- }
-
- public File(String id, String name, String description, Path path) {
- this(id, name, description, path, null);
- }
-
- public File(String id, String name, String description, Path path, String contentType) {
- super(id, name, description);
+ super(id, name, contentType);
this.path = path;
this.dataSource = new FileDataSource(path.toFile());
- this.contentType = contentType;
}
public Path path() {
@@ -86,30 +88,99 @@ public abstract class Inline extends BodyPartSource {
return TYPE;
}
- String contentType() {
- return contentType != null ? contentType : dataSource.getContentType();
- }
-
@Override
public void writeTo(MimeBodyPart part) throws MessagingException {
- DataHandler handler = contentType != null ?
- new DataHandler(dataSource, contentType) :
- new DataHandler(dataSource);
- part.setDataHandler(handler);
+ part.setDataHandler(new DataHandler(dataSource, contentType));
+ }
+ }
+
+ public static class Stream extends Inline {
+
+ static final String TYPE = "stream";
+
+ private final Provider source;
+
+ public Stream(String id, String name, Provider source) {
+ this(id, name, fileTypeMap.getContentType(name), source);
+ }
+
+ public Stream(String id, String name, String contentType, Provider source) {
+ super(id, name, contentType);
+ this.source = source;
}
- /**
- * intentionally not emitting path as it may come as an information leak
- */
@Override
- public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- return builder.startObject()
- .field("type", type())
- .field("id", id)
- .field("name", name)
- .field("description", description)
- .field("content_type", contentType())
- .endObject();
+ public String type() {
+ return TYPE;
+ }
+
+ @Override
+ protected void writeTo(MimeBodyPart part) throws MessagingException {
+ DataSource ds = new StreamDataSource(name, contentType, source);
+ DataHandler dh = new DataHandler(ds);
+ part.setDataHandler(dh);
+ }
+
+ static class StreamDataSource implements DataSource {
+
+ private final String name;
+ private final String contentType;
+ private final Provider source;
+
+ public StreamDataSource(String name, String contentType, Provider source) {
+ this.name = name;
+ this.contentType = contentType;
+ this.source = source;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return source.get();
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+ }
+ }
+
+ public static class Bytes extends Stream {
+
+ public Bytes(String id, String name, String contentType, byte[] bytes) {
+ super(id, name, contentType, new BytesStreamProvider(bytes));
+ }
+
+ public Bytes(String id, String name, String contentType, BytesReference bytes) {
+ super(id, name, contentType, new BytesStreamProvider(bytes));
+ }
+
+ static class BytesStreamProvider implements Provider {
+
+ private final BytesReference bytes;
+
+ public BytesStreamProvider(byte[] bytes) {
+ this(new BytesArray(bytes));
+ }
+
+ public BytesStreamProvider(BytesReference bytes) {
+ this.bytes = bytes;
+ }
+
+ @Override
+ public InputStream get() {
+ return new ByteArrayInputStream(bytes.array(), bytes.arrayOffset(), bytes.length());
+ }
}
}
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/InternalEmailService.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/InternalEmailService.java
index e53eec8398a..1f28ecac190 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/InternalEmailService.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/InternalEmailService.java
@@ -5,25 +5,23 @@
*/
package org.elasticsearch.alerts.actions.email.service;
-import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.common.component.AbstractComponent;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.node.settings.NodeSettingsService;
import javax.mail.MessagingException;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
*
*/
-public class InternalEmailService extends AbstractComponent implements EmailService {
+public class InternalEmailService extends AbstractLifecycleComponent implements EmailService {
private volatile Accounts accounts;
- private final AtomicBoolean started = new AtomicBoolean(false);
-
@Inject
public InternalEmailService(Settings settings, NodeSettingsService nodeSettingsService) {
super(settings);
@@ -35,6 +33,19 @@ public class InternalEmailService extends AbstractComponent implements EmailServ
});
}
+ @Override
+ protected void doStart() throws ElasticsearchException {
+ reset(settings);
+ }
+
+ @Override
+ protected void doStop() throws ElasticsearchException {
+ }
+
+ @Override
+ protected void doClose() throws ElasticsearchException {
+ }
+
@Override
public EmailSent send(Email email, Authentication auth, Profile profile) {
return send(email, auth, profile, (String) null);
@@ -59,29 +70,16 @@ public class InternalEmailService extends AbstractComponent implements EmailServ
return new EmailSent(account.name(), email);
}
- @Override
- public synchronized void start(ClusterState state) {
- if (started.get()) {
- return;
- }
- reset(state.metaData().settings());
- started.set(true);
- }
-
- @Override
- public synchronized void stop() {
- started.set(false);
- }
-
- synchronized void reset(Settings nodeSettings) {
- if (!started.get()) {
- return;
- }
+ void reset(Settings nodeSettings) {
Settings settings = ImmutableSettings.builder()
.put(componentSettings)
.put(nodeSettings.getComponentSettings(InternalEmailService.class))
.build();
- accounts = new Accounts(settings, logger);
+ accounts = createAccounts(settings, logger);
+ }
+
+ protected Accounts createAccounts(Settings settings, ESLogger logger) {
+ return new Accounts(settings, logger);
}
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/Profile.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/Profile.java
index 8fdf4ceb22e..547708a10df 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/Profile.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/Profile.java
@@ -16,7 +16,6 @@ import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.io.IOException;
-import java.util.Date;
import java.util.Locale;
/**
@@ -26,11 +25,51 @@ import java.util.Locale;
public enum Profile implements ToXContent {
STANDARD() {
+
+ @Override
+ public String textBody(MimeMessage msg) throws IOException, MessagingException {
+ MimeMultipart mixed = (MimeMultipart) msg.getContent();
+ MimeMultipart related = null;
+ for (int i = 0; i < mixed.getCount(); i++) {
+ MimeBodyPart part = (MimeBodyPart) mixed.getBodyPart(i);
+
+ if (part.getContentType().startsWith("multipart/related")) {
+ related = (MimeMultipart) part.getContent();
+ break;
+ }
+ }
+ if (related == null) {
+ throw new EmailException("could not extract body text from mime message");
+ }
+
+ MimeMultipart alternative = null;
+ for (int i = 0; i < related.getCount(); i++) {
+ MimeBodyPart part = (MimeBodyPart) related.getBodyPart(i);
+ if (part.getContentType().startsWith("multipart/alternative")) {
+ alternative = (MimeMultipart) part.getContent();
+ break;
+ }
+ }
+ if (alternative == null) {
+ throw new EmailException("could not extract body text from mime message");
+ }
+
+ for (int i = 0; i < alternative.getCount(); i++) {
+ MimeBodyPart part = (MimeBodyPart) alternative.getBodyPart(i);
+ if (part.getContentType().startsWith("text/plain")) {
+ return (String) part.getContent();
+ }
+ }
+
+ throw new EmailException("could not extract body text from mime message");
+ }
+
@Override
public MimeMessage toMimeMessage(Email email, Session session) throws MessagingException {
MimeMessage message = createCommon(email, session);
MimeMultipart mixed = new MimeMultipart("mixed");
+ message.setContent(mixed);
MimeMultipart related = new MimeMultipart("related");
mixed.addBodyPart(wrap(related, null));
@@ -39,12 +78,16 @@ public enum Profile implements ToXContent {
related.addBodyPart(wrap(alternative, "text/alternative"));
MimeBodyPart text = new MimeBodyPart();
- text.setText(email.textBody, Charsets.UTF_8.name());
+ if (email.textBody != null) {
+ text.setText(email.textBody, Charsets.UTF_8.name());
+ } else {
+ text.setText("", Charsets.UTF_8.name());
+ }
alternative.addBodyPart(text);
if (email.htmlBody != null) {
MimeBodyPart html = new MimeBodyPart();
- text.setText(email.textBody, Charsets.UTF_8.name(), "html");
+ html.setText(email.htmlBody, Charsets.UTF_8.name(), "html");
alternative.addBodyPart(html);
}
@@ -65,18 +108,36 @@ public enum Profile implements ToXContent {
},
OUTLOOK() {
+
+ @Override
+ public String textBody(MimeMessage msg) throws IOException, MessagingException {
+ return STANDARD.textBody(msg);
+ }
+
@Override
public MimeMessage toMimeMessage(Email email, Session session) throws MessagingException {
return STANDARD.toMimeMessage(email, session);
}
},
GMAIL() {
+
+ @Override
+ public String textBody(MimeMessage msg) throws IOException, MessagingException {
+ return STANDARD.textBody(msg);
+ }
+
@Override
public MimeMessage toMimeMessage(Email email, Session session) throws MessagingException {
return STANDARD.toMimeMessage(email, session);
}
},
MAC() {
+
+ @Override
+ public String textBody(MimeMessage msg) throws IOException, MessagingException {
+ return STANDARD.textBody(msg);
+ }
+
@Override
public MimeMessage toMimeMessage(Email email, Session session) throws MessagingException {
return STANDARD.toMimeMessage(email, session);
@@ -87,6 +148,8 @@ public enum Profile implements ToXContent {
public abstract MimeMessage toMimeMessage(Email email, Session session) throws MessagingException ;
+ public abstract String textBody(MimeMessage msg) throws IOException, MessagingException;
+
public static Profile resolve(String name) {
Profile profile = resolve(name, null);
if (profile == null) {
@@ -104,6 +167,7 @@ public enum Profile implements ToXContent {
case "standard": return STANDARD;
case "outlook": return OUTLOOK;
case "gmail": return GMAIL;
+ case "mac": return MAC;
default:
return defaultProfile;
}
@@ -126,14 +190,19 @@ public enum Profile implements ToXContent {
if (email.priority != null) {
email.priority.applyTo(message);
}
- Date sentDate = email.sentDate != null ? email.sentDate.toDate() : new Date();
- message.setSentDate(sentDate);
-
+ message.setSentDate(email.sentDate.toDate());
message.setRecipients(Message.RecipientType.TO, email.to.toArray());
- message.setRecipients(Message.RecipientType.CC, email.cc.toArray());
- message.setRecipients(Message.RecipientType.BCC, email.bcc.toArray());
-
- message.setSubject(email.subject, Charsets.UTF_8.name());
+ if (email.cc != null) {
+ message.setRecipients(Message.RecipientType.CC, email.cc.toArray());
+ }
+ if (email.bcc != null) {
+ message.setRecipients(Message.RecipientType.BCC, email.bcc.toArray());
+ }
+ if (email.subject != null) {
+ message.setSubject(email.subject, Charsets.UTF_8.name());
+ } else {
+ message.setSubject("", Charsets.UTF_8.name());
+ }
return message;
}
diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/service/support/BodyPartSource.java b/src/main/java/org/elasticsearch/alerts/actions/email/service/support/BodyPartSource.java
index 3cfa1548bcb..f5339d49258 100644
--- a/src/main/java/org/elasticsearch/alerts/actions/email/service/support/BodyPartSource.java
+++ b/src/main/java/org/elasticsearch/alerts/actions/email/service/support/BodyPartSource.java
@@ -7,6 +7,7 @@ package org.elasticsearch.alerts.actions.email.service.support;
import org.elasticsearch.common.xcontent.ToXContent;
+import javax.activation.FileTypeMap;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
@@ -15,22 +16,20 @@ import javax.mail.internet.MimeBodyPart;
*/
public abstract class BodyPartSource implements ToXContent {
+ protected static FileTypeMap fileTypeMap = FileTypeMap.getDefaultFileTypeMap();
+
protected final String id;
protected final String name;
- protected final String description;
+ protected final String contentType;
- public BodyPartSource(String id) {
- this(id, id);
+ public BodyPartSource(String id, String contentType) {
+ this(id, id, contentType);
}
- public BodyPartSource(String id, String name) {
- this(id, name, name);
- }
-
- public BodyPartSource(String id, String name, String description) {
+ public BodyPartSource(String id, String name, String contentType) {
this.id = id;
this.name = name;
- this.description = description;
+ this.contentType = contentType;
}
public String id() {
@@ -41,8 +40,8 @@ public abstract class BodyPartSource implements ToXContent {
return name;
}
- public String description() {
- return description;
+ public String contentType() {
+ return contentType;
}
public abstract MimeBodyPart bodyPart() throws MessagingException;
diff --git a/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java b/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java
index 029bdc1a81f..9d59fc6824a 100644
--- a/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java
+++ b/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java
@@ -50,6 +50,10 @@ public class ScriptTemplate implements ToXContent, Template {
this(service, text, DEFAULT_LANG, ScriptService.ScriptType.INLINE, Collections.emptyMap());
}
+ public ScriptTemplate(ScriptServiceProxy service, String text, String lang, ScriptService.ScriptType type) {
+ this(service, text, lang, type, Collections.emptyMap());
+ }
+
public ScriptTemplate(ScriptServiceProxy service, String text, String lang, ScriptService.ScriptType type, Map params) {
this.service = service;
this.text = text;
@@ -135,8 +139,12 @@ public class ScriptTemplate implements ToXContent, Template {
@Override
public ScriptTemplate parse(XContentParser parser) throws IOException {
- assert parser.currentToken() == XContentParser.Token.START_OBJECT : "Expected START_OBJECT, but was " + parser.currentToken();
-
+ if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
+ return new ScriptTemplate(scriptService, parser.text());
+ }
+ if (parser.currentToken() != XContentParser.Token.START_OBJECT) {
+ throw new ParseException("expected either a string or an object, but found [" + parser.currentToken() + "] instead");
+ }
String text = null;
ScriptService.ScriptType type = ScriptService.ScriptType.INLINE;
String lang = DEFAULT_LANG;
diff --git a/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java b/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java
index 6315fed66ac..47f1f2d49a6 100644
--- a/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java
+++ b/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java
@@ -55,7 +55,7 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.*;
-import static org.elasticsearch.common.xcontent.XContentFactory.*;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
import static org.hamcrest.Matchers.*;
@@ -174,12 +174,12 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
Email.AddressList to = new Email.AddressList(emailAddressList);
- Email.Builder emailBuilder = Email.builder();
+ Email.Builder emailBuilder = Email.builder().id("prototype");
emailBuilder.from(from);
emailBuilder.to(to);
- EmailAction emailAction = new EmailAction(logger, noopEmailService(), emailBuilder,
+ EmailAction emailAction = new EmailAction(logger, noopEmailService(), emailBuilder.build(),
new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", body, body, null, true);
actions.add(emailAction);
@@ -471,19 +471,10 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest
}
private static class NoopEmailService implements EmailService {
- @Override
- public void start(ClusterState state) {
-
- }
-
- @Override
- public void stop() {
-
- }
@Override
public EmailSent send(Email email, Authentication auth, Profile profile) {
- return new EmailSent(auth.username(), email);
+ return new EmailSent(auth.user(), email);
}
@Override
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java b/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java
deleted file mode 100644
index 38d29972222..00000000000
--- a/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-package org.elasticsearch.alerts.actions.email;
-
-import org.elasticsearch.alerts.Alert;
-import org.elasticsearch.alerts.ExecutionContext;
-import org.elasticsearch.alerts.Payload;
-import org.elasticsearch.alerts.actions.email.service.Authentication;
-import org.elasticsearch.alerts.actions.email.service.Email;
-import org.elasticsearch.alerts.actions.email.service.EmailService;
-import org.elasticsearch.alerts.actions.email.service.Profile;
-import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
-import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
-import org.elasticsearch.alerts.support.template.ScriptTemplate;
-import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.common.joda.time.DateTime;
-import org.elasticsearch.common.settings.ImmutableSettings;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.env.Environment;
-import org.elasticsearch.script.ScriptEngineService;
-import org.elasticsearch.script.ScriptService;
-import org.elasticsearch.script.mustache.MustacheScriptEngineService;
-import org.elasticsearch.test.ElasticsearchTestCase;
-import org.elasticsearch.threadpool.ThreadPool;
-import org.elasticsearch.watcher.ResourceWatcherService;
-
-import javax.mail.MessagingException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
-*/
-public class EmailActionTest extends ElasticsearchTestCase {
-
- public void testEmailTemplateRender() throws IOException, MessagingException {
-
- Settings settings = ImmutableSettings.settingsBuilder().build();
- MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings);
- ThreadPool threadPool = new ThreadPool(ThreadPool.Names.SAME);
- Set engineServiceSet = new HashSet<>();
- engineServiceSet.add(mustacheScriptEngineService);
-
- ScriptServiceProxy scriptService = ScriptServiceProxy.of(new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, threadPool)));
-
- ScriptTemplate template = new ScriptTemplate(scriptService, "{{alert_name}} executed with {{response.hits.total}} hits");
-
- EmailService emailService = new EmailServiceMock();
-
- Email.Address from = new Email.Address("from@test.com");
- List emailAddressList = new ArrayList<>();
- emailAddressList.add(new Email.Address("to@test.com"));
- Email.AddressList to = new Email.AddressList(emailAddressList);
-
-
- Email.Builder emailBuilder = Email.builder();
- emailBuilder.from(from);
- emailBuilder.to(to);
-
- EmailAction emailAction = new EmailAction(logger, emailService, emailBuilder,
- new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", template, template, null, true);
-
- //This is ok since the execution of the action only relies on the alert name
- Alert alert = new Alert(
- "test-serialization",
- new CronSchedule("0/5 * * * * ? *"),
- null,
- null,
- new TimeValue(0),
- null,
- null,
- new Alert.Status()
- );
- ExecutionContext ctx = new ExecutionContext("test-serialization#1", alert, new DateTime(), new DateTime());
- EmailAction.Result result = emailAction.execute(ctx, new Payload.Simple());
-
- threadPool.shutdownNow();
-
- assertTrue(result.success());
-
- EmailAction.Result.Success success = (EmailAction.Result.Success) result;
- assertEquals(success.account(), "testaccount");
- assertArrayEquals(success.email().to().toArray(), to.toArray() );
- assertEquals(success.email().from(), from);
- //@TODO add more here
- }
-
- static class EmailServiceMock implements EmailService {
- @Override
- public void start(ClusterState state) {
-
- }
-
- @Override
- public void stop() {
-
- }
-
- @Override
- public EmailSent send(Email email, Authentication auth, Profile profile) {
- return new EmailSent(auth.username(), email);
- }
-
- @Override
- public EmailSent send(Email email, Authentication auth, Profile profile, String accountName) {
- return new EmailSent(accountName, email);
- }
- }
-
-}
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTests.java b/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTests.java
new file mode 100644
index 00000000000..90f2f49f51e
--- /dev/null
+++ b/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTests.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.alerts.actions.email;
+
+import com.carrotsearch.randomizedtesting.annotations.Repeat;
+import org.elasticsearch.alerts.Alert;
+import org.elasticsearch.alerts.ExecutionContext;
+import org.elasticsearch.alerts.Payload;
+import org.elasticsearch.alerts.actions.ActionSettingsException;
+import org.elasticsearch.alerts.actions.email.service.*;
+import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy;
+import org.elasticsearch.alerts.support.template.ScriptTemplate;
+import org.elasticsearch.alerts.support.template.Template;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.collect.ImmutableMap;
+import org.elasticsearch.common.joda.time.DateTime;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ *
+ */
+public class EmailActionTests extends ElasticsearchTestCase {
+
+ @Test @Repeat(iterations = 20)
+ public void testExecute() throws Exception {
+ final String account = "account1";
+ EmailService service = new EmailService() {
+ @Override
+ public EmailSent send(Email email, Authentication auth, Profile profile) {
+ return new EmailSent(account, email);
+ }
+
+ @Override
+ public EmailSent send(Email email, Authentication auth, Profile profile, String accountName) {
+ return new EmailSent(account, email);
+ }
+ };
+ Email email = Email.builder().id("prototype").build();
+ Authentication auth = new Authentication("user", "passwd");
+ Profile profile = randomFrom(Profile.values());
+
+ Template subject = mock(Template.class);
+ Template textBody = mock(Template.class);
+ Template htmlBody = randomBoolean() ? null : mock(Template.class);
+ boolean attachPayload = randomBoolean();
+ EmailAction action = new EmailAction(logger, service, email, auth, profile, account, subject, textBody, htmlBody, attachPayload);
+
+ final Map data = new HashMap<>();
+ Payload payload = new Payload() {
+ @Override
+ public Map data() {
+ return data;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ return builder.map(data);
+ }
+ };
+
+ String ctxId = randomAsciiOfLength(5);
+ ExecutionContext ctx = mock(ExecutionContext.class);
+ when(ctx.id()).thenReturn(ctxId);
+ Alert alert = mock(Alert.class);
+ when(alert.name()).thenReturn("alert1");
+ when(ctx.alert()).thenReturn(alert);
+
+ Map expectedModel = ImmutableMap.builder()
+ .put("alert_name", "alert1")
+ .put("payload", data)
+ .build();
+
+ when(subject.render(expectedModel)).thenReturn("_subject");
+ when(textBody.render(expectedModel)).thenReturn("_text_body");
+ if (htmlBody != null) {
+ when (htmlBody.render(expectedModel)).thenReturn("_html_body");
+ }
+
+ EmailAction.Result result = action.execute(ctx, payload);
+
+ assertThat(result, notNullValue());
+ assertThat(result, instanceOf(EmailAction.Result.Success.class));
+ assertThat(((EmailAction.Result.Success) result).account(), equalTo(account));
+ Email actualEmail = ((EmailAction.Result.Success) result).email();
+ assertThat(actualEmail.id(), is(ctxId));
+ assertThat(actualEmail, notNullValue());
+ assertThat(actualEmail.subject(), is("_subject"));
+ assertThat(actualEmail.textBody(), is("_text_body"));
+ if (htmlBody != null) {
+ assertThat(actualEmail.htmlBody(), is("_html_body"));
+ }
+ if (attachPayload) {
+ assertThat(actualEmail.attachments(), hasKey("payload"));
+ }
+ }
+
+ @Test @Repeat(iterations = 20)
+ public void testParser() throws Exception {
+ ScriptServiceProxy scriptService = mock(ScriptServiceProxy.class);
+ EmailService emailService = mock(EmailService.class);
+ Profile profile = randomFrom(Profile.values());
+ Email.Priority priority = randomFrom(Email.Priority.values());
+ Email.Address[] to = rarely() ? null : Email.AddressList.parse(randomBoolean() ? "to@domain" : "to1@domain,to2@domain").toArray();
+ Email.Address[] cc = rarely() ? null : Email.AddressList.parse(randomBoolean() ? "cc@domain" : "cc1@domain,cc2@domain").toArray();
+ Email.Address[] bcc = rarely() ? null : Email.AddressList.parse(randomBoolean() ? "bcc@domain" : "bcc1@domain,bcc2@domain").toArray();
+ Email.Address[] replyTo = rarely() ? null : Email.AddressList.parse(randomBoolean() ? "reply@domain" : "reply1@domain,reply2@domain").toArray();
+ ScriptTemplate subject = randomBoolean() ? new ScriptTemplate(scriptService, "_subject") : null;
+ ScriptTemplate textBody = randomBoolean() ? new ScriptTemplate(scriptService, "_text_body") : null;
+ ScriptTemplate htmlBody = randomBoolean() ? new ScriptTemplate(scriptService, "_text_html") : null;
+ boolean attachPayload = randomBoolean();
+ XContentBuilder builder = jsonBuilder().startObject()
+ .field("account", "_account")
+ .field("profile", profile.name())
+ .field("user", "_user")
+ .field("password", "_passwd")
+ .field("attach_payload", attachPayload)
+ .field("from", "from@domain")
+ .field("priority", priority.name());
+ if (to != null) {
+ if (to.length == 1) {
+ builder.field("to", to[0]);
+ } else {
+ builder.array("to", (Object[]) to);
+ }
+ }
+ if (cc != null) {
+ if (cc.length == 1) {
+ builder.field("cc", cc[0]);
+ } else {
+ builder.array("cc", (Object[]) cc);
+ }
+ }
+ if (bcc != null) {
+ if (bcc.length == 1) {
+ builder.field("bcc", bcc[0]);
+ } else {
+ builder.array("bcc", (Object[]) bcc);
+ }
+ }
+ if (replyTo != null) {
+ if (replyTo.length == 1) {
+ builder.field("reply_to", replyTo[0]);
+ } else {
+ builder.array("reply_to", (Object[]) replyTo);
+ }
+ }
+ if (subject != null) {
+ if (randomBoolean()) {
+ builder.field("subject", subject.text());
+ } else {
+ builder.field("subject", subject);
+ }
+ }
+ if (textBody != null) {
+ if (randomBoolean()) {
+ builder.field("text_body", textBody.text());
+ } else {
+ builder.field("text_body", textBody);
+ }
+ }
+ if (htmlBody != null) {
+ if (randomBoolean()) {
+ builder.field("html_body", htmlBody.text());
+ } else {
+ builder.field("html_body", htmlBody);
+ }
+ }
+ BytesReference bytes = builder.bytes();
+ XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
+ parser.nextToken();
+ EmailAction action = new EmailAction.Parser(ImmutableSettings.EMPTY, emailService,
+ new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService)).parse(parser);
+
+ assertThat(action, notNullValue());
+ assertThat(action.account, is("_account"));
+ assertThat(action.attachPayload, is(attachPayload));
+ assertThat(action.auth, notNullValue());
+ assertThat(action.auth.user(), is("_user"));
+ assertThat(action.auth.password(), is("_passwd"));
+ assertThat(action.subject, is((Template) subject));
+ assertThat(action.textBody, is((Template) textBody));
+ assertThat(action.htmlBody, is((Template) htmlBody));
+ assertThat(action.emailPrototype.priority(), is(priority));
+ if (to != null) {
+ assertThat(action.emailPrototype.to().toArray(), arrayContainingInAnyOrder(to));
+ } else {
+ assertThat(action.emailPrototype.to(), nullValue());
+ }
+ if (cc != null) {
+ assertThat(action.emailPrototype.cc().toArray(), arrayContainingInAnyOrder(cc));
+ } else {
+ assertThat(action.emailPrototype.cc(), nullValue());
+ }
+ if (bcc != null) {
+ assertThat(action.emailPrototype.bcc().toArray(), arrayContainingInAnyOrder(bcc));
+ } else {
+ assertThat(action.emailPrototype.bcc(), nullValue());
+ }
+ if (replyTo != null) {
+ assertThat(action.emailPrototype.replyTo().toArray(), arrayContainingInAnyOrder(replyTo));
+ } else {
+ assertThat(action.emailPrototype.replyTo(), nullValue());
+ }
+ }
+
+ @Test @Repeat(iterations = 20)
+ public void testParser_SelfGenerated() throws Exception {
+ EmailService service = mock(EmailService.class);
+ Email.Builder emailPrototypeBuilder = Email.builder().id("prototype");
+ if (randomBoolean()) {
+ emailPrototypeBuilder.from(new Email.Address("from@domain"));
+ }
+ if (randomBoolean()) {
+ emailPrototypeBuilder.to(Email.AddressList.parse(randomBoolean() ? "to@domain" : "to1@domain,to2@damain"));
+ }
+ if (randomBoolean()) {
+ emailPrototypeBuilder.cc(Email.AddressList.parse(randomBoolean() ? "cc@domain" : "cc1@domain,cc2@damain"));
+ }
+ if (randomBoolean()) {
+ emailPrototypeBuilder.bcc(Email.AddressList.parse(randomBoolean() ? "bcc@domain" : "bcc1@domain,bcc2@damain"));
+ }
+ if (randomBoolean()) {
+ emailPrototypeBuilder.replyTo(Email.AddressList.parse(randomBoolean() ? "reply@domain" : "reply1@domain,reply2@damain"));
+ }
+ Email email = emailPrototypeBuilder.build();
+ Authentication auth = randomBoolean() ? null : new Authentication("_user", "_passwd");
+ Profile profile = randomFrom(Profile.values());
+ String account = randomAsciiOfLength(6);
+ Template subject = new TemplateMock("_subject");
+ Template textBody = new TemplateMock("_text_body");
+ Template htmlBody = randomBoolean() ? null : new TemplateMock("_html_body");
+ boolean attachPayload = randomBoolean();
+
+ EmailAction action = new EmailAction(logger, service, email, auth, profile, account, subject, textBody, htmlBody, attachPayload);
+
+ XContentBuilder builder = jsonBuilder();
+ action.toXContent(builder, Attachment.XContent.EMPTY_PARAMS);
+ BytesReference bytes = builder.bytes();
+ XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
+ parser.nextToken();
+ EmailAction parsed = new EmailAction.Parser(ImmutableSettings.EMPTY, service, new TemplateMock.Parser()).parse(parser);
+ assertThat(parsed, equalTo(action));
+
+ }
+
+ @Test(expected = ActionSettingsException.class) @Repeat(iterations = 100)
+ public void testParser_Invalid() throws Exception {
+ ScriptServiceProxy scriptService = mock(ScriptServiceProxy.class);
+ EmailService emailService = mock(EmailService.class);
+ XContentBuilder builder = jsonBuilder().startObject()
+ .field("unknown_field", "value");
+ BytesReference bytes = builder.bytes();
+ XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
+ new EmailAction.Parser(ImmutableSettings.EMPTY, emailService,
+ new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService)).parse(parser);
+ }
+
+ @Test @Repeat(iterations = 20)
+ public void testParser_Result() throws Exception {
+ boolean success = randomBoolean();
+ Email email = Email.builder().id("_id")
+ .from(new Email.Address("from@domain"))
+ .to(Email.AddressList.parse("to@domain"))
+ .sentDate(new DateTime())
+ .subject("_subject")
+ .textBody("_text_body")
+ .build();
+ XContentBuilder builder = jsonBuilder().startObject()
+ .field("success", success);
+ if (success) {
+ builder.field("email", email);
+ builder.field("account", "_account");
+ } else {
+ builder.field("reason", "_reason");
+ }
+ builder.endObject();
+ BytesReference bytes = builder.bytes();
+ XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
+ parser.nextToken();
+ EmailAction.Result result = new EmailAction.Parser(ImmutableSettings.EMPTY, mock(EmailService.class), new TemplateMock.Parser())
+ .parseResult(parser);
+ assertThat(result.success(), is(success));
+ if (success) {
+ assertThat(result, instanceOf(EmailAction.Result.Success.class));
+ assertThat(((EmailAction.Result.Success) result).email(), equalTo(email));
+ assertThat(((EmailAction.Result.Success) result).account(), is("_account"));
+ } else {
+ assertThat(result, instanceOf(EmailAction.Result.Failure.class));
+ assertThat(((EmailAction.Result.Failure) result).reason(), is("_reason"));
+ }
+ }
+
+ @Test(expected = EmailException.class)
+ public void testParser_Result_Invalid() throws Exception {
+ XContentBuilder builder = jsonBuilder().startObject()
+ .field("unknown_field", "value")
+ .endObject();
+ BytesReference bytes = builder.bytes();
+ XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
+ parser.nextToken();
+ new EmailAction.Parser(ImmutableSettings.EMPTY, mock(EmailService.class), new TemplateMock.Parser())
+ .parseResult(parser);
+ }
+
+ static class TemplateMock implements Template {
+
+ private final String name;
+
+ public TemplateMock(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String render(Map model) {
+ return "";
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ return builder.value(name);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TemplateMock that = (TemplateMock) o;
+
+ if (!name.equals(that.name)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ public static class Parser implements Template.Parser {
+
+ @Override
+ public TemplateMock parse(XContentParser parser) throws IOException, ParseException {
+ return new TemplateMock(parser.text());
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/service/AccountTests.java b/src/test/java/org/elasticsearch/alerts/actions/email/service/AccountTests.java
new file mode 100644
index 00000000000..15fa087e4b2
--- /dev/null
+++ b/src/test/java/org/elasticsearch/alerts/actions/email/service/AccountTests.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.alerts.actions.email.service;
+
+import org.elasticsearch.alerts.actions.email.service.support.EmailServer;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.mail.Address;
+import javax.mail.Message;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ */
+public class AccountTests extends ElasticsearchTestCase {
+
+ static final String USERNAME = "_user";
+ static final String PASSWORD = "_passwd";
+
+ private EmailServer server;
+
+ @Before
+ public void init() throws Exception {
+ server = new EmailServer("localhost", 2500, USERNAME, PASSWORD);
+ server.start();
+ }
+
+ @After
+ public void cleanup() throws Exception {
+ server.stop();
+ }
+
+ @Test
+ public void testConfig() throws Exception {
+
+ ImmutableSettings.Builder builder = ImmutableSettings.builder();
+
+ Profile profile = rarely() ? Profile.STANDARD : randomFrom(Profile.values());
+ if (profile != Profile.STANDARD) {
+ builder.put("profile", profile.name());
+ }
+
+ Account.Config.EmailDefaults emailDefaults;
+ if (randomBoolean()) {
+ ImmutableSettings.Builder sb = ImmutableSettings.builder();
+ if (randomBoolean()) {
+ sb.put(Email.FROM_FIELD.getPreferredName(), "from@domain");
+ }
+ if (randomBoolean()) {
+ sb.put(Email.REPLY_TO_FIELD.getPreferredName(), "replyto@domain");
+ }
+ if (randomBoolean()) {
+ sb.put(Email.PRIORITY_FIELD.getPreferredName(), randomFrom(Email.Priority.values()));
+ }
+ if (randomBoolean()) {
+ sb.put(Email.TO_FIELD.getPreferredName(), "to@domain");
+ }
+ if (randomBoolean()) {
+ sb.put(Email.CC_FIELD.getPreferredName(), "cc@domain");
+ }
+ if (randomBoolean()) {
+ sb.put(Email.BCC_FIELD.getPreferredName(), "bcc@domain");
+ }
+ if (randomBoolean()) {
+ sb.put(Email.SUBJECT_FIELD.getPreferredName(), "_subject");
+ }
+ Settings settings = sb.build();
+ emailDefaults = new Account.Config.EmailDefaults(settings);
+ for (String name : settings.names()) {
+ builder.put("email_defaults." + name, settings.get(name));
+ }
+ } else {
+ emailDefaults = new Account.Config.EmailDefaults(ImmutableSettings.EMPTY);
+ }
+
+ Properties smtpProps = new Properties();
+ ImmutableSettings.Builder smtpBuilder = ImmutableSettings.builder();
+ String host = "somehost";
+ String setting = randomFrom("host", "localaddress", "local_address");
+ smtpBuilder.put(setting, host);
+ if (setting.equals("local_address")) {
+ // we need to remove the `_`... we only added support for `_` for readability
+ // the actual properties (java mail properties) don't contain underscores
+ setting = "localaddress";
+ }
+ smtpProps.put("mail.smtp." + setting, host);
+ String user = null;
+ if (randomBoolean()) {
+ user = randomAsciiOfLength(5);
+ setting = randomFrom("user", "from");
+ smtpBuilder.put(setting, user);
+ smtpProps.put("mail.smtp." + setting, user);
+ }
+ int port = 25;
+ if (randomBoolean()) {
+ port = randomIntBetween(2000, 2500);
+ setting = randomFrom("port", "localport", "local_port");
+ smtpBuilder.put(setting, port);
+ if (setting.equals("local_port")) {
+ setting = "localport";
+ }
+ smtpProps.setProperty("mail.smtp." + setting, String.valueOf(port));
+ }
+ String password = null;
+ if (randomBoolean()) {
+ password = randomAsciiOfLength(8);
+ smtpBuilder.put("password", password);
+ smtpProps.put("mail.smtp.password", password);
+ }
+ for (int i = 0; i < 5; i++) {
+ String name = randomAsciiOfLength(5);
+ String value = randomAsciiOfLength(6);
+ smtpProps.put("mail.smtp." + name, value);
+ smtpBuilder.put(name, value);
+ }
+
+ Settings smtpSettings = smtpBuilder.build();
+ for (String name : smtpSettings.names()) {
+ builder.put("smtp." + name, smtpSettings.get(name));
+ }
+
+ Settings settings = builder.build();
+
+ Account.Config config = new Account.Config("_name", settings);
+
+ if (profile != null) {
+ assertThat(config.profile, is(profile));
+ }
+ assertThat(config.defaults, equalTo(emailDefaults));
+ assertThat(config.smtp, notNullValue());
+ assertThat(config.smtp.port, is(port));
+ assertThat(config.smtp.host, is(host));
+ assertThat(config.smtp.user, is(user));
+ assertThat(config.smtp.password, is(password));
+ assertThat(config.smtp.properties, equalTo(smtpProps));
+ }
+
+ @Test
+ public void testSend() throws Exception {
+ Account account = new Account(new Account.Config("default", ImmutableSettings.builder()
+ .put("smtp.host", "localhost")
+ .put("smtp.port", 2500)
+ .put("smtp.user", USERNAME)
+ .put("smtp.password", PASSWORD)
+ .build()), logger);
+
+ Email email = Email.builder()
+ .id("_id")
+ .from(new Email.Address("from@domain.com"))
+ .to(Email.AddressList.parse("To"))
+ .subject("_subject")
+ .textBody("_text_body")
+ .build();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ EmailServer.Listener.Handle handle = server.addListener(new EmailServer.Listener() {
+ @Override
+ public void on(MimeMessage message) throws Exception {
+ assertThat(message.getFrom().length, is(1));
+ assertThat((InternetAddress) message.getFrom()[0], equalTo(new InternetAddress("from@domain.com")));
+ assertThat(message.getRecipients(Message.RecipientType.TO).length, is(1));
+ assertThat((InternetAddress) message.getRecipients(Message.RecipientType.TO)[0], equalTo(new InternetAddress("to@domain.com", "To")));
+ assertThat(message.getSubject(), equalTo("_subject"));
+ assertThat(Profile.STANDARD.textBody(message), equalTo("_text_body"));
+ latch.countDown();
+ }
+ });
+
+ account.send(email, null, Profile.STANDARD);
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("waiting for email too long");
+ }
+
+ handle.remove();
+ }
+
+ @Test
+ public void testSend_CC_BCC() throws Exception {
+ Account account = new Account(new Account.Config("default", ImmutableSettings.builder()
+ .put("smtp.host", "localhost")
+ .put("smtp.port", 2500)
+ .put("smtp.user", USERNAME)
+ .put("smtp.password", PASSWORD)
+ .build()), logger);
+
+ Email email = Email.builder()
+ .id("_id")
+ .from(new Email.Address("from@domain.com"))
+ .to(Email.AddressList.parse("TO"))
+ .cc(Email.AddressList.parse("CC1,cc2@domain.com"))
+ .bcc(Email.AddressList.parse("BCC1,bcc2@domain.com"))
+ .replyTo(Email.AddressList.parse("noreply@domain.com"))
+ .build();
+
+ final CountDownLatch latch = new CountDownLatch(5);
+ EmailServer.Listener.Handle handle = server.addListener(new EmailServer.Listener() {
+ @Override
+ public void on(MimeMessage message) throws Exception {
+ assertThat(message.getFrom().length, is(1));
+ assertThat((InternetAddress) message.getFrom()[0], equalTo(new InternetAddress("from@domain.com")));
+ assertThat(message.getRecipients(Message.RecipientType.TO).length, is(1));
+ assertThat((InternetAddress) message.getRecipients(Message.RecipientType.TO)[0], equalTo(new InternetAddress("to@domain.com", "TO")));
+ assertThat(message.getRecipients(Message.RecipientType.CC).length, is(2));
+ assertThat(message.getRecipients(Message.RecipientType.CC), hasItemInArray((Address) new InternetAddress("cc1@domain.com", "CC1")));
+ assertThat(message.getRecipients(Message.RecipientType.CC), hasItemInArray((Address) new InternetAddress("cc2@domain.com")));
+ assertThat(message.getReplyTo(), arrayWithSize(1));
+ assertThat(message.getReplyTo(), hasItemInArray((Address) new InternetAddress("noreply@domain.com")));
+ // bcc should not be there... (it's bcc after all)
+ latch.countDown();
+ }
+ });
+
+ account.send(email, null, Profile.STANDARD);
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("waiting for email too long");
+ }
+
+ handle.remove();
+ }
+
+ @Test
+ public void testSend_Authentication() throws Exception {
+ Account account = new Account(new Account.Config("default", ImmutableSettings.builder()
+ .put("smtp.host", "localhost")
+ .put("smtp.port", 2500)
+ .build()), logger);
+
+ Email email = Email.builder()
+ .id("_id")
+ .from(new Email.Address("from@domain.com"))
+ .to(Email.AddressList.parse("To"))
+ .subject("_subject")
+ .textBody("_text_body")
+ .build();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ EmailServer.Listener.Handle handle = server.addListener(new EmailServer.Listener() {
+ @Override
+ public void on(MimeMessage message) throws Exception {
+ latch.countDown();
+ }
+ });
+
+ account.send(email, new Authentication(USERNAME, PASSWORD), Profile.STANDARD);
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("waiting for email too long");
+ }
+
+ handle.remove();
+ }
+
+}
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/service/AccountsTests.java b/src/test/java/org/elasticsearch/alerts/actions/email/service/AccountsTests.java
new file mode 100644
index 00000000000..d13a90ba258
--- /dev/null
+++ b/src/test/java/org/elasticsearch/alerts/actions/email/service/AccountsTests.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.alerts.actions.email.service;
+
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.isOneOf;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+
+/**
+ *
+ */
+public class AccountsTests extends ElasticsearchTestCase {
+
+ @Test
+ public void testSingleAccount() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder()
+ .put("default_account", "account1");
+ addAccountSettings("account1", builder);
+
+ Accounts accounts = new Accounts(builder.build(), logger);
+ Account account = accounts.account("account1");
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ account = accounts.account(null); // falling back on the default
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ }
+
+ @Test
+ public void testSingleAccount_NoExplicitDefault() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder();
+ addAccountSettings("account1", builder);
+
+ Accounts accounts = new Accounts(builder.build(), logger);
+ Account account = accounts.account("account1");
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ account = accounts.account(null); // falling back on the default
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ }
+
+ @Test
+ public void testMultipleAccounts() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder()
+ .put("default_account", "account1");
+ addAccountSettings("account1", builder);
+ addAccountSettings("account2", builder);
+
+ Accounts accounts = new Accounts(builder.build(), logger);
+ Account account = accounts.account("account1");
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ account = accounts.account("account2");
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account2"));
+ account = accounts.account(null); // falling back on the default
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ }
+
+ @Test
+ public void testMultipleAccounts_NoExplicitDefault() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder()
+ .put("default_account", "account1");
+ addAccountSettings("account1", builder);
+ addAccountSettings("account2", builder);
+
+ Accounts accounts = new Accounts(builder.build(), logger);
+ Account account = accounts.account("account1");
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account1"));
+ account = accounts.account("account2");
+ assertThat(account, notNullValue());
+ assertThat(account.name(), equalTo("account2"));
+ account = accounts.account(null);
+ assertThat(account, notNullValue());
+ assertThat(account.name(), isOneOf("account1", "account2"));
+ }
+
+ @Test(expected = EmailSettingsException.class)
+ public void testMultipleAccounts_UnknownDefault() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder()
+ .put("default_account", "unknown");
+ addAccountSettings("account1", builder);
+ addAccountSettings("account2", builder);
+ new Accounts(builder.build(), logger);
+ }
+
+ @Test
+ public void testNoAccount() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder();
+ Accounts accounts = new Accounts(builder.build(), logger);
+ Account account = accounts.account(null);
+ assertThat(account, nullValue());
+ }
+
+ @Test(expected = EmailSettingsException.class)
+ public void testNoAccount_WithDefaultAccount() throws Exception {
+ ImmutableSettings.Builder builder = ImmutableSettings.builder()
+ .put("default_account", "unknown");
+ new Accounts(builder.build(), logger);
+ }
+
+ private void addAccountSettings(String name, ImmutableSettings.Builder builder) {
+ builder.put("account." + name + ".smtp.host", "_host");
+ }
+}
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/service/InternalEmailServiceTests.java b/src/test/java/org/elasticsearch/alerts/actions/email/service/InternalEmailServiceTests.java
new file mode 100644
index 00000000000..f4f07f08d3a
--- /dev/null
+++ b/src/test/java/org/elasticsearch/alerts/actions/email/service/InternalEmailServiceTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.alerts.actions.email.service;
+
+import org.elasticsearch.common.logging.ESLogger;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.node.settings.NodeSettingsService;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ *
+ */
+public class InternalEmailServiceTests extends ElasticsearchTestCase {
+
+ private InternalEmailService service;
+ private Accounts accounts;
+
+ @Before
+ public void init() throws Exception {
+ accounts = mock(Accounts.class);
+ service = new InternalEmailService(ImmutableSettings.EMPTY, new NodeSettingsService(ImmutableSettings.EMPTY)) {
+ @Override
+ protected Accounts createAccounts(Settings settings, ESLogger logger) {
+ return accounts;
+ }
+ };
+ service.start();
+ }
+
+ @After
+ public void cleanup() throws Exception {
+ service.stop();
+ }
+
+ @Test
+ public void testSend() throws Exception {
+ Account account = mock(Account.class);
+ when(account.name()).thenReturn("account1");
+ when(accounts.account("account1")).thenReturn(account);
+ Email email = mock(Email.class);
+ Authentication auth = new Authentication("user", "passwd");
+ Profile profile = randomFrom(Profile.values());
+ EmailService.EmailSent sent = service.send(email, auth, profile, "account1");
+ verify(account).send(email, auth, profile);
+ assertThat(sent, notNullValue());
+ assertThat(sent.email(), sameInstance(email));
+ assertThat(sent.account(), is("account1"));
+ }
+
+}
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/service/ManualPublicSmtpServersTests.java b/src/test/java/org/elasticsearch/alerts/actions/email/service/ManualPublicSmtpServersTests.java
new file mode 100644
index 00000000000..41807d932d5
--- /dev/null
+++ b/src/test/java/org/elasticsearch/alerts/actions/email/service/ManualPublicSmtpServersTests.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.alerts.actions.email.service;
+
+import org.elasticsearch.common.cli.Terminal;
+import org.elasticsearch.common.inject.Provider;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.node.settings.NodeSettingsService;
+import org.junit.Ignore;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ *
+ */
+@Ignore
+public class ManualPublicSmtpServersTests {
+
+ private static final Terminal terminal = Terminal.DEFAULT;
+
+ public static class Gmail {
+
+ public static void main(String[] args) throws Exception {
+ test(Profile.GMAIL, ImmutableSettings.builder()
+ .put("alerts.actions.email.service.account.gmail.smtp.auth", true)
+ .put("alerts.actions.email.service.account.gmail.smtp.starttls.enable", true)
+ .put("alerts.actions.email.service.account.gmail.smtp.host", "smtp.gmail.com")
+ .put("alerts.actions.email.service.account.gmail.smtp.port", 587)
+ .put("alerts.actions.email.service.account.gmail.smtp.user", terminal.readText("username: "))
+ .put("alerts.actions.email.service.account.gmail.smtp.password", new String(terminal.readSecret("password: ")))
+ .put("alerts.actions.email.service.account.gmail.email_defaults.to", terminal.readText("to: "))
+ );
+
+ }
+ }
+
+ public static class OutlookDotCom {
+
+ public static void main(String[] args) throws Exception {
+ test(Profile.STANDARD, ImmutableSettings.builder()
+ .put("alerts.actions.email.service.account.outlook.smtp.auth", true)
+ .put("alerts.actions.email.service.account.outlook.smtp.starttls.enable", true)
+ .put("alerts.actions.email.service.account.outlook.smtp.host", "smtp-mail.outlook.com")
+ .put("alerts.actions.email.service.account.outlook.smtp.port", 587)
+ .put("alerts.actions.email.service.account.outlook.smtp.user", "elastic.user@outlook.com")
+ .put("alerts.actions.email.service.account.outlook.smtp.password", "fantastic42")
+ .put("alerts.actions.email.service.account.outlook.email_defaults.to", "elastic.user@outlook.com")
+ .put()
+ );
+ }
+ }
+
+ public static class YahooMail {
+
+ public static void main(String[] args) throws Exception {
+ test(Profile.STANDARD, ImmutableSettings.builder()
+ .put("alerts.actions.email.service.account.yahoo.smtp.starttls.enable", true)
+ .put("alerts.actions.email.service.account.yahoo.smtp.auth", true)
+ .put("alerts.actions.email.service.account.yahoo.smtp.host", "smtp.mail.yahoo.com")
+ .put("alerts.actions.email.service.account.yahoo.smtp.port", 587)
+ .put("alerts.actions.email.service.account.yahoo.smtp.user", "elastic.user@yahoo.com")
+ .put("alerts.actions.email.service.account.yahoo.smtp.password", "fantastic42")
+ // note: from must be set to the same authenticated user account
+ .put("alerts.actions.email.service.account.yahoo.email_defaults.from", "elastic.user@yahoo.com")
+ .put("alerts.actions.email.service.account.yahoo.email_defaults.to", "elastic.user@yahoo.com")
+ );
+ }
+ }
+
+ static void test(Profile profile, Settings.Builder builder) throws Exception {
+ InternalEmailService service = startEmailService(builder);
+ try {
+
+ ToXContent content = new ToXContent() {
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ return builder.startObject()
+ .field("key1", "value1")
+ .field("key2", "value2")
+ .field("key3", "value3")
+ .endObject();
+ }
+ };
+
+ Email email = Email.builder()
+ .id("_id")
+ .subject("_subject")
+ .textBody("_text_body")
+ .htmlBody("html body")
+ .attach(new Attachment.XContent.Yaml("test.yml", content))
+ .inline(new Inline.Stream("logo", "logo.jpg", new Provider() {
+ @Override
+ public InputStream get() {
+ return InternalEmailServiceTests.class.getResourceAsStream("logo.jpg");
+ }
+ }))
+ .build();
+
+ EmailService.EmailSent sent = service.send(email, null, profile);
+
+ terminal.println("email sent via account [%s]", sent.account());
+ } finally {
+ service.stop();
+ }
+ }
+
+ static InternalEmailService startEmailService(Settings.Builder builder) {
+ Settings settings = builder.build();
+ InternalEmailService service = new InternalEmailService(settings, new NodeSettingsService(settings));
+ service.start();
+ return service;
+ }
+}
diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/service/support/EmailServer.java b/src/test/java/org/elasticsearch/alerts/actions/email/service/support/EmailServer.java
new file mode 100644
index 00000000000..36dbcb9fb79
--- /dev/null
+++ b/src/test/java/org/elasticsearch/alerts/actions/email/service/support/EmailServer.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.alerts.actions.email.service.support;
+
+import org.subethamail.smtp.TooMuchDataException;
+import org.subethamail.smtp.auth.EasyAuthenticationHandlerFactory;
+import org.subethamail.smtp.auth.LoginFailedException;
+import org.subethamail.smtp.auth.UsernamePasswordValidator;
+import org.subethamail.smtp.helper.SimpleMessageListener;
+import org.subethamail.smtp.helper.SimpleMessageListenerAdapter;
+import org.subethamail.smtp.server.SMTPServer;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.fail;
+
+/**
+ * An mini email smtp server that can be used for unit testing
+ *
+ *
+ */
+public class EmailServer {
+
+ private final List listeners = new CopyOnWriteArrayList<>();
+
+ private final SMTPServer server;
+
+ public EmailServer(String host, int port, final String username, final String password) {
+ server = new SMTPServer(new SimpleMessageListenerAdapter(new SimpleMessageListener() {
+ @Override
+ public boolean accept(String from, String recipient) {
+ return true;
+ }
+
+ @Override
+ public void deliver(String from, String recipient, InputStream data) throws TooMuchDataException, IOException {
+ try {
+ Session session = Session.getDefaultInstance(new Properties());
+ MimeMessage msg = new MimeMessage(session, data);
+ for (Listener listener : listeners) {
+ try {
+ listener.on(msg);
+ } catch (Exception e) {
+ fail(e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ } catch (MessagingException me) {
+ throw new RuntimeException("could not create mime message", me);
+ }
+ }
+ }), new EasyAuthenticationHandlerFactory(new UsernamePasswordValidator() {
+ @Override
+ public void login(String user, String passwd) throws LoginFailedException {
+ assertThat(user, is(username));
+ assertThat(passwd, is(password));
+ }
+ }));
+ server.setHostName(host);
+ server.setPort(port);
+ }
+
+ public void start() {
+ server.start();
+ }
+
+ public void stop() {
+ server.stop();
+ listeners.clear();
+ }
+
+ public Listener.Handle addListener(Listener listener) {
+ listeners.add(listener);
+ return new Listener.Handle(listeners, listener);
+ }
+
+ public static interface Listener {
+
+ void on(MimeMessage message) throws Exception;
+
+ public static class Handle {
+
+ private final List listeners;
+ private final Listener listener;
+
+ Handle(List listeners, Listener listener) {
+ this.listeners = listeners;
+ this.listener = listener;
+ }
+
+ public void remove() {
+ listeners.remove(listener);
+ }
+ }
+
+ }
+
+}
diff --git a/src/test/resources/org/elasticsearch/alerts/actions/email/service/logo.jpg b/src/test/resources/org/elasticsearch/alerts/actions/email/service/logo.jpg
new file mode 100644
index 00000000000..f45114b4b99
Binary files /dev/null and b/src/test/resources/org/elasticsearch/alerts/actions/email/service/logo.jpg differ