diff --git a/bubble-server/pom.xml b/bubble-server/pom.xml
index bb0f8330..4ed177c5 100644
--- a/bubble-server/pom.xml
+++ b/bubble-server/pom.xml
@@ -276,6 +276,11 @@
+
+ com.sendgrid
+ sendgrid-java
+ 4.6.5
+
diff --git a/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java
index 5f1e7abd..af47b474 100644
--- a/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java
+++ b/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java
@@ -10,6 +10,7 @@ import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.jknack.handlebars.Handlebars;
+import lombok.NonNull;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.http.HttpResponseBean;
import org.cobbzilla.util.http.HttpUtil;
@@ -28,6 +29,18 @@ public interface CloudServiceDriver {
default boolean disableDelegation () { return false; }
+ @NonNull default CloudService setupDelegatedCloudService(@NonNull final BubbleConfiguration configuration,
+ @NonNull final CloudService parentService,
+ @NonNull final CloudService delegatedService) {
+ return delegatedService.setDelegated(parentService.getUuid())
+ .setCredentials(CloudCredentials.delegate(configuration.getThisNode(), configuration))
+ .setTemplate(false);
+ }
+
+ default void postServiceDelete(@NonNull final CloudService service) {
+ // noop
+ }
+
void setConfig(JsonNode json, CloudService cloudService);
static T setupDriver(BubbleConfiguration configuration, T driver) {
diff --git a/bubble-server/src/main/java/bubble/cloud/email/SendgridSmtpEmailDriver.java b/bubble-server/src/main/java/bubble/cloud/email/SendgridSmtpEmailDriver.java
new file mode 100644
index 00000000..ea1ba7b5
--- /dev/null
+++ b/bubble-server/src/main/java/bubble/cloud/email/SendgridSmtpEmailDriver.java
@@ -0,0 +1,116 @@
+package bubble.cloud.email;
+
+import bubble.dao.account.AccountDAO;
+import bubble.dao.cloud.CloudServiceDAO;
+import bubble.model.account.Account;
+import bubble.model.cloud.CloudCredentials;
+import bubble.model.cloud.CloudService;
+import bubble.server.BubbleConfiguration;
+import com.sendgrid.Method;
+import com.sendgrid.Request;
+import com.sendgrid.Response;
+import com.sendgrid.SendGrid;
+import lombok.*;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.io.IOException;
+
+import static bubble.cloud.storage.StorageCryptStream.MIN_DISTINCT_LENGTH;
+import static bubble.cloud.storage.StorageCryptStream.MIN_KEY_LENGTH;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.cobbzilla.util.daemon.ZillaRuntime.die;
+import static org.cobbzilla.util.json.JsonUtil.EMPTY_JSON;
+import static org.cobbzilla.util.json.JsonUtil.json;
+import static org.cobbzilla.util.security.CryptoUtil.generatePassword;
+import static org.cobbzilla.util.string.StringUtil.repeat;
+
+public class SendgridSmtpEmailDriver extends SmtpEmailDriver {
+
+ private static final String PARAM_PARENT_SERVICE = "parentService";
+
+ @Autowired private AccountDAO accountDAO;
+ @Autowired private CloudServiceDAO serviceDAO;
+
+ @Override protected boolean isServiceCompatible() { return this.config.getHost().equals(SENDGRID_SMTP); }
+
+ /**
+ * Build username which will be used for Subuser created on SendGrid's service for specified Account's object.
+ */
+ @NonNull private String sgUsername(@NonNull final CloudService delegatedService) {
+ return delegatedService.getShortId();
+ }
+
+ @Override @NonNull public CloudService setupDelegatedCloudService(@NonNull final BubbleConfiguration configuration,
+ @NonNull final CloudService parentService,
+ @NonNull final CloudService delegatedService) {
+ final CloudCredentials parentCredentials = parentService.getCredentials();
+ if (parentService.delegated() || !parentCredentials.getParam(PARAM_HOST).contains(".sendgrid.net")) {
+ return super.setupDelegatedCloudService(configuration, parentService, delegatedService);
+ }
+
+ final SendGrid sg = new SendGrid(parentCredentials.getParam(PARAM_PASSWORD));
+ final Account accountWithDelegate = accountDAO.findByUuid(delegatedService.getAccount());
+ final String user = sgUsername(delegatedService);
+ String password = generatePassword(MIN_KEY_LENGTH, MIN_DISTINCT_LENGTH);
+ final CreateSubuserRequest data = new CreateSubuserRequest(user, accountWithDelegate.getEmail(), password,
+ new String[]{});
+ final Request req = new Request();
+ req.setMethod(Method.POST);
+ req.setEndpoint("subusers");
+ req.setBody(json(data));
+
+ final Response res;
+ try {
+ res = sg.api(req);
+ } catch (IOException e) {
+ return die("Cannot create SendGrid Subuser", e);
+ }
+ if (res.getStatusCode() != HTTP_OK) {
+ return die("Wrong response when creating SendGrid Subuser: " + res.getStatusCode() + " : " + res.getBody());
+ }
+
+ delegatedService.setDelegated(null).setTemplate(false);
+ delegatedService.getCredentials()
+ .setParam(PARAM_USER, user)
+ .setParam(PARAM_PASSWORD, password)
+ .setParam(PARAM_PARENT_SERVICE, parentService.getUuid());
+ password = repeat("x", MIN_KEY_LENGTH); // Override password (in memory) just in case
+ return delegatedService;
+ }
+
+ @Override public void postServiceDelete(@NonNull final CloudService service) {
+ final String parentServiceUuid = service.getCredentials().getParam(PARAM_PARENT_SERVICE);
+ if (parentServiceUuid == null) return;
+
+ final CloudService parentService = serviceDAO.findByUuid(parentServiceUuid);
+ if (parentService == null) return;
+
+ final SendGrid sg = new SendGrid(parentService.getCredentials().getParam(PARAM_PASSWORD));
+ final String sgUserToDelete = sgUsername(service);
+
+ final Request req = new Request();
+ req.setMethod(Method.DELETE);
+ req.setEndpoint("subusers/" + sgUserToDelete);
+ req.setBody(EMPTY_JSON);
+
+ final Response res;
+ try {
+ res = sg.api(req);
+ } catch (IOException e) {
+ die("Cannot delete SendGrid Subuser " + sgUserToDelete, e);
+ return;
+ }
+ if (res.getStatusCode() != HTTP_NO_CONTENT) {
+ die("Wrong response when creating SendGrid Subuser: " + res.getStatusCode() + " : " + res.getBody());
+ }
+ }
+
+ @AllArgsConstructor @NoArgsConstructor
+ private class CreateSubuserRequest {
+ @Getter @Setter private String username;
+ @Getter @Setter private String email;
+ @Getter @Setter private String password;
+ @Getter @Setter private String[] ips;
+ }
+}
diff --git a/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java b/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java
index 16916e78..1e757428 100644
--- a/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java
+++ b/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java
@@ -10,19 +10,30 @@ import bubble.cloud.email.mock.MockMailSender;
import bubble.model.account.Account;
import bubble.model.account.AccountContact;
import bubble.model.account.message.AccountMessage;
+import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
+import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.mail.sender.SmtpMailSender;
import org.springframework.beans.factory.annotation.Autowired;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.cobbzilla.util.daemon.ZillaRuntime.die;
+
@Slf4j
public class SmtpEmailDriver extends CloudServiceDriverBase implements EmailServiceDriver {
+ protected static final String SENDGRID_SMTP = "smtp.sendgrid.net";
+
+ private static final List SEPARATE_DRIVERS_SMTPS = new ArrayList<>();
+ static { SEPARATE_DRIVERS_SMTPS.add(SENDGRID_SMTP); }
- private static final String PARAM_USER = "user";
- private static final String PARAM_PASSWORD = "password";
- private static final String PARAM_HOST = "host";
- private static final String PARAM_PORT = "port";
+ protected static final String PARAM_USER = "user";
+ protected static final String PARAM_PASSWORD = "password";
+ protected static final String PARAM_HOST = "host";
+ protected static final String PARAM_PORT = "port";
@Autowired @Getter protected BubbleConfiguration configuration;
@@ -38,6 +49,13 @@ public class SmtpEmailDriver extends CloudServiceDriverBase i
return smtpSender;
}
+ @Override public void setConfig(JsonNode json, CloudService cloudService) {
+ super.setConfig(json, cloudService);
+ if (!isServiceCompatible()) die("Specified SmtpEmailDriver is not compatible with given config");
+ }
+
+ protected boolean isServiceCompatible() { return !SEPARATE_DRIVERS_SMTPS.contains(this.config.getHost()); }
+
@Override public boolean send(Account account, AccountMessage message, AccountContact contact) {
return EmailServiceDriver.send(this, account, message, contact);
}
diff --git a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java
index 45746fe0..271a69f2 100644
--- a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java
+++ b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java
@@ -229,12 +229,11 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc
.setTemplate(false);
} else {
- return accountEntity.setDelegated(parentEntity.getUuid())
- .setCredentials(CloudCredentials.delegate(configuration.getThisNode(), configuration))
- .setTemplate(false);
+ return driver.setupDelegatedCloudService(configuration, parentEntity, accountEntity);
}
}
}
+
@Override public void postCreate(CloudService parentEntity, CloudService accountEntity) {
clouds.put(parentEntity.getUuid(), accountEntity);
}
diff --git a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java
index 9379ac8f..52003d73 100644
--- a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java
+++ b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import lombok.NonNull;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.cobbzilla.util.collection.ArrayUtil;
@@ -342,4 +343,8 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun
}
return errors;
}
+
+ @PostRemove void onPostRemove(@NonNull final CloudService service) {
+ service.getDriver().postServiceDelete(service);
+ }
}
diff --git a/bubble-server/src/main/resources/models/defaults/cloudService.json b/bubble-server/src/main/resources/models/defaults/cloudService.json
index aa718d66..cf410649 100644
--- a/bubble-server/src/main/resources/models/defaults/cloudService.json
+++ b/bubble-server/src/main/resources/models/defaults/cloudService.json
@@ -72,7 +72,7 @@
{
"name": "SmtpServer",
"type": "email",
- "driverClass": "bubble.cloud.email.SmtpEmailDriver",
+ "driverClass": "{{BUBBLE_SMTP_DRIVER}}",
"driverConfig": {
"tlsEnabled": true
},