From bf2afd873946f9637d79d3867c8c6c59aec264e5 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Wed, 20 Sep 2017 17:22:51 +0200 Subject: [PATCH] Further work --- .../io/kamax/mxisd/config/SessionConfig.java | 35 ++++++++ .../invite/medium/EmailInviteConfig.java | 67 --------------- .../threepid/connector/EmailSmtpConfig.java | 2 +- .../config/threepid/medium/EmailConfig.java | 69 ++++++++++++---- .../threepid/medium/EmailTemplateConfig.java | 82 +++++++++++++++++++ .../controller/v1/SessionController.groovy | 4 +- .../exception/NotImplementedException.groovy | 7 +- .../mxisd/invitation/InvitationManager.java | 35 ++------ .../INotificationHandler.java} | 9 +- .../notification/NotificationManager.java | 69 ++++++++++++++++ .../SessionMananger.java} | 45 +++++----- .../connector/IThreePidConnector.java | 6 +- .../{ => email}/EmailSmtpConnector.java | 46 ++++------- .../connector/email/IEmailConnector.java | 35 ++++++++ .../notification/INotificationGenerator.java | 38 +++++++++ .../email/EmailNotificationGenerator.java} | 74 +++++++++++------ .../email/EmailNotificationHandler.java | 71 ++++++++++++++++ .../email/IEmailNotificationGenerator.java | 33 ++++++++ .../threepid/session/IThreePidSession.java | 4 +- .../threepid/session/ThreePidSession.java | 5 ++ src/main/resources/application.yaml | 27 +++--- .../resources/email/validate-template.eml | 66 +++++++++++++++ 22 files changed, 623 insertions(+), 206 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/config/SessionConfig.java delete mode 100644 src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java rename src/main/groovy/io/kamax/mxisd/{invitation/generator/IInviteContentGenerator.java => notification/INotificationHandler.java} (79%) create mode 100644 src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java rename src/main/groovy/io/kamax/mxisd/{mapping/MappingManager.java => session/SessionMananger.java} (87%) rename src/main/groovy/io/kamax/mxisd/threepid/connector/{ => email}/EmailSmtpConnector.java (64%) create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java rename src/main/groovy/io/kamax/mxisd/{invitation/generator/EmailInviteContentGenerator.java => threepid/notification/email/EmailNotificationGenerator.java} (56%) create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java create mode 100644 src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java create mode 100644 src/main/resources/email/validate-template.eml diff --git a/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java new file mode 100644 index 0000000..fb51537 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/SessionConfig.java @@ -0,0 +1,35 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("session") +public class SessionConfig { + + private static Logger log = LoggerFactory.getLogger(SessionConfig.class); + + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java b/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java deleted file mode 100644 index a244aff..0000000 --- a/src/main/groovy/io/kamax/mxisd/config/invite/medium/EmailInviteConfig.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * mxisd - Matrix Identity Server Daemon - * Copyright (C) 2017 Maxime Dor - * - * https://max.kamax.io/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package io.kamax.mxisd.config.invite.medium; - -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import javax.annotation.PostConstruct; -import java.io.File; - -@Configuration -@ConfigurationProperties("invite.medium.email") -public class EmailInviteConfig { - - private Logger log = LoggerFactory.getLogger(EmailInviteConfig.class); - - private String template; - - public String getTemplate() { - return template; - } - - public void setTemplate(String template) { - this.template = template; - } - - @PostConstruct - public void build() { - log.info("--- E-mail invites config ---"); - - if (!StringUtils.startsWith(getTemplate(), "classpath:")) { - if (StringUtils.isBlank(getTemplate())) { - log.warn("invite.medium.email is empty! Will not send invites"); - } else { - File cp = new File(getTemplate()).getAbsoluteFile(); - log.info("Template: {}", cp.getAbsolutePath()); - if (!cp.exists() || !cp.isFile() || !cp.canRead()) { - log.warn(getTemplate() + " does not exist, is not a file or cannot be read"); - } - } - } else { - log.info("Template: Built-in: {}", getTemplate()); - } - } - -} diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java index 24a5e49..49848b8 100644 --- a/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/connector/EmailSmtpConfig.java @@ -29,7 +29,7 @@ import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; @Configuration -@ConfigurationProperties(prefix = "threepid.email.connector.provider.smtp") +@ConfigurationProperties(prefix = "threepid.medium.email.connectors.smtp") public class EmailSmtpConfig { private Logger log = LoggerFactory.getLogger(EmailSmtpConfig.class); diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java index 2d47bf3..29ac7c0 100644 --- a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailConfig.java @@ -21,6 +21,7 @@ package io.kamax.mxisd.config.threepid.medium; import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.exception.ConfigurationException; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.slf4j.Logger; @@ -35,43 +36,81 @@ import javax.annotation.PostConstruct; @ConfigurationProperties("threepid.medium.email") public class EmailConfig { + public static class Identity { + private String from; + private String name; + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + + private String generator; + private String connector; + private Logger log = LoggerFactory.getLogger(EmailConfig.class); private MatrixConfig mxCfg; - - private String from; - private String name; + private Identity identity = new Identity(); @Autowired public EmailConfig(MatrixConfig mxCfg) { this.mxCfg = mxCfg; } - public String getFrom() { - return from; + public Identity getIdentity() { + return identity; } - public void setFrom(String from) { - this.from = from; + public String getGenerator() { + return generator; } - public String getName() { - return name; + public void setGenerator(String generator) { + this.generator = generator; } - public void setName(String name) { - this.name = name; + public String getConnector() { + return connector; + } + + public void setConnector(String connector) { + this.connector = connector; } @PostConstruct public void build() { log.info("--- E-mail config ---"); - log.info("From: {}", getFrom()); - if (StringUtils.isBlank(getName())) { - setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); + if (StringUtils.isBlank(getGenerator())) { + throw new ConfigurationException("generator"); } - log.info("Name: {}", getName()); + + if (StringUtils.isBlank(getConnector())) { + throw new ConfigurationException("connector"); + } + + log.info("From: {}", identity.getFrom()); + + if (StringUtils.isBlank(identity.getName())) { + identity.setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server"); + } + log.info("Name: {}", identity.getName()); + log.info("Generator: {}", getGenerator()); + log.info("Connector: {}", getConnector()); } } diff --git a/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java new file mode 100644 index 0000000..4054dd7 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/threepid/medium/EmailTemplateConfig.java @@ -0,0 +1,82 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config.threepid.medium; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties("threepid.medium.email.generators.template") +public class EmailTemplateConfig { + + private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class); + private static final String classpathPrefix = "classpath:"; + + private static String getName(String path) { + if (StringUtils.startsWith(path, classpathPrefix)) { + return "Built-in (" + path.substring(classpathPrefix.length()) + ")"; + } + + return path; + } + + public static class Session { + + private String validation; + + public String getValidation() { + return validation; + } + + public void setValidation(String validation) { + this.validation = validation; + } + + } + + private String invite; + private Session session = new Session(); + + public String getInvite() { + return invite; + } + + public void setInvite(String invite) { + this.invite = invite; + } + + public Session getSession() { + return session; + } + + @PostConstruct + public void build() { + log.info("--- E-mail Generator templates config ---"); + log.info("Invite: {}", getName(getInvite())); + log.info("Session validation: {}", getName(getSession().getValidation())); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy index 93181b4..1e107d7 100644 --- a/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/SessionController.groovy @@ -29,7 +29,7 @@ import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.lookup.ThreePidValidation -import io.kamax.mxisd.mapping.MappingManager +import io.kamax.mxisd.session.SessionMananger import org.apache.commons.io.IOUtils import org.apache.http.HttpStatus import org.slf4j.Logger @@ -48,7 +48,7 @@ import java.nio.charset.StandardCharsets class SessionController { @Autowired - private MappingManager mgr + private SessionMananger mgr @Autowired private InvitationManager invMgr; diff --git a/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy b/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy index 57ab376..5912136 100644 --- a/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy +++ b/src/main/groovy/io/kamax/mxisd/exception/NotImplementedException.groovy @@ -24,5 +24,10 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus @ResponseStatus(value = HttpStatus.NOT_IMPLEMENTED) -class NotImplementedException extends RuntimeException { +public class NotImplementedException extends RuntimeException { + + public NotImplementedException(String s) { + super(s); + } + } diff --git a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java index d5502d3..f0acdd5 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java @@ -26,14 +26,13 @@ import io.kamax.mxisd.config.DnsOverwrite; import io.kamax.mxisd.config.DnsOverwriteEntry; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; -import io.kamax.mxisd.invitation.generator.IInviteContentGenerator; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.signature.SignatureManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO; -import io.kamax.mxisd.threepid.connector.IThreePidConnector; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; @@ -84,35 +83,15 @@ public class InvitationManager { @Autowired private DnsOverwrite dns; - private Map generators; - private Map connectors; + private NotificationManager notifMgr; private CloseableHttpClient client; private Gson gson; private Timer refreshTimer; @Autowired - public InvitationManager( - List generatorList, - List connectorList - ) { - generators = new HashMap<>(); - generatorList.forEach(sender -> { // FIXME to support several possible implementations - if (generators.containsKey(sender.getMedium())) { - throw new RuntimeException("More than one " + sender.getMedium() + " content generator"); - } - - generators.put(sender.getMedium(), sender); - }); - - connectors = new HashMap<>(); - connectorList.forEach(connector -> { // FIXME to support several possible implementations - if (connectors.containsKey(connector.getMedium())) { - throw new RuntimeException("More than one " + connector.getMedium() + " connector"); - } - - connectors.put(connector.getMedium(), connector); - }); + public InvitationManager(NotificationManager notifMgr) { + this.notifMgr = notifMgr; } @PostConstruct @@ -221,9 +200,7 @@ public class InvitationManager { } public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync - IInviteContentGenerator generator = generators.get(invitation.getMedium()); - IThreePidConnector connector = connectors.get(invitation.getMedium()); - if (generator == null || connector == null) { + if (!notifMgr.isMediumSupported(invitation.getMedium())) { throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); } @@ -246,7 +223,7 @@ public class InvitationManager { IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); - connector.send(reply, generator.generate(reply)); + notifMgr.send(reply); log.info("Storing invite under ID {}", invId); storage.insertInvite(reply); diff --git a/src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java similarity index 79% rename from src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java rename to src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java index a05b381..0b622df 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/generator/IInviteContentGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/notification/INotificationHandler.java @@ -18,14 +18,17 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.invitation.generator; +package io.kamax.mxisd.notification; import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; -public interface IInviteContentGenerator { +public interface INotificationHandler { String getMedium(); - String generate(IThreePidInviteReply invite); + void notify(IThreePidInviteReply invite); + + void notify(IThreePidSession session); } diff --git a/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java new file mode 100644 index 0000000..63d72e4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/notification/NotificationManager.java @@ -0,0 +1,69 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.notification; + +import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class NotificationManager { + + private Map handlers; + + @Autowired + public NotificationManager(List handlers) { + this.handlers = new HashMap<>(); + handlers.forEach(h -> this.handlers.put(h.getMedium(), h)); + } + + private INotificationHandler ensureMedium(String medium) { + INotificationHandler handler = handlers.get(medium); + if (handler == null) { + throw new NotImplementedException(medium + " is not a supported 3PID medium type"); + } + + return handler; + } + + public boolean isMediumSupported(String medium) { + return handlers.containsKey(medium); + } + + public void sendForInvite(IThreePidInviteReply invite) { + ensureMedium(invite.getInvite().getMedium()).notify(invite); + } + + public void sendForValidation(IThreePidSession session) { + ensureMedium(session.getThreePid().getMedium()).notify(session); + } + + public void sendforRemotePublish(IThreePidSession session) { + throw new NotImplementedException("Remote publish of 3PID bind"); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java similarity index 87% rename from src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java rename to src/main/groovy/io/kamax/mxisd/session/SessionMananger.java index 42e53b0..d6072d7 100644 --- a/src/main/groovy/io/kamax/mxisd/mapping/MappingManager.java +++ b/src/main/groovy/io/kamax/mxisd/session/SessionMananger.java @@ -18,14 +18,16 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.mapping; +package io.kamax.mxisd.session; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.config.SessionConfig; import io.kamax.mxisd.exception.InvalidCredentialsException; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.threepid.session.ThreePidSession; @@ -40,16 +42,21 @@ import java.util.Optional; import java.util.UUID; @Component -public class MappingManager { +public class SessionMananger { - private Logger log = LoggerFactory.getLogger(MappingManager.class); + private Logger log = LoggerFactory.getLogger(SessionMananger.class); + private SessionConfig cfg; private IStorage storage; private LookupStrategy lookup; + private NotificationManager notifMgr; @Autowired - public MappingManager(IStorage storage, LookupStrategy lookup) { + public SessionMananger(SessionConfig cfg, IStorage storage, LookupStrategy lookup, NotificationManager notifMgr) { + this.cfg = cfg; this.storage = storage; + this.lookup = lookup; + this.notifMgr = notifMgr; } private ThreePidSession getSession(String sid, String secret) { @@ -78,7 +85,8 @@ public class MappingManager { log.info("We already have a session for {}: {}", tpid, session.getId()); if (session.getAttempt() < attempt) { log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt()); - // TODO send via connector + notifMgr.sendForValidation(session); + log.info("Sent validation notification to {}", tpid); session.increaseAttempt(); storage.updateThreePidSession(session.getDao()); } @@ -95,8 +103,8 @@ public class MappingManager { ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token); log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); - // TODO send via connector - // log.info("Sent validation notification to {}", tpid); + notifMgr.sendForValidation(session); + log.info("Sent validation notification to {}", tpid); storage.insertThreePidSession(session.getDao()); log.info("Stored session {}", sessionId, tpid, server); @@ -124,13 +132,7 @@ public class MappingManager { ThreePidSession session = getSessionIfValidated(sid, secret); log.info("Attempting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer()); - // We lookup if the 3PID is already known locally. - // If it is, we do not need to process any further as it is already bound. - Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); - boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); - log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); - - // MXID is not known locally, checking remotely + // We lookup if the 3PID is already known remotely. Optional rRemote = lookup.findRemote(session.getThreePid().getMedium(), session.getThreePid().getAddress()); boolean knownRemote = rRemote.isPresent() && StringUtils.equals(rRemote.get().getMxid().getId(), mxid); log.info("Mapping {} -> {} is " + (knownRemote ? "already" : "not") + " known remotely", mxid, session.getThreePid()); @@ -143,25 +145,28 @@ public class MappingManager { isLocalDomain = session.getThreePid().getAddress().isEmpty(); // FIXME only for testing } if (knownRemote) { - if (isLocalDomain && !knownLocal) { - log.warn("Mapping {} -> {} is not known locally but is about a local domain!"); - } - log.info("No further action needed for Mapping {} -> {}"); return; } + // We lookup if the 3PID is already known locally. + Optional rLocal = lookup.findLocal(session.getThreePid().getMedium(), session.getThreePid().getAddress()); + boolean knownLocal = rLocal.isPresent() && StringUtils.equals(rLocal.get().getMxid().getId(), mxid); + log.info("Mapping {} -> {} is " + (knownLocal ? "already" : "not") + " known locally", mxid, session.getThreePid()); + // This might need a configuration by medium type? if (knownLocal) { // 3PID is ony known local if (isLocalDomain) { // TODO // 1. Check if global publishing is enabled, allowed and offered. If one is no, return. // 2. Publish globally + notifMgr.sendforRemotePublish(session); } - if (System.currentTimeMillis() % 2 == 0) { + if (System.currentTimeMillis() % 2 == 0) { // FIXME only for testing // TODO // 1. Check if configured to publish globally non-local domain. If no, return + notifMgr.sendforRemotePublish(session); } // TODO @@ -172,6 +177,7 @@ public class MappingManager { // 2. call mxisd-specific endpoint to publish globally // 3. check regularly on client page for a binding // 4. when found, show page "Done globally!" + notifMgr.sendforRemotePublish(session); } else { if (isLocalDomain) { // 3PID is not known anywhere but is a local domain // TODO @@ -180,6 +186,7 @@ public class MappingManager { } else { // 3PID is not known anywhere and is remote // TODO // Proxy to configurable IS, by default Matrix.org + notifMgr.sendforRemotePublish(session); } } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java index 07bdc1a..c8fc23f 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/IThreePidConnector.java @@ -20,12 +20,10 @@ package io.kamax.mxisd.threepid.connector; -import io.kamax.mxisd.invitation.IThreePidInviteReply; - public interface IThreePidConnector { + String getId(); + String getMedium(); - void send(IThreePidInviteReply invite, String content); - } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java similarity index 64% rename from src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java rename to src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java index 7bc3ae3..8124bf7 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/connector/EmailSmtpConnector.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java @@ -18,14 +18,11 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.threepid.connector; +package io.kamax.mxisd.threepid.connector.email; import com.sun.mail.smtp.SMTPTransport; import io.kamax.matrix.ThreePidMedium; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; -import io.kamax.mxisd.config.threepid.medium.EmailConfig; -import io.kamax.mxisd.exception.ConfigurationException; -import io.kamax.mxisd.invitation.IThreePidInviteReply; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,26 +39,22 @@ import java.nio.charset.StandardCharsets; import java.util.Date; @Component -public class EmailSmtpConnector implements IThreePidConnector { +public class EmailSmtpConnector implements IEmailConnector { private Logger log = LoggerFactory.getLogger(EmailSmtpConnector.class); private EmailSmtpConfig cfg; - private Session session; - private InternetAddress sender; @Autowired - public EmailSmtpConnector(EmailConfig cfg, EmailSmtpConfig smtpCfg) { - try { - session = Session.getInstance(System.getProperties()); - sender = new InternetAddress(cfg.getFrom(), cfg.getName()); - } catch (UnsupportedEncodingException e) { - // What are we supposed to do with this?! - throw new ConfigurationException(e); - } + public EmailSmtpConnector(EmailSmtpConfig cfg) { + this.cfg = cfg; + session = Session.getInstance(System.getProperties()); + } - this.cfg = smtpCfg; + @Override + public String getId() { + return "smtp"; } @Override @@ -70,20 +63,17 @@ public class EmailSmtpConnector implements IThreePidConnector { } @Override - public void send(IThreePidInviteReply invite, String content) { - if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) { - throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type"); - } - + public void send(String senderAddress, String senderName, String recipient, String content) { try { + InternetAddress sender = new InternetAddress(senderAddress, senderName); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); - msg.setHeader("X-Mailer", "mxisd"); // TODO set version + msg.setHeader("X-Mailer", "mxisd"); // FIXME set version msg.setSentDate(new Date()); msg.setFrom(sender); - msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress()); + msg.setRecipients(Message.RecipientType.TO, recipient); msg.saveChanges(); - log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort()); + log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort()); SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); transport.setStartTLS(cfg.getTls() > 0); transport.setRequireStartTLS(cfg.getTls() > 1); @@ -91,13 +81,13 @@ public class EmailSmtpConnector implements IThreePidConnector { log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort()); transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword()); try { - transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress())); - log.info("Invite to {} was sent", invite.getInvite().getAddress()); + transport.sendMessage(msg, InternetAddress.parse(recipient)); + log.info("Invite to {} was sent", recipient); } finally { transport.close(); } - } catch (MessagingException e) { - throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e); + } catch (UnsupportedEncodingException | MessagingException e) { + throw new RuntimeException("Unable to send e-mail invite to " + recipient, e); } } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java new file mode 100644 index 0000000..e2b7bd1 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/connector/email/IEmailConnector.java @@ -0,0 +1,35 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.connector.email; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.threepid.connector.IThreePidConnector; + +public interface IEmailConnector extends IThreePidConnector { + + @Override + default String getMedium() { + return ThreePidMedium.Email.getId(); + } + + void send(String senderAddress, String senderName, String recipient, String content); + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java new file mode 100644 index 0000000..969a87b --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/INotificationGenerator.java @@ -0,0 +1,38 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification; + +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; + +public interface INotificationGenerator { + + String getId(); + + String getMedium(); + + String get(IThreePidInviteReply invite); + + String getForValidation(IThreePidSession session); + + String getForRemotePublishingValidation(IThreePidSession session); + +} diff --git a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java similarity index 56% rename from src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java rename to src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java index 0bce14d..b327bea 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/generator/EmailInviteContentGenerator.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationGenerator.java @@ -18,13 +18,14 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.invitation.generator; +package io.kamax.mxisd.threepid.notification.email; -import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.config.MatrixConfig; -import io.kamax.mxisd.config.invite.medium.EmailInviteConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig; +import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig; import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; @@ -34,55 +35,68 @@ import org.springframework.stereotype.Component; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; @Component -public class EmailInviteContentGenerator implements IInviteContentGenerator { +public class EmailNotificationGenerator implements IEmailNotificationGenerator { private EmailConfig cfg; - private EmailInviteConfig invCfg; + private EmailTemplateConfig templateCfg; private MatrixConfig mxCfg; private ApplicationContext app; @Autowired // FIXME ApplicationContext shouldn't be injected, find another way from config (?) - public EmailInviteContentGenerator(EmailConfig cfg, EmailInviteConfig invCfg, MatrixConfig mxCfg, ApplicationContext app) { + public EmailNotificationGenerator(EmailConfig cfg, EmailTemplateConfig templateCfg, MatrixConfig mxCfg, ApplicationContext app) { this.cfg = cfg; - this.invCfg = invCfg; + this.templateCfg = templateCfg; this.mxCfg = mxCfg; this.app = app; } @Override - public String getMedium() { - return ThreePidMedium.Email.getId(); + public String getId() { + return "template"; + } + + private String getTemplateContent(String location) throws IOException { + InputStream is = StringUtils.startsWith(location, "classpath:") ? + app.getResource(location).getInputStream() : new FileInputStream(location); + return IOUtils.toString(is, StandardCharsets.UTF_8); + } + + private String populateCommon(String content, ThreePid recipient) { + String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); + + content = content.replace("%DOMAIN%", mxCfg.getDomain()); + content = content.replace("%DOMAIN_PRETTY%", domainPretty); + content = content.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom()); + content = content.replace("%FROM_NAME%", cfg.getIdentity().getName()); + content = content.replace("%RECIPIENT_MEDIUM%", recipient.getMedium()); + content = content.replace("%RECIPIENT_ADDRESS%", recipient.getAddress()); + return content; + } + + private String getTemplateAndPopulate(String location, ThreePid recipient) throws IOException { + return populateCommon(getTemplateContent(location), recipient); } @Override - public String generate(IThreePidInviteReply invite) { - if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) { - throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type"); - } - + public String get(IThreePidInviteReply invite) { try { - String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); + ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); + String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid); + String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); - String templateBody = IOUtils.toString( - StringUtils.startsWith(invCfg.getTemplate(), "classpath:") ? - app.getResource(invCfg.getTemplate()).getInputStream() : new FileInputStream(invCfg.getTemplate()), - StandardCharsets.UTF_8); - templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain()); - templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); - templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getFrom()); - templateBody = templateBody.replace("%FROM_NAME%", cfg.getName()); templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId()); templateBody = templateBody.replace("%SENDER_NAME%", senderName); templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId); - templateBody = templateBody.replace("%INVITE_MEDIUM%", invite.getInvite().getMedium()); - templateBody = templateBody.replace("%INVITE_ADDRESS%", invite.getInvite().getAddress()); + templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium()); + templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress()); templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId()); templateBody = templateBody.replace("%ROOM_NAME%", roomName); templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId); @@ -93,4 +107,14 @@ public class EmailInviteContentGenerator implements IInviteContentGenerator { } } + @Override + public String getForValidation(IThreePidSession session) { + return null; + } + + @Override + public String getForRemotePublishingValidation(IThreePidSession session) { + return null; + } + } diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java new file mode 100644 index 0000000..e0d9cfe --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/EmailNotificationHandler.java @@ -0,0 +1,71 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification.email; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.config.threepid.medium.EmailConfig; +import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.notification.INotificationHandler; +import io.kamax.mxisd.threepid.connector.email.IEmailConnector; +import io.kamax.mxisd.threepid.session.IThreePidSession; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class EmailNotificationHandler implements INotificationHandler { + + private EmailConfig cfg; + private IEmailNotificationGenerator generator; + private IEmailConnector connector; + + @Autowired + public EmailNotificationHandler(EmailConfig cfg, List generators, List connectors) { + generator = generators.stream() + .filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId())) + .findFirst() + .orElseThrow(() -> new ConfigurationException("Email notification generator [" + cfg.getGenerator() + "] could not be found")); + + connector = connectors.stream() + .filter(o -> StringUtils.equals(cfg.getConnector(), o.getId())) + .findFirst() + .orElseThrow(() -> new ConfigurationException("Email sender connector [" + cfg.getConnector() + "] could not be found")); + } + + @Override + public String getMedium() { + return ThreePidMedium.Email.getId(); + } + + @Override + public void notify(IThreePidInviteReply invite) { + + } + + @Override + public void notify(IThreePidSession session) { + + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java new file mode 100644 index 0000000..a7401ab --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/threepid/notification/email/IEmailNotificationGenerator.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.threepid.notification.email; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.threepid.notification.INotificationGenerator; + +public interface IEmailNotificationGenerator extends INotificationGenerator { + + @Override + default String getMedium() { + return ThreePidMedium.Email.getId(); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java index 28af430..d5c7dfc 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/IThreePidSession.java @@ -29,8 +29,6 @@ public interface IThreePidSession { String getId(); - String getHash(); - Instant getCreationTime(); String getServer(); @@ -43,6 +41,8 @@ public interface IThreePidSession { Optional getNextLink(); + String getToken(); + void validate(String token); boolean isValidated(); diff --git a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java index 8d0fcbc..4b37358 100644 --- a/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java +++ b/src/main/groovy/io/kamax/mxisd/threepid/session/ThreePidSession.java @@ -113,6 +113,11 @@ public class ThreePidSession implements IThreePidSession { return Optional.ofNullable(nextLink); } + @Override + public String getToken() { + return token; + } + public synchronized void setAttempt(int attempt) { if (isValidated()) { throw new IllegalStateException(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8dc80b6..82c1fec 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -63,18 +63,25 @@ forward: - "https://vector.im" threepid: - email: - connector: - active: 'smtp' - provider: - smtp: - port: 587 - tls: 1 - -invite: medium: email: - template: 'classpath:email/invite-template.eml' + identity: + from: '' + name: '' + connector: 'smtp' + generator: 'template' + connectors: + smtp: + host: '' + port: 587 + tls: 1 + login: '' + password: '' + generators: + template: + invite: 'classpath:email/invite-template.eml' + session: + validation: 'classpath:email/validate-template.eml' storage: backend: 'sqlite' diff --git a/src/main/resources/email/validate-template.eml b/src/main/resources/email/validate-template.eml new file mode 100644 index 0000000..0761712 --- /dev/null +++ b/src/main/resources/email/validate-template.eml @@ -0,0 +1,66 @@ +Subject: Your Matrix Validation Token +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ" + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline + +Hello, + +We have received a request to link this email address with a Matrix account. +If this was you who made this request, you may use the following link to complete the verification of your email address: + + %VALIDATION_LINK% + +If your client requires a code, the code is %VALIDATION_TOKEN% + +If you aren't aware of making such a request, please disregard this email. + +Regards, +%DOMAIN_PRETTY% Admins + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + + + + + +

Hello,

+ +

We have received a request to link this email address with a Matrix account. + If this was you who made this request, you may use the following link to + complete the verification of your email address:

+ +

Complete email verification

+ +

...or copy this link into your web browser:

+ +

%VALIDATION_LINK%

+ +

If your client requires a code, the code is %VALIDATION_TOKEN%

+ +

If you aren't aware of making such a request, please disregard this +email.

+ +
+

Regards,
+ %DOMAIN_PRETTY% Admins

+ + + + +--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--