Polishing, prepare for proxying 3PID sessions
This commit is contained in:
@@ -42,6 +42,7 @@ public class SessionConfig {
|
|||||||
public static class PolicySource {
|
public static class PolicySource {
|
||||||
|
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
private boolean alwaysValidate;
|
||||||
private boolean toLocal;
|
private boolean toLocal;
|
||||||
private boolean toRemote;
|
private boolean toRemote;
|
||||||
|
|
||||||
@@ -53,6 +54,14 @@ public class SessionConfig {
|
|||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAlwaysValidate() {
|
||||||
|
return alwaysValidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlwaysValidate(boolean alwaysValidate) {
|
||||||
|
this.alwaysValidate = alwaysValidate;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean toLocal() {
|
public boolean toLocal() {
|
||||||
return toLocal;
|
return toLocal;
|
||||||
}
|
}
|
||||||
@@ -98,6 +107,10 @@ public class SessionConfig {
|
|||||||
public PolicySource forRemote() {
|
public PolicySource forRemote() {
|
||||||
return forRemote;
|
return forRemote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PolicySource forIf(boolean isLocal) {
|
||||||
|
return isLocal ? forLocal : forRemote;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PolicyTemplate bind = new PolicyTemplate();
|
private PolicyTemplate bind = new PolicyTemplate();
|
||||||
|
@@ -45,13 +45,36 @@ public class EmailTemplateConfig {
|
|||||||
|
|
||||||
public static class Session {
|
public static class Session {
|
||||||
|
|
||||||
private String validation;
|
public static class SessionValidation {
|
||||||
|
|
||||||
public String getValidation() {
|
private String local;
|
||||||
|
private String remote;
|
||||||
|
|
||||||
|
public String getLocal() {
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocal(String local) {
|
||||||
|
this.local = local;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemote() {
|
||||||
|
return remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemote(String remote) {
|
||||||
|
this.remote = remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private SessionValidation validation;
|
||||||
|
|
||||||
|
public SessionValidation getValidation() {
|
||||||
return validation;
|
return validation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setValidation(String validation) {
|
public void setValidation(SessionValidation validation) {
|
||||||
this.validation = validation;
|
this.validation = validation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +99,9 @@ public class EmailTemplateConfig {
|
|||||||
public void build() {
|
public void build() {
|
||||||
log.info("--- E-mail Generator templates config ---");
|
log.info("--- E-mail Generator templates config ---");
|
||||||
log.info("Invite: {}", getName(getInvite()));
|
log.info("Invite: {}", getName(getInvite()));
|
||||||
log.info("Session validation: {}", getName(getSession().getValidation()));
|
log.info("Session validation:");
|
||||||
|
log.info("\tLocal: {}", getName(getSession().getValidation().getLocal()));
|
||||||
|
log.info("\tRemote: {}", getName(getSession().getValidation().getRemote()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.controller.v1.remote;
|
||||||
|
|
||||||
|
public class RemoteIdentityAPIv1 {
|
||||||
|
|
||||||
|
public static final String BASE = "/_matrix/identity-remote/api/v1";
|
||||||
|
|
||||||
|
}
|
@@ -20,14 +20,13 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.exception;
|
package io.kamax.mxisd.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
|
|
||||||
@ResponseStatus(value = HttpStatus.FORBIDDEN)
|
import org.apache.http.HttpStatus;
|
||||||
public class NotAllowedException extends RuntimeException {
|
|
||||||
|
public class NotAllowedException extends MatrixException {
|
||||||
|
|
||||||
public NotAllowedException(String s) {
|
public NotAllowedException(String s) {
|
||||||
super(s);
|
super(HttpStatus.SC_FORBIDDEN, "M_FORBIDDEN", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -31,4 +31,6 @@ public interface INotificationHandler {
|
|||||||
|
|
||||||
void sendForValidation(IThreePidSession session);
|
void sendForValidation(IThreePidSession session);
|
||||||
|
|
||||||
|
void sendForRemoteValidation(IThreePidSession session);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -63,7 +63,7 @@ public class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void sendforRemoteValidation(IThreePidSession session) {
|
public void sendforRemoteValidation(IThreePidSession session) {
|
||||||
throw new NotImplementedException("Remote publish of 3PID bind");
|
ensureMedium(session.getThreePid().getMedium()).sendForRemoteValidation(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -114,24 +114,14 @@ public class SessionMananger {
|
|||||||
} else {
|
} else {
|
||||||
log.info("No existing session for {}", tpid);
|
log.info("No existing session for {}", tpid);
|
||||||
|
|
||||||
boolean isLocalDomain = isLocal(tpid);
|
boolean isLocal = isLocal(tpid);
|
||||||
log.info("Is 3PID bound to local domain? {}", isLocalDomain);
|
log.info("Is 3PID bound to local domain? {}", isLocal);
|
||||||
|
|
||||||
if (isLocalDomain && (!policy.forLocal().isEnabled() || !policy.forLocal().toLocal())) {
|
// This might need a configuration by medium type?
|
||||||
throw new NotAllowedException("Validating local 3PID is not allowed");
|
SessionConfig.Policy.PolicyTemplate.PolicySource policySource = policy.forIf(isLocal);
|
||||||
}
|
if (!policySource.isEnabled() || (!policySource.toLocal() && !policySource.toRemote())) {
|
||||||
|
log.info("Session for {}: cancelled due to policy", tpid);
|
||||||
// We lookup if the 3PID is already known locally.
|
throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed");
|
||||||
boolean knownLocal = isKnownLocal(tpid);
|
|
||||||
log.info("Mapping with {} is " + (knownLocal ? "already" : "not") + " known locally", tpid);
|
|
||||||
|
|
||||||
if (!isLocalDomain && (
|
|
||||||
!policy.forRemote().isEnabled() || (
|
|
||||||
!policy.forRemote().toLocal() &&
|
|
||||||
!policy.forRemote().toRemote()
|
|
||||||
)
|
|
||||||
)) {
|
|
||||||
throw new NotAllowedException("Validating unknown remote 3PID is not allowed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String sessionId;
|
String sessionId;
|
||||||
@@ -144,14 +134,12 @@ public class SessionMananger {
|
|||||||
log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server);
|
log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server);
|
||||||
|
|
||||||
// This might need a configuration by medium type?
|
// This might need a configuration by medium type?
|
||||||
if (!isLocalDomain) {
|
if (policySource.toLocal()) {
|
||||||
if (policy.forRemote().toLocal() && policy.forRemote().toRemote()) {
|
log.info("Session {} for {}: sending local validation notification", sessionId, tpid);
|
||||||
log.info("Session {} for {}: sending local validation notification", sessionId, tpid);
|
notifMgr.sendForValidation(session);
|
||||||
notifMgr.sendForValidation(session);
|
} else {
|
||||||
} else {
|
log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid);
|
||||||
log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid);
|
notifMgr.sendforRemoteValidation(session);
|
||||||
notifMgr.sendforRemoteValidation(session);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.insertThreePidSession(session.getDao());
|
storage.insertThreePidSession(session.getDao());
|
||||||
|
@@ -33,6 +33,6 @@ public interface INotificationGenerator {
|
|||||||
|
|
||||||
String getForValidation(IThreePidSession session);
|
String getForValidation(IThreePidSession session);
|
||||||
|
|
||||||
String getForRemotePublishingValidation(IThreePidSession session);
|
String getForRemoteValidation(IThreePidSession session);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -26,8 +26,8 @@ import io.kamax.mxisd.config.ServerConfig;
|
|||||||
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
|
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
|
||||||
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig;
|
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig;
|
||||||
import io.kamax.mxisd.controller.v1.IdentityAPIv1;
|
import io.kamax.mxisd.controller.v1.IdentityAPIv1;
|
||||||
|
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
|
||||||
import io.kamax.mxisd.exception.InternalServerError;
|
import io.kamax.mxisd.exception.InternalServerError;
|
||||||
import io.kamax.mxisd.exception.NotImplementedException;
|
|
||||||
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
||||||
import io.kamax.mxisd.threepid.session.IThreePidSession;
|
import io.kamax.mxisd.threepid.session.IThreePidSession;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
@@ -121,8 +121,9 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public String getForValidation(IThreePidSession session) {
|
public String getForValidation(IThreePidSession session) {
|
||||||
log.info("Generating notification content for 3PID Session validation");
|
log.info("Generating notification content for 3PID Session validation");
|
||||||
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation(), session.getThreePid());
|
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid());
|
||||||
|
|
||||||
|
// FIXME should have a global link builder, most likely in the SDK?
|
||||||
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
|
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
|
||||||
"/validate/" + session.getThreePid().getMedium() +
|
"/validate/" + session.getThreePid().getMedium() +
|
||||||
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
|
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
|
||||||
@@ -135,8 +136,19 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getForRemotePublishingValidation(IThreePidSession session) {
|
public String getForRemoteValidation(IThreePidSession session) {
|
||||||
throw new NotImplementedException("");
|
log.info("Generating notification content for 3PID Session validation");
|
||||||
|
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid());
|
||||||
|
|
||||||
|
// FIXME should have a global link builder, specific to mxisd
|
||||||
|
String nextStepLink = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.BASE +
|
||||||
|
"/validate/requestToken?sid=" + session.getId() + "&client_secret=" + session.getSecret();
|
||||||
|
|
||||||
|
templateBody = templateBody.replace("%SESSION_ID%", session.getId());
|
||||||
|
templateBody = templateBody.replace("%SESSION_SECRET%", session.getSecret());
|
||||||
|
templateBody = templateBody.replace("%NEXT_STEP_LINK%", nextStepLink);
|
||||||
|
|
||||||
|
return templateBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -43,6 +43,7 @@ public class EmailNotificationHandler implements INotificationHandler {
|
|||||||
@Autowired
|
@Autowired
|
||||||
public EmailNotificationHandler(EmailConfig cfg, List<IEmailNotificationGenerator> generators, List<IEmailConnector> connectors) {
|
public EmailNotificationHandler(EmailConfig cfg, List<IEmailNotificationGenerator> generators, List<IEmailConnector> connectors) {
|
||||||
this.cfg = cfg;
|
this.cfg = cfg;
|
||||||
|
|
||||||
generator = generators.stream()
|
generator = generators.stream()
|
||||||
.filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId()))
|
.filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
@@ -59,24 +60,28 @@ public class EmailNotificationHandler implements INotificationHandler {
|
|||||||
return ThreePidMedium.Email.getId();
|
return ThreePidMedium.Email.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void send(String recipient, String content) {
|
||||||
public void sendForInvite(IThreePidInviteReply invite) {
|
|
||||||
connector.send(
|
connector.send(
|
||||||
cfg.getIdentity().getFrom(),
|
cfg.getIdentity().getFrom(),
|
||||||
cfg.getIdentity().getName(),
|
cfg.getIdentity().getName(),
|
||||||
invite.getInvite().getAddress(),
|
recipient,
|
||||||
generator.getForInvite(invite)
|
content
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendForInvite(IThreePidInviteReply invite) {
|
||||||
|
send(invite.getInvite().getAddress(), generator.getForInvite(invite));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendForValidation(IThreePidSession session) {
|
public void sendForValidation(IThreePidSession session) {
|
||||||
connector.send(
|
send(session.getThreePid().getAddress(), generator.getForValidation(session));
|
||||||
cfg.getIdentity().getFrom(),
|
}
|
||||||
cfg.getIdentity().getName(),
|
|
||||||
session.getThreePid().getAddress(),
|
@Override
|
||||||
generator.getForValidation(session)
|
public void sendForRemoteValidation(IThreePidSession session) {
|
||||||
);
|
send(session.getThreePid().getAddress(), generator.getForRemoteValidation(session));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -81,17 +81,19 @@ threepid:
|
|||||||
template:
|
template:
|
||||||
invite: 'classpath:email/invite-template.eml'
|
invite: 'classpath:email/invite-template.eml'
|
||||||
session:
|
session:
|
||||||
validation: 'classpath:email/validate-template.eml'
|
validation:
|
||||||
|
local: 'classpath:email/validate-local-template.eml'
|
||||||
|
remote: 'classpath:email/validate-remote-template.eml'
|
||||||
|
|
||||||
session.policy.validation:
|
session.policy.validation:
|
||||||
enabled: true
|
enabled: true
|
||||||
forLocal:
|
forLocal:
|
||||||
enabled: true
|
enabled: true
|
||||||
toLocal: true
|
toLocal: true # This should not be changed unless you know exactly the implications!
|
||||||
toRemote: true
|
toRemote: true
|
||||||
forRemote:
|
forRemote:
|
||||||
enabled: true
|
enabled: true
|
||||||
toLocal: true # This should not be changed unless you know exactly the implications!
|
toLocal: true
|
||||||
toRemote: true
|
toRemote: true
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
|
91
src/main/resources/email/validate-local-template.eml
Normal file
91
src/main/resources/email/validate-local-template.eml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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 there!
|
||||||
|
|
||||||
|
We have received a request to link this email address with your Matrix account.
|
||||||
|
|
||||||
|
If it was really you who made this request, you can click on the following link to
|
||||||
|
complete the verification of your email address:
|
||||||
|
|
||||||
|
%VALIDATION_LINK%
|
||||||
|
|
||||||
|
If you didn't make this request, you can safely disregard this email.
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
%DOMAIN_PRETTY% Admins
|
||||||
|
|
||||||
|
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
|
||||||
|
Content-Type: multipart/related;
|
||||||
|
boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR";
|
||||||
|
type="text/html"
|
||||||
|
|
||||||
|
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Disposition: inline
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page {
|
||||||
|
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
|
||||||
|
font-color: #454545;
|
||||||
|
font-size: 12pt;
|
||||||
|
width: 100%%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inner {
|
||||||
|
width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif_link a, .footer a {
|
||||||
|
color: #76CFA6 ! important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table id="page">
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td id="inner">
|
||||||
|
<p>Hello there!</p>
|
||||||
|
|
||||||
|
<p>We have received a request to link this email address with your Matrix account.</p>
|
||||||
|
|
||||||
|
<p>If it was really you who made this request, you can click on the following link to
|
||||||
|
complete the verification of your email address:</p>
|
||||||
|
|
||||||
|
<p><a href="%VALIDATION_LINK%">Complete email verification</a></p>
|
||||||
|
|
||||||
|
<p>If you didn't make this request, you can safely disregard this email.</p>
|
||||||
|
|
||||||
|
<p>Thanks!</p>
|
||||||
|
|
||||||
|
<p>%DOMAIN_PRETTY% Admins</p>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
|
||||||
|
|
||||||
|
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--
|
@@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title></title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 12pt;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Hello,</p>
|
|
||||||
|
|
||||||
<p>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:</p>
|
|
||||||
|
|
||||||
<p><a href="%VALIDATION_LINK%">Complete email verification</a></p>
|
|
||||||
|
|
||||||
<p>...or copy this link into your web browser:</p>
|
|
||||||
|
|
||||||
<p>%VALIDATION_LINK%</p>
|
|
||||||
|
|
||||||
<p>If your client requires a code, the code is %VALIDATION_TOKEN%</p>
|
|
||||||
|
|
||||||
<p>If you aren't aware of making such a request, please disregard this
|
|
||||||
email.</p>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<p>Regards,<br>
|
|
||||||
%DOMAIN_PRETTY% Admins</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--
|
|
Reference in New Issue
Block a user