@@ -276,6 +276,11 @@ | |||||
</exclusions> | </exclusions> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>com.sendgrid</groupId> | |||||
<artifactId>sendgrid-java</artifactId> | |||||
<version>4.6.5</version> | |||||
</dependency> | |||||
</dependencies> | </dependencies> | ||||
<build> | <build> | ||||
@@ -10,6 +10,7 @@ import bubble.model.cloud.CloudService; | |||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
import com.github.jknack.handlebars.Handlebars; | import com.github.jknack.handlebars.Handlebars; | ||||
import lombok.NonNull; | |||||
import org.cobbzilla.util.handlebars.HandlebarsUtil; | import org.cobbzilla.util.handlebars.HandlebarsUtil; | ||||
import org.cobbzilla.util.http.HttpResponseBean; | import org.cobbzilla.util.http.HttpResponseBean; | ||||
import org.cobbzilla.util.http.HttpUtil; | import org.cobbzilla.util.http.HttpUtil; | ||||
@@ -28,6 +29,18 @@ public interface CloudServiceDriver { | |||||
default boolean disableDelegation () { return false; } | 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); | void setConfig(JsonNode json, CloudService cloudService); | ||||
static <T extends CloudServiceDriver> T setupDriver(BubbleConfiguration configuration, T driver) { | static <T extends CloudServiceDriver> T setupDriver(BubbleConfiguration configuration, T driver) { | ||||
@@ -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; | |||||
} | |||||
} |
@@ -10,19 +10,30 @@ import bubble.cloud.email.mock.MockMailSender; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.account.AccountContact; | import bubble.model.account.AccountContact; | ||||
import bubble.model.account.message.AccountMessage; | import bubble.model.account.message.AccountMessage; | ||||
import bubble.model.cloud.CloudService; | |||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import com.fasterxml.jackson.databind.JsonNode; | |||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.mail.sender.SmtpMailSender; | import org.cobbzilla.mail.sender.SmtpMailSender; | ||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import java.util.ArrayList; | |||||
import java.util.List; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||||
@Slf4j | @Slf4j | ||||
public class SmtpEmailDriver extends CloudServiceDriverBase<EmailDriverConfig> implements EmailServiceDriver { | public class SmtpEmailDriver extends CloudServiceDriverBase<EmailDriverConfig> implements EmailServiceDriver { | ||||
protected static final String SENDGRID_SMTP = "smtp.sendgrid.net"; | |||||
private static final List<String> 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; | @Autowired @Getter protected BubbleConfiguration configuration; | ||||
@@ -38,6 +49,13 @@ public class SmtpEmailDriver extends CloudServiceDriverBase<EmailDriverConfig> i | |||||
return smtpSender; | 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) { | @Override public boolean send(Account account, AccountMessage message, AccountContact contact) { | ||||
return EmailServiceDriver.send(this, account, message, contact); | return EmailServiceDriver.send(this, account, message, contact); | ||||
} | } | ||||
@@ -229,12 +229,11 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||||
.setTemplate(false); | .setTemplate(false); | ||||
} else { | } 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) { | @Override public void postCreate(CloudService parentEntity, CloudService accountEntity) { | ||||
clouds.put(parentEntity.getUuid(), accountEntity); | clouds.put(parentEntity.getUuid(), accountEntity); | ||||
} | } | ||||
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; | |||||
import com.fasterxml.jackson.databind.node.ObjectNode; | import com.fasterxml.jackson.databind.node.ObjectNode; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.NoArgsConstructor; | import lombok.NoArgsConstructor; | ||||
import lombok.NonNull; | |||||
import lombok.Setter; | import lombok.Setter; | ||||
import lombok.experimental.Accessors; | import lombok.experimental.Accessors; | ||||
import org.cobbzilla.util.collection.ArrayUtil; | import org.cobbzilla.util.collection.ArrayUtil; | ||||
@@ -342,4 +343,8 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun | |||||
} | } | ||||
return errors; | return errors; | ||||
} | } | ||||
@PostRemove void onPostRemove(@NonNull final CloudService service) { | |||||
service.getDriver().postServiceDelete(service); | |||||
} | |||||
} | } |
@@ -72,7 +72,7 @@ | |||||
{ | { | ||||
"name": "SmtpServer", | "name": "SmtpServer", | ||||
"type": "email", | "type": "email", | ||||
"driverClass": "bubble.cloud.email.SmtpEmailDriver", | |||||
"driverClass": "{{BUBBLE_SMTP_DRIVER}}", | |||||
"driverConfig": { | "driverConfig": { | ||||
"tlsEnabled": true | "tlsEnabled": true | ||||
}, | }, | ||||